@officexapp/vidfarm-devcli 0.21.33 → 0.21.35
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/.agents/skills/editor-capabilities/SKILL.md +13 -2
- package/.agents/skills/vidfarm/SKILL.md +73 -29
- package/.agents/skills/vidfarm/harnesses/README.md +112 -0
- package/.agents/skills/vidfarm/{regimes/explainer.QA_REGIME.md → harnesses/explainer.HARNESS.md} +22 -2
- package/.agents/skills/vidfarm/{regimes/hooks.QA_REGIME.md → harnesses/hooks.HARNESS.md} +3 -3
- package/.agents/skills/vidfarm/{regimes/product-demo.QA_REGIME.md → harnesses/product-demo.HARNESS.md} +19 -1
- package/.agents/skills/vidfarm/{regimes/short-form.QA_REGIME.md → harnesses/short-form.HARNESS.md} +67 -7
- package/.agents/skills/vidfarm/{regimes/ugc-testimonial.QA_REGIME.md → harnesses/ugc-testimonial.HARNESS.md} +10 -3
- package/.agents/skills/vidfarm/recipes/{bulk-scripting-with-a-regime.md → bulk-scripting-with-a-harness.md} +35 -12
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +46 -2
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +2 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +84 -22
- package/.agents/skills/vidfarm/references/editor-workflows.md +19 -4
- package/.agents/skills/vidfarm/references/hooks-and-virality.md +62 -5
- package/.agents/skills/vidfarm/references/reviewing-renders.md +140 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -2
- package/.agents/skills/vidfarm-media/references/tts.md +26 -4
- package/SKILL.director.md +462 -75
- package/SKILL.md +33 -14
- package/dist/src/cli.js +799 -91
- package/dist/src/devcli/{qa-regime.js → harness.js} +132 -55
- package/dist/src/devcli/qa-check.js +209 -4
- package/dist/src/devcli/skill-docs.js +136 -0
- package/dist/src/devcli/stills.js +65 -1
- package/package.json +4 -3
- package/.agents/skills/vidfarm/regimes/README.md +0 -77
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
//
|
|
1
|
+
// HARNESS.md — the reusable AI harness for one format or template.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// A HARNESS is the written apparatus that lets an agent reproduce what makes a
|
|
4
|
+
// format good, over and over, without the director in the room. It is the unit
|
|
5
|
+
// the product is organised around: "create me a harness", "update the harness
|
|
6
|
+
// for this format", and "give me the harness for this template_id" are all the
|
|
7
|
+
// same noun, arrived at from different directions.
|
|
8
|
+
//
|
|
9
|
+
// TWO PROVENANCES, ONE ARTIFACT:
|
|
10
|
+
// • DERIVED — `vidfarm harness derive <forkId|dir>` distils a decomposed
|
|
11
|
+
// template's DNA (viral / visual / structural / audio / build) into a
|
|
12
|
+
// HARNESS.md. This is what "the harness for this template_id" means.
|
|
13
|
+
// • AUTHORED — `vidfarm harness init <base>` copies a bundled starting point
|
|
14
|
+
// the director then edits into their own format contract.
|
|
15
|
+
// Both produce the same file, and a derived one is still meant to be edited.
|
|
16
|
+
//
|
|
17
|
+
// WHY IT ISN'T CODE: `vidfarm qa`'s built-in rules are UNIVERSAL (no HTML slop,
|
|
18
|
+
// the caption font regime, the thumbnail frame) — the same for every video
|
|
19
|
+
// anyone makes, so they live in code. A harness is the opposite: it is what
|
|
20
|
+
// makes THIS format good, and it changes per account, per offer, per campaign,
|
|
21
|
+
// per source template. So it lives next to the work as Markdown the director
|
|
22
|
+
// owns, edits, versions, and hands to the next agent.
|
|
9
23
|
//
|
|
10
24
|
// It matters most in SCRIPTING MODE. One video gets human eyes on every frame;
|
|
11
|
-
// fifty variants generated in a loop do not. The
|
|
25
|
+
// fifty variants generated in a loop do not. The harness is what a bulk run is
|
|
12
26
|
// graded against — the thing that keeps variant #37 as good as variant #1.
|
|
13
27
|
//
|
|
14
|
-
// THE FORMAT is a plain Markdown doc with
|
|
28
|
+
// THE FORMAT is a plain Markdown doc with three machine-readable affordances:
|
|
15
29
|
//
|
|
16
30
|
// 1. Optional YAML-ish front matter with a `checks:` block. These are the
|
|
17
31
|
// assertions the CLI can settle DETERMINISTICALLY from the composition DOM
|
|
@@ -21,27 +35,34 @@
|
|
|
21
35
|
// a question the CLI hands back for the agent (or the human) to answer.
|
|
22
36
|
// "Is the withheld answer one the viewer can't supply themselves?" is a
|
|
23
37
|
// judgment call; pretending a linter can settle it would be a lie.
|
|
38
|
+
// 3. Any heading containing "DNA" is indexed as a DNA SECTION, so a harness
|
|
39
|
+
// speaks the same vocabulary the decompose pass writes into the template
|
|
40
|
+
// JSON (viral_dna, visual_dna, …) and an agent can ask for one strand by
|
|
41
|
+
// name instead of re-reading the whole doc.
|
|
24
42
|
//
|
|
25
43
|
// Everything else is prose the agent reads for context. That split is the whole
|
|
26
44
|
// design: the CLI is honest about which half it can enforce, and it never
|
|
27
45
|
// silently "passes" a video on the strength of the half it can't.
|
|
28
46
|
//
|
|
29
|
-
// COMPOSABLE BY CONSTRUCTION:
|
|
30
|
-
// from a built-in name OR any path the user points at, and a
|
|
31
|
-
//
|
|
32
|
-
// feedback-not-a-gate contract as the rest of `vidfarm qa`.
|
|
47
|
+
// COMPOSABLE BY CONSTRUCTION: harnesses stack (`--harness a --harness b`),
|
|
48
|
+
// resolve from a built-in name OR any path the user points at, and a
|
|
49
|
+
// directory's own HARNESS.md is picked up automatically. Nothing here is a gate
|
|
50
|
+
// — same feedback-not-a-gate contract as the rest of `vidfarm qa`.
|
|
33
51
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
34
52
|
import path from "node:path";
|
|
35
53
|
import { fileURLToPath } from "node:url";
|
|
36
|
-
const BUILTIN_SUFFIX = ".
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
54
|
+
const BUILTIN_SUFFIX = ".HARNESS.md";
|
|
55
|
+
/** The canonical filename, plus the legacy names we still read (never write). */
|
|
56
|
+
export const HARNESS_FILENAME = "HARNESS.md";
|
|
57
|
+
const DISCOVERY_NAMES = ["HARNESS.md", "harness.md", "QA_REGIME.md", "qa_regime.md", "QA-REGIME.md"];
|
|
58
|
+
// ── Locating the built-in harnesses ──────────────────────────────────────────
|
|
59
|
+
// They ship inside the skill pack (.agents/skills/vidfarm/harnesses/) rather
|
|
60
|
+
// than as TS string constants, so a director can read, diff, and copy them as
|
|
61
|
+
// normal files — the file IS the documentation.
|
|
41
62
|
function builtinDir() {
|
|
42
63
|
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
43
64
|
for (let i = 0; i < 6; i += 1) {
|
|
44
|
-
const candidate = path.join(dir, ".agents", "skills", "vidfarm", "
|
|
65
|
+
const candidate = path.join(dir, ".agents", "skills", "vidfarm", "harnesses");
|
|
45
66
|
if (existsSync(candidate))
|
|
46
67
|
return candidate;
|
|
47
68
|
const parent = path.dirname(dir);
|
|
@@ -51,7 +72,7 @@ function builtinDir() {
|
|
|
51
72
|
}
|
|
52
73
|
return null;
|
|
53
74
|
}
|
|
54
|
-
export function
|
|
75
|
+
export function listBuiltinHarnesses() {
|
|
55
76
|
const dir = builtinDir();
|
|
56
77
|
if (!dir)
|
|
57
78
|
return [];
|
|
@@ -60,12 +81,12 @@ export function listBuiltinRegimes() {
|
|
|
60
81
|
.sort()
|
|
61
82
|
.map((file) => {
|
|
62
83
|
const full = path.join(dir, file);
|
|
63
|
-
const parsed =
|
|
84
|
+
const parsed = parseHarness(readFileSync(full, "utf8"), full);
|
|
64
85
|
return { name: file.slice(0, -BUILTIN_SUFFIX.length), path: full, video_type: parsed.video_type, summary: parsed.summary };
|
|
65
86
|
});
|
|
66
87
|
}
|
|
67
|
-
/** Resolve `hooks` (built-in) or `./my/
|
|
68
|
-
export function
|
|
88
|
+
/** Resolve `hooks` (built-in) or `./my/HARNESS.md` (a path) to a file. */
|
|
89
|
+
export function resolveHarnessPath(ref) {
|
|
69
90
|
const direct = path.resolve(ref);
|
|
70
91
|
if (existsSync(direct) && !direct.endsWith(path.sep))
|
|
71
92
|
return direct;
|
|
@@ -75,18 +96,28 @@ export function resolveRegimePath(ref) {
|
|
|
75
96
|
if (existsSync(candidate))
|
|
76
97
|
return candidate;
|
|
77
98
|
}
|
|
78
|
-
const names =
|
|
79
|
-
throw new Error(`No
|
|
80
|
-
`Scaffold your own with \`vidfarm
|
|
99
|
+
const names = listBuiltinHarnesses().map((entry) => entry.name);
|
|
100
|
+
throw new Error(`No harness "${ref}". Pass a file path, or one of the built-ins: ${names.join(", ") || "(none bundled)"}. ` +
|
|
101
|
+
`Scaffold your own with \`vidfarm harness init <name> --out ./work/HARNESS.md\`, ` +
|
|
102
|
+
`or derive one from a decomposed template with \`vidfarm harness derive <forkId>\`.`);
|
|
81
103
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
/**
|
|
105
|
+
* A directory's own harness, if it has one. `HARNESS.md` is canonical; the
|
|
106
|
+
* legacy `QA_REGIME.md` names are still read so existing work directories keep
|
|
107
|
+
* working, but nothing writes them any more.
|
|
108
|
+
*/
|
|
109
|
+
export function discoverHarness(dir) {
|
|
110
|
+
for (const name of DISCOVERY_NAMES) {
|
|
85
111
|
const candidate = path.join(dir, name);
|
|
86
112
|
if (existsSync(candidate))
|
|
87
113
|
return candidate;
|
|
88
114
|
}
|
|
89
|
-
|
|
115
|
+
const nested = path.join(dir, ".harness", HARNESS_FILENAME);
|
|
116
|
+
return existsSync(nested) ? nested : null;
|
|
117
|
+
}
|
|
118
|
+
/** True for the legacy filenames, so callers can nudge without breaking. */
|
|
119
|
+
export function isLegacyHarnessFilename(file) {
|
|
120
|
+
return /^qa[_-]regime\.md$/i.test(path.basename(file));
|
|
90
121
|
}
|
|
91
122
|
/**
|
|
92
123
|
* A deliberately tiny front-matter reader: `key: value`, and one level of
|
|
@@ -149,9 +180,15 @@ function parseFrontMatter(raw) {
|
|
|
149
180
|
const KNOWN_CHECKS = new Set([
|
|
150
181
|
"duration_sec", "aspect", "first_frame_visual", "first_frame_text", "hook_words_max",
|
|
151
182
|
"text_by_sec", "audio", "captions", "font_regime", "safe_zone", "forbid_text",
|
|
152
|
-
"require_text", "max_text_cards", "max_simultaneous_text", "
|
|
183
|
+
"require_text", "max_text_cards", "max_simultaneous_text", "max_words_per_cue", "max_dead_air_sec",
|
|
184
|
+
"max_tail_sec", "scenes", "max_scene_sec"
|
|
153
185
|
]);
|
|
154
|
-
|
|
186
|
+
/** "Visual DNA — the look" → `visual_dna`. Matches the decompose JSON keys. */
|
|
187
|
+
function dnaKey(heading) {
|
|
188
|
+
const head = heading.split(/[—–:(]/)[0];
|
|
189
|
+
return head.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
190
|
+
}
|
|
191
|
+
export function parseHarness(raw, source) {
|
|
155
192
|
const { front, body } = parseFrontMatter(raw);
|
|
156
193
|
const checks = {};
|
|
157
194
|
const unknown = [];
|
|
@@ -165,13 +202,27 @@ export function parseRegime(raw, source) {
|
|
|
165
202
|
unknown.push(bare);
|
|
166
203
|
}
|
|
167
204
|
const reviewItems = [];
|
|
205
|
+
const dna = [];
|
|
206
|
+
let openDna = null;
|
|
207
|
+
let openDnaLevel = 0;
|
|
168
208
|
let section = "Checklist";
|
|
169
209
|
for (const line of body.split(/\r?\n/)) {
|
|
170
|
-
const heading = line.match(
|
|
210
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
171
211
|
if (heading) {
|
|
172
|
-
section = heading[
|
|
212
|
+
section = heading[2].replace(/[*_`]/g, "").trim();
|
|
213
|
+
// A "…DNA" heading opens a strand; the next heading at the same level or
|
|
214
|
+
// shallower closes it, so nested sub-headings stay inside their strand.
|
|
215
|
+
if (openDna && heading[1].length <= openDnaLevel)
|
|
216
|
+
openDna = null;
|
|
217
|
+
if (/\bdna\b/i.test(section)) {
|
|
218
|
+
openDna = { key: dnaKey(section), heading: section, body: "" };
|
|
219
|
+
openDnaLevel = heading[1].length;
|
|
220
|
+
dna.push(openDna);
|
|
221
|
+
}
|
|
173
222
|
continue;
|
|
174
223
|
}
|
|
224
|
+
if (openDna)
|
|
225
|
+
openDna.body += `${line}\n`;
|
|
175
226
|
const bold = line.match(/^\*\*(.+?)\*\*\s*$/);
|
|
176
227
|
if (bold) {
|
|
177
228
|
section = bold[1].trim();
|
|
@@ -181,14 +232,19 @@ export function parseRegime(raw, source) {
|
|
|
181
232
|
if (box)
|
|
182
233
|
reviewItems.push({ section, text: box[2].trim() });
|
|
183
234
|
}
|
|
235
|
+
for (const strand of dna)
|
|
236
|
+
strand.body = strand.body.trim();
|
|
184
237
|
const headline = body.split(/\r?\n/).find((line) => line.trim() && !line.startsWith("#") && !line.startsWith(">"));
|
|
185
|
-
const name = String(front.name ?? path.basename(source).replace(/\.md$/i, "").replace(/\.QA_REGIME$/i, ""));
|
|
238
|
+
const name = String(front.name ?? path.basename(source).replace(/\.md$/i, "").replace(/\.(HARNESS|QA_REGIME)$/i, ""));
|
|
186
239
|
return {
|
|
187
240
|
name,
|
|
188
241
|
source,
|
|
189
242
|
video_type: front.video_type ? String(front.video_type) : null,
|
|
190
243
|
summary: headline ? headline.replace(/[*_`]/g, "").trim().slice(0, 160) : null,
|
|
244
|
+
derived_from: front.derived_from ? String(front.derived_from) : null,
|
|
245
|
+
source_template_id: front.source_template_id ? String(front.source_template_id) : null,
|
|
191
246
|
checks,
|
|
247
|
+
dna,
|
|
192
248
|
review_items: reviewItems,
|
|
193
249
|
unknown_check_keys: unknown
|
|
194
250
|
};
|
|
@@ -225,15 +281,15 @@ function isRequired(value) {
|
|
|
225
281
|
function isForbidden(value) {
|
|
226
282
|
return String(value ?? "").trim().toLowerCase() === "forbidden";
|
|
227
283
|
}
|
|
228
|
-
export function
|
|
284
|
+
export function evaluateHarness(parsed, facts) {
|
|
229
285
|
const checks = [];
|
|
230
286
|
const findings = [];
|
|
231
|
-
const where = `
|
|
287
|
+
const where = `harness "${parsed.name}"`;
|
|
232
288
|
const settle = (key, expected, actual, ok, fix) => {
|
|
233
289
|
checks.push({ key, expected, actual, ok });
|
|
234
290
|
if (!ok) {
|
|
235
291
|
findings.push({
|
|
236
|
-
rule: `
|
|
292
|
+
rule: `harness:${key}`,
|
|
237
293
|
severity: "error",
|
|
238
294
|
message: `${parsed.name}: ${key} expects ${expected} — this composition is ${actual}.`,
|
|
239
295
|
where,
|
|
@@ -244,7 +300,7 @@ export function evaluateRegime(parsed, facts) {
|
|
|
244
300
|
const c = parsed.checks;
|
|
245
301
|
if (c.duration_sec !== undefined && facts.duration_sec !== null) {
|
|
246
302
|
const spec = String(c.duration_sec);
|
|
247
|
-
settle("duration_sec", spec, `${facts.duration_sec.toFixed(1)}s`, rangeSatisfied(spec, facts.duration_sec), "Retime the timeline (`vidfarm retime` / `vidfarm ripple`) or set the composition duration to land inside the
|
|
303
|
+
settle("duration_sec", spec, `${facts.duration_sec.toFixed(1)}s`, rangeSatisfied(spec, facts.duration_sec), "Retime the timeline (`vidfarm retime` / `vidfarm ripple`) or set the composition duration to land inside the harness's window.");
|
|
248
304
|
}
|
|
249
305
|
if (c.aspect !== undefined && facts.canvas.aspect) {
|
|
250
306
|
const allowed = String(c.aspect).split(/[|,]/).map((entry) => entry.trim());
|
|
@@ -259,7 +315,7 @@ export function evaluateRegime(parsed, facts) {
|
|
|
259
315
|
settle("first_frame_text", "hook words up at t=0", has ? `"${facts.first_frame_text}"` : "no text at t=0", has, "Start the hook caption at 0 so the thumbnail states the promise before a word is spoken (muted autoplay is the default).");
|
|
260
316
|
}
|
|
261
317
|
else if (isForbidden(c.first_frame_text)) {
|
|
262
|
-
settle("first_frame_text", "no text at t=0", has ? `"${facts.first_frame_text}"` : "clean", !has, "Delay the first text layer past 0 — this
|
|
318
|
+
settle("first_frame_text", "no text at t=0", has ? `"${facts.first_frame_text}"` : "clean", !has, "Delay the first text layer past 0 — this harness wants the opening frame to carry the image alone.");
|
|
263
319
|
}
|
|
264
320
|
}
|
|
265
321
|
if (c.hook_words_max !== undefined) {
|
|
@@ -282,7 +338,7 @@ export function evaluateRegime(parsed, facts) {
|
|
|
282
338
|
settle("audio", "at least one audio layer", `${facts.audio_layers}`, facts.audio_layers > 0, "Add narration or a bed (`vidfarm tts` free local voice, or `vidfarm media search --type bgm`).");
|
|
283
339
|
}
|
|
284
340
|
else if (isForbidden(c.audio)) {
|
|
285
|
-
settle("audio", "no audio layers", `${facts.audio_layers}`, facts.audio_layers === 0, "Remove the audio layers — this
|
|
341
|
+
settle("audio", "no audio layers", `${facts.audio_layers}`, facts.audio_layers === 0, "Remove the audio layers — this harness is for silent/overlay output.");
|
|
286
342
|
}
|
|
287
343
|
}
|
|
288
344
|
if (isRequired(c.captions)) {
|
|
@@ -298,11 +354,11 @@ export function evaluateRegime(parsed, facts) {
|
|
|
298
354
|
}
|
|
299
355
|
for (const phrase of asList(c.forbid_text)) {
|
|
300
356
|
const hit = phrase && new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(facts.all_text);
|
|
301
|
-
settle("forbid_text", `no "${phrase}" on screen`, hit ? `found "${phrase}"` : "absent", !hit, "Rewrite the line. The
|
|
357
|
+
settle("forbid_text", `no "${phrase}" on screen`, hit ? `found "${phrase}"` : "absent", !hit, "Rewrite the line. The harness bans this phrasing for a reason recorded in the doc — read the section it sits under.");
|
|
302
358
|
}
|
|
303
359
|
for (const phrase of asList(c.require_text)) {
|
|
304
360
|
const hit = phrase && new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(facts.all_text);
|
|
305
|
-
settle("require_text", `"${phrase}" appears on screen`, hit ? "present" : "missing", Boolean(hit), "Add the required line as a timed caption — this is usually a compliance or positioning beat the
|
|
361
|
+
settle("require_text", `"${phrase}" appears on screen`, hit ? "present" : "missing", Boolean(hit), "Add the required line as a timed caption — this is usually a compliance or positioning beat the harness treats as mandatory.");
|
|
306
362
|
}
|
|
307
363
|
if (c.max_text_cards !== undefined) {
|
|
308
364
|
const max = Number(c.max_text_cards);
|
|
@@ -316,9 +372,27 @@ export function evaluateRegime(parsed, facts) {
|
|
|
316
372
|
settle("max_simultaneous_text", `≤${max} text runs at once`, `${facts.max_simultaneous_text}`, facts.max_simultaneous_text <= max, "Stagger the overlapping text layers. Two things to read at once means neither gets read.");
|
|
317
373
|
}
|
|
318
374
|
}
|
|
375
|
+
if (c.max_words_per_cue !== undefined) {
|
|
376
|
+
const max = Number(c.max_words_per_cue);
|
|
377
|
+
if (Number.isFinite(max)) {
|
|
378
|
+
settle("max_words_per_cue", `≤${max} words in any one text run`, `${facts.max_words_in_run}`, facts.max_words_in_run <= max, "Page the long run into kinetic cues — `vidfarm captions generate <dir> --style word-pop` splits narration into ~3–5-word cues on real word timings. A paragraph on screen gets read by nobody.");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (c.max_dead_air_sec !== undefined) {
|
|
382
|
+
const max = Number(c.max_dead_air_sec);
|
|
383
|
+
if (Number.isFinite(max)) {
|
|
384
|
+
settle("max_dead_air_sec", `≤${max}s with nothing to read`, `${facts.max_dead_air_sec}s`, facts.max_dead_air_sec <= max, "Run the deletion test on that stretch and cut it, then CLOSE the hole with `vidfarm ripple <dir> --at <sec> --delta -<sec>`. Dead screen time is a free exit.");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (c.max_tail_sec !== undefined) {
|
|
388
|
+
const max = Number(c.max_tail_sec);
|
|
389
|
+
if (Number.isFinite(max)) {
|
|
390
|
+
settle("max_tail_sec", `≤${max}s after the last word`, `${facts.tail_silence_sec}s`, facts.tail_silence_sec <= max, "End on the bait. Trim the trailing clip and pull the composition duration in so the last frame is the one that loops best.");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
319
393
|
if (c.scenes !== undefined) {
|
|
320
394
|
const spec = String(c.scenes);
|
|
321
|
-
settle("scenes", `${spec} visual clips`, `${facts.visual_clips.length}`, rangeSatisfied(spec, facts.visual_clips.length), "Split or merge scenes to hit the
|
|
395
|
+
settle("scenes", `${spec} visual clips`, `${facts.visual_clips.length}`, rangeSatisfied(spec, facts.visual_clips.length), "Split or merge scenes to hit the harness's pacing shape (`vidfarm split` / `vidfarm retime`).");
|
|
322
396
|
}
|
|
323
397
|
if (c.max_scene_sec !== undefined) {
|
|
324
398
|
const max = Number(c.max_scene_sec);
|
|
@@ -329,7 +403,7 @@ export function evaluateRegime(parsed, facts) {
|
|
|
329
403
|
}
|
|
330
404
|
for (const key of parsed.unknown_check_keys) {
|
|
331
405
|
findings.push({
|
|
332
|
-
rule: "
|
|
406
|
+
rule: "harness:unknown-check",
|
|
333
407
|
severity: "warn",
|
|
334
408
|
message: `${parsed.name}: unknown check "${key}" — ignored (the CLI can't settle it).`,
|
|
335
409
|
where,
|
|
@@ -340,22 +414,23 @@ export function evaluateRegime(parsed, facts) {
|
|
|
340
414
|
name: parsed.name,
|
|
341
415
|
source: parsed.source,
|
|
342
416
|
video_type: parsed.video_type,
|
|
417
|
+
dna: parsed.dna.map((strand) => strand.key),
|
|
343
418
|
checks,
|
|
344
419
|
review_items: parsed.review_items,
|
|
345
420
|
findings
|
|
346
421
|
};
|
|
347
422
|
}
|
|
348
|
-
export function
|
|
349
|
-
const file =
|
|
350
|
-
return
|
|
423
|
+
export function loadAndEvaluateHarness(ref, facts) {
|
|
424
|
+
const file = resolveHarnessPath(ref);
|
|
425
|
+
return evaluateHarness(parseHarness(readFileSync(file, "utf8"), file), facts);
|
|
351
426
|
}
|
|
352
427
|
/**
|
|
353
|
-
* Fold
|
|
428
|
+
* Fold harness results into a QaReport. Harness failures are the DIRECTOR's own
|
|
354
429
|
* rules, so they land in the same errors list as the built-ins — but the verdict
|
|
355
430
|
* stays advisory: `vidfarm qa` still exits 0 unless the caller asked for
|
|
356
431
|
* --strict, exactly as it does for slop.
|
|
357
432
|
*/
|
|
358
|
-
export function
|
|
433
|
+
export function mergeHarnessIntoReport(report, evaluations) {
|
|
359
434
|
const findings = evaluations.flatMap((evaluation) => evaluation.findings);
|
|
360
435
|
const errors = [...report.errors, ...findings.filter((finding) => finding.severity === "error")];
|
|
361
436
|
const warnings = [...report.warnings, ...findings.filter((finding) => finding.severity === "warn")];
|
|
@@ -365,17 +440,19 @@ export function mergeRegimeIntoReport(report, evaluations) {
|
|
|
365
440
|
verdict: errors.length ? "slop" : warnings.length ? "warnings" : "clean",
|
|
366
441
|
errors,
|
|
367
442
|
warnings,
|
|
368
|
-
|
|
443
|
+
harnesses: evaluations
|
|
369
444
|
};
|
|
370
445
|
}
|
|
371
|
-
/** Human-readable
|
|
372
|
-
export function
|
|
446
|
+
/** Human-readable harness block for the CLI (findings are printed by qa itself). */
|
|
447
|
+
export function formatHarnessReport(evaluation, colors) {
|
|
373
448
|
const { green, red, dim, reset } = colors;
|
|
374
449
|
const lines = [];
|
|
375
450
|
const failed = evaluation.checks.filter((check) => !check.ok).length;
|
|
376
|
-
lines.push(`${dim}
|
|
451
|
+
lines.push(`${dim}harness${reset} ${evaluation.name}${evaluation.video_type ? ` ${dim}(${evaluation.video_type})${reset}` : ""} ` +
|
|
377
452
|
`${dim}— ${evaluation.checks.length - failed}/${evaluation.checks.length} machine checks passed, ` +
|
|
378
453
|
`${evaluation.review_items.length} item(s) need your judgment${reset}`);
|
|
454
|
+
if (evaluation.dna.length)
|
|
455
|
+
lines.push(` ${dim}DNA documented: ${evaluation.dna.join(", ")}${reset}`);
|
|
379
456
|
for (const check of evaluation.checks) {
|
|
380
457
|
const mark = check.ok ? `${green}✓${reset}` : `${red}✗${reset}`;
|
|
381
458
|
lines.push(` ${mark} ${check.key} ${dim}expects ${check.expected} — got ${check.actual}${reset}`);
|
|
@@ -393,4 +470,4 @@ export function formatRegimeReport(evaluation, colors) {
|
|
|
393
470
|
}
|
|
394
471
|
return lines.join("\n");
|
|
395
472
|
}
|
|
396
|
-
//# sourceMappingURL=
|
|
473
|
+
//# sourceMappingURL=harness.js.map
|