@litfamily/litopencode 1.0.29 → 1.0.31
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.
- package/CHANGELOG.md +22 -0
- package/README-Ko-KR.md +7 -7
- package/README.md +7 -7
- package/dist/activation-primary-prompts.js +15 -6
- package/dist/activation-prompt-utils.d.ts +2 -2
- package/dist/activation-prompt-utils.js +9 -3
- package/dist/activation-reentry.d.ts +29 -0
- package/dist/activation-reentry.js +71 -0
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +40 -7
- package/dist/activation-workflow-prompts.d.ts +2 -0
- package/dist/activation-workflow-prompts.js +32 -7
- package/dist/activation.d.ts +6 -1
- package/dist/activation.js +34 -7
- package/dist/cli/args.js +14 -0
- package/dist/cli/auto-update.js +2 -4
- package/dist/cli/command-plan.d.ts +40 -0
- package/dist/cli/command-plan.js +62 -0
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +65 -4
- package/dist/cli/host-detect.d.ts +58 -0
- package/dist/cli/host-detect.js +199 -0
- package/dist/cli/install-report.js +3 -1
- package/dist/cli/install.d.ts +9 -1
- package/dist/cli/install.js +134 -31
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +5 -3
- package/dist/cli/office-dependencies.d.ts +2 -0
- package/dist/cli/office-dependencies.js +7 -3
- package/dist/cli/scientific-visualization-dependencies.js +1 -1
- package/dist/cli/tui-config.d.ts +27 -2
- package/dist/cli/tui-config.js +77 -2
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli.js +9 -6
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +17 -3
- package/dist/features.d.ts +27 -1
- package/dist/features.js +40 -8
- package/dist/host/server.d.ts +7 -0
- package/dist/host/server.js +12 -0
- package/dist/host/tui.d.ts +7 -0
- package/dist/host/tui.js +12 -0
- package/dist/server.js +9 -0
- package/dist/skill-renames.d.ts +1 -1
- package/dist/skills.d.ts +9 -2
- package/dist/skills.js +22 -8
- package/dist/tui/mission-source-v2.d.ts +51 -0
- package/dist/tui/mission-source-v2.js +52 -0
- package/dist/v2/agents.d.ts +105 -0
- package/dist/v2/agents.js +185 -0
- package/dist/v2/commands.d.ts +44 -0
- package/dist/v2/commands.js +65 -0
- package/dist/v2/context.d.ts +67 -0
- package/dist/v2/context.js +17 -0
- package/dist/v2/events.d.ts +55 -0
- package/dist/v2/events.js +193 -0
- package/dist/v2/hooks.d.ts +39 -0
- package/dist/v2/hooks.js +61 -0
- package/dist/v2/server.d.ts +2 -0
- package/dist/v2/server.js +68 -0
- package/dist/v2/session-hooks.d.ts +64 -0
- package/dist/v2/session-hooks.js +230 -0
- package/dist/v2/tools.d.ts +26 -0
- package/dist/v2/tools.js +49 -0
- package/dist/v2/tui.d.ts +63 -0
- package/dist/v2/tui.js +259 -0
- package/docs/assets/cover-motion-still.webp +0 -0
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/readme/litfamily-machines.png +0 -0
- package/docs/migration.md +4 -3
- package/docs/reference-Ko-KR.md +109 -7
- package/docs/reference.md +122 -9
- package/package.json +10 -5
- package/skills/deep-interview/SKILL.md +1 -1
- package/skills/lit-brainstorm/SKILL.md +299 -0
- package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
- package/skills/lit-brainstorm/references/lenses.md +106 -0
- package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
- package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-docx/run.py +42 -4
- package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
- package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
- package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
- package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
- package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
- package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
- package/skills/lit-humanizer/scripts/detect.mjs +1 -1
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/lit-pptx/command.mjs +94 -0
- package/skills/lit-pptx/run.mjs +16 -6
- package/skills/lit-pptx/scripts/compile-deck.js +2 -2
- package/skills/lit-typographic-motion/SKILL.md +5 -2
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
- package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
- package/skills/lit-typographic-motion/engine/command.mjs +94 -0
- package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
- package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
- package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
- package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
- package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
- package/skills/lit-typographic-motion/engine/look.mjs +113 -10
- package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
- package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
- package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
- package/skills/lit-typographic-motion/fonts/pins.json +3 -1
- package/skills/lit-typographic-motion/gate.mjs +34 -1
- package/skills/lit-typographic-motion/probe.mjs +8 -3
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
- package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
- package/skills/lit-typographic-motion/references/cover.md +423 -0
- package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
- package/skills/lit-typographic-motion/references/runtime.md +17 -2
- package/skills/lit-typographic-motion/references/stage.md +3 -0
- package/skills/lit-typographic-motion/references/treatment.md +4 -0
- package/skills/lit-typographic-motion/render.mjs +30 -6
- package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
- package/skills/lit-typographic-motion/runtime.mjs +12 -13
- package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
- package/skills/managed-skill-manifest.json +176 -32
- package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/skills/review-work/SKILL.md +27 -18
- package/skills/skill-rename-aliases.json +2 -1
- package/tools/check-pack-payload.mjs +4 -1
- package/tools/payload-substance-allowlist.json +1 -1
- package/tools/payload-substance-parity.json +6 -4
- package/tools/qa-cover-recipes.mjs +167 -0
- 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
|
-
|
|
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 =
|
|
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 >
|
|
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 (
|
|
85
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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"),
|
|
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 });
|