@glissade/cli 0.49.0-pre.1 → 0.50.0-pre.0
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/dist/cli.js +19 -4
- package/dist/parity.js +232 -4
- package/package.json +10 -10
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,8 @@ const KNOWN_BOOLEAN_FLAGS = new Set([
|
|
|
29
29
|
"lottie",
|
|
30
30
|
"lint",
|
|
31
31
|
"global",
|
|
32
|
-
"iife"
|
|
32
|
+
"iife",
|
|
33
|
+
"update-baseline"
|
|
33
34
|
]);
|
|
34
35
|
function parseArgs(argv) {
|
|
35
36
|
const positional = [];
|
|
@@ -99,7 +100,7 @@ const USAGE = `usage:
|
|
|
99
100
|
gs types [--out <file.ts>] [--from <api.json>] [--check] [--global] codegen a type-checked track() SDK from the describe() manifest: only registered animatable paths + their value types compile, so a typo'd path or wrong value-type id is a COMPILE error (import track from the generated file). --check fails if --out is stale. Zero-runtime (types + a re-typed re-export of the real track). --global (alias --iife) instead emits a SELF-CONTAINED ambient window.glissade .d.ts for the no-build <script src> author (typed IIFE surface — a typo'd window.glissade member is a compile error)
|
|
100
101
|
gs migrate <baseline-api.json> [--json] [--check] diff a saved API manifest against the current engine: moved imports / removed / added / changed, with a suggested fix per breaking item (advisory; --check exits non-zero on any breaking change for CI gating)
|
|
101
102
|
gs repin <scene-module> --golden <dir> [--name <p>] [--frames a,b,..] [--fps <n>] [--since <ref>] [--write] [--only a,b] [--heatmap <dir>] [--floor <ssim>] [--force] narration-aware golden reviewer: render current vs committed goldens, report perceptual delta + the re-narration cause, re-pin only frames you allow (default dry-run; --floor refuses a bigger-than-expected drop)
|
|
102
|
-
gs parity <scene-module> [--backends skia,lottie] [--frames a,b,..] [--fps <n>] [--width <n>] [--height <n>] [--heatmap <dir>] [--min <ssim>] cross-backend perceptual review: render ONE scene across backends and report per-frame SSIM vs the Skia reference + the worst 8×8 tile (skia = reference, lottie = export↔import round-trip). --heatmap writes a thermal PNG per frame; --min is the SSIM floor (default 0.98) — a below-floor frame exits non-zero. (dom = Phase B, not yet shipped)
|
|
103
|
+
gs parity <scene-module> [--backends skia,lottie] [--frames a,b,..] [--fps <n>] [--width <n>] [--height <n>] [--heatmap <dir>] [--min <ssim>] [--baseline <file>] [--update-baseline] [--tolerance <eps>] cross-backend perceptual review: render ONE scene across backends and report per-frame SSIM vs the Skia reference + the worst 8×8 tile (skia = reference, lottie = export↔import round-trip). --heatmap writes a thermal PNG per frame; --min is the SSIM floor (default 0.98) — a below-floor frame exits non-zero. --baseline turns it into a KNOWN-DROP regression gate: compare each mean vs a committed per-scene baseline of EXPECTED drops and fail ONLY on a deviation (a new/worse drop), so documented scope-outs that legitimately fail the floor PASS while a real regression FAILs; --update-baseline (re)writes that baseline from the live run; --tolerance is the expected-SSIM band (default 1e-4). --baseline takes precedence over --min. (dom = Phase B, not yet shipped)
|
|
103
104
|
gs localize <scene-module> --to <locale> [--from <locale>] [--write] [--strict] [--keep-voice] [--json] fork a narration into a new locale (clone segment/pause structure, PRESERVING beat ids so .start() anchors survive) + stub messages.<locale>.json from the scene's t() ids, running the render path's parity + localize checks BEFORE any TTS. Default dry-run (exits non-zero on drift); --write emits <base>.<locale>.narration.json + messages.<locale>.json (re-localize CARRIES existing translations over — never clobbers); --strict refuses to write on a preflight failure
|
|
104
105
|
gs --version print the engine version
|
|
105
106
|
|
|
@@ -453,6 +454,15 @@ async function main() {
|
|
|
453
454
|
min = Number(minRaw);
|
|
454
455
|
if (!(min >= -1 && min <= 1)) fail(`parity: --min must be an SSIM floor in [-1, 1], got '${minRaw}'`);
|
|
455
456
|
}
|
|
457
|
+
const baselinePath = pf.get("baseline");
|
|
458
|
+
const updateBaseline = pf.has("update-baseline");
|
|
459
|
+
if (updateBaseline && baselinePath === void 0) fail(`parity: --update-baseline needs --baseline <file> (the baseline path to write to)\n${USAGE}`);
|
|
460
|
+
const tolRaw = pf.get("tolerance");
|
|
461
|
+
let tolerance;
|
|
462
|
+
if (tolRaw !== void 0) {
|
|
463
|
+
tolerance = Number(tolRaw);
|
|
464
|
+
if (!(tolerance >= 0) || !Number.isFinite(tolerance)) fail(`parity: --tolerance must be a non-negative number, got '${tolRaw}'`);
|
|
465
|
+
}
|
|
456
466
|
const { parityCommand, parseBackends, ParityBackendError } = await import("./parity.js");
|
|
457
467
|
let backends;
|
|
458
468
|
try {
|
|
@@ -471,10 +481,15 @@ async function main() {
|
|
|
471
481
|
...width !== void 0 ? { width } : {},
|
|
472
482
|
...height !== void 0 ? { height } : {},
|
|
473
483
|
...pf.get("heatmap") ? { heatmapDir: pf.get("heatmap") } : {},
|
|
474
|
-
...min !== void 0 ? { min } : {}
|
|
484
|
+
...min !== void 0 ? { min } : {},
|
|
485
|
+
...baselinePath !== void 0 ? { baselinePath } : {},
|
|
486
|
+
...updateBaseline ? { updateBaseline: true } : {},
|
|
487
|
+
...tolerance !== void 0 ? { tolerance } : {}
|
|
475
488
|
});
|
|
476
489
|
process.stdout.write(`${result.report}\n`);
|
|
477
|
-
if (
|
|
490
|
+
if (updateBaseline) {} else if (baselinePath !== void 0) {
|
|
491
|
+
if (result.gateOk !== true) process.exit(1);
|
|
492
|
+
} else if (!result.ok) process.exit(1);
|
|
478
493
|
} catch (err) {
|
|
479
494
|
fail(err instanceof ParityBackendError ? err.message : err instanceof Error ? err.message : String(err));
|
|
480
495
|
}
|
package/dist/parity.js
CHANGED
|
@@ -1,9 +1,124 @@
|
|
|
1
1
|
import { d as prepareSkiaRenderEnv, o as loadSceneModule } from "./render.js";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { evaluate, withDeterminismGuards } from "@glissade/scene";
|
|
5
5
|
import { SkiaBackend, heatmapRgba, ssimMap } from "@glissade/backend-skia";
|
|
6
6
|
import { exportLottie, importLottie } from "@glissade/lottie";
|
|
7
|
+
//#region src/parityBaseline.ts
|
|
8
|
+
/**
|
|
9
|
+
* `gs parity` known-drop regression-gate — the committed per-scene baseline.
|
|
10
|
+
*
|
|
11
|
+
* `gs parity` (Phase A) is RED-BY-DESIGN: documented scope-outs (non-center-anchor
|
|
12
|
+
* transforms, gradient/mesh fills, variable-font axes, text-wrap, media) legitimately
|
|
13
|
+
* fall below the 0.98 SSIM floor and fail the strict `--min` path. That makes the
|
|
14
|
+
* command a fidelity READOUT, not a regression gate you can wire into CI.
|
|
15
|
+
*
|
|
16
|
+
* This module turns it into a real gate: a committed baseline pins each scope-out's
|
|
17
|
+
* EXPECTED SSIM drop per (frame, backend); the run compares actual vs expected and
|
|
18
|
+
* alerts only on DEVIATION — a new/worse drop is a REGRESSION (fail), while a
|
|
19
|
+
* documented drop that matches its pin PASSES even though it's below the floor.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors `gs repin`'s golden/baseline model (write vs compare split): a default
|
|
22
|
+
* conventional path (`parityBaselinePathFor`, like `goldenPathFor`), a loud header
|
|
23
|
+
* validation (the baseline is pinned at a specific w×h/fps/reference), and a
|
|
24
|
+
* four-way classify (`compareToBaseline`, like repin's `RepinStatus`).
|
|
25
|
+
*
|
|
26
|
+
* Pure of the render pipeline — plain JSON I/O over `node:fs`. `parity.ts` owns the
|
|
27
|
+
* render; this owns the baseline. (Dependency runs one way: parity.ts → here.)
|
|
28
|
+
*/
|
|
29
|
+
/** Raised for a malformed / mismatched baseline — the CLI turns it into a loud fail(). */
|
|
30
|
+
var ParityBaselineError = class extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ParityBaselineError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function isRecord(v) {
|
|
37
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
38
|
+
}
|
|
39
|
+
function readExpectation(where, raw) {
|
|
40
|
+
if (!isRecord(raw)) throw new ParityBaselineError(`parity baseline: ${where} is not an object`);
|
|
41
|
+
if (typeof raw.mean !== "number" || !Number.isFinite(raw.mean)) throw new ParityBaselineError(`parity baseline: ${where} is missing a numeric 'mean'`);
|
|
42
|
+
const exp = { mean: raw.mean };
|
|
43
|
+
if (raw.min !== void 0) {
|
|
44
|
+
if (typeof raw.min !== "number" || !Number.isFinite(raw.min)) throw new ParityBaselineError(`parity baseline: ${where}.min is not a number`);
|
|
45
|
+
exp.min = raw.min;
|
|
46
|
+
}
|
|
47
|
+
if (raw.minTile !== void 0) {
|
|
48
|
+
const mt = raw.minTile;
|
|
49
|
+
if (!isRecord(mt) || typeof mt.tx !== "number" || typeof mt.ty !== "number") throw new ParityBaselineError(`parity baseline: ${where}.minTile is not {tx,ty}`);
|
|
50
|
+
exp.minTile = {
|
|
51
|
+
tx: mt.tx,
|
|
52
|
+
ty: mt.ty
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return exp;
|
|
56
|
+
}
|
|
57
|
+
/** Read + structurally validate a baseline file. Any malformation fails LOUD. */
|
|
58
|
+
function loadParityBaseline(path) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new ParityBaselineError(`could not read parity baseline '${path}': ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(parsed)) throw new ParityBaselineError(`parity baseline '${path}' is not a JSON object`);
|
|
66
|
+
const { name, width, height, fps, reference, frames } = parsed;
|
|
67
|
+
if (typeof name !== "string") throw new ParityBaselineError(`parity baseline '${path}': missing string 'name'`);
|
|
68
|
+
if (typeof width !== "number" || width <= 0) throw new ParityBaselineError(`parity baseline '${path}': missing positive 'width'`);
|
|
69
|
+
if (typeof height !== "number" || height <= 0) throw new ParityBaselineError(`parity baseline '${path}': missing positive 'height'`);
|
|
70
|
+
if (typeof fps !== "number" || fps <= 0) throw new ParityBaselineError(`parity baseline '${path}': missing positive 'fps'`);
|
|
71
|
+
if (typeof reference !== "string") throw new ParityBaselineError(`parity baseline '${path}': missing string 'reference'`);
|
|
72
|
+
if (!isRecord(frames)) throw new ParityBaselineError(`parity baseline '${path}': missing 'frames' object`);
|
|
73
|
+
const outFrames = {};
|
|
74
|
+
for (const [frame, backends] of Object.entries(frames)) {
|
|
75
|
+
if (!isRecord(backends)) throw new ParityBaselineError(`parity baseline '${path}': frame '${frame}' is not an object`);
|
|
76
|
+
const perBackend = {};
|
|
77
|
+
for (const [backend, exp] of Object.entries(backends)) perBackend[backend] = readExpectation(`frame '${frame}' backend '${backend}'`, exp);
|
|
78
|
+
outFrames[frame] = perBackend;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
width,
|
|
83
|
+
height,
|
|
84
|
+
fps,
|
|
85
|
+
reference,
|
|
86
|
+
frames: outFrames
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Write a baseline (pretty JSON), creating the parent dir — mirrors repin's --write emit. */
|
|
90
|
+
function saveParityBaseline(path, baseline) {
|
|
91
|
+
const dir = dirname(path);
|
|
92
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
93
|
+
writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Assert the baseline was pinned at the SAME config as the live run. A mismatch
|
|
97
|
+
* means the numbers aren't comparable (a 480×480 pin vs a 240×240 run) → LOUD fail
|
|
98
|
+
* rather than a meaningless gate verdict.
|
|
99
|
+
*/
|
|
100
|
+
function assertBaselineHeader(b, live) {
|
|
101
|
+
const mm = [];
|
|
102
|
+
if (b.width !== live.width) mm.push(`width ${b.width} ≠ ${live.width}`);
|
|
103
|
+
if (b.height !== live.height) mm.push(`height ${b.height} ≠ ${live.height}`);
|
|
104
|
+
if (b.fps !== live.fps) mm.push(`fps ${b.fps} ≠ ${live.fps}`);
|
|
105
|
+
if (b.reference !== live.reference) mm.push(`reference '${b.reference}' ≠ '${live.reference}'`);
|
|
106
|
+
if (mm.length > 0) throw new ParityBaselineError(`parity baseline pinned at a different config: ${mm.join(", ")} — re-pin with --update-baseline`);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Classify one live comparison against its pin.
|
|
110
|
+
* • no pin (unpinned frame/backend) → 'new' → FAIL (must be accepted via --update-baseline)
|
|
111
|
+
* • mean ≥ expected − tol → 'ok' → PASS (even below the 0.98 floor — the point)
|
|
112
|
+
* • mean < expected − tol → 'regressed'→ FAIL (a new/worse drop)
|
|
113
|
+
* • mean > expected + tol → 'improved' → PASS but FLAG (re-pin tighter)
|
|
114
|
+
*/
|
|
115
|
+
function compareToBaseline(actualMean, expected, tolerance) {
|
|
116
|
+
if (expected === void 0) return "new";
|
|
117
|
+
if (actualMean < expected.mean - tolerance) return "regressed";
|
|
118
|
+
if (actualMean > expected.mean + tolerance) return "improved";
|
|
119
|
+
return "ok";
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
7
122
|
//#region src/parity.ts
|
|
8
123
|
/**
|
|
9
124
|
* `gs parity` (Phase A) — the cross-backend perceptual reviewer.
|
|
@@ -116,6 +231,18 @@ async function parityCommand(opts) {
|
|
|
116
231
|
const frames = opts.frames ?? DEFAULT_PARITY_FRAMES;
|
|
117
232
|
const floor = opts.min ?? .98;
|
|
118
233
|
const name = opts.name ?? basename(opts.modulePath).replace(/\.[jt]sx?$/, "");
|
|
234
|
+
const tolerance = opts.tolerance ?? 1e-4;
|
|
235
|
+
const gating = opts.baselinePath !== void 0 && opts.updateBaseline !== true;
|
|
236
|
+
let baseline;
|
|
237
|
+
if (gating) {
|
|
238
|
+
baseline = loadParityBaseline(opts.baselinePath);
|
|
239
|
+
assertBaselineHeader(baseline, {
|
|
240
|
+
width: w,
|
|
241
|
+
height: h,
|
|
242
|
+
fps,
|
|
243
|
+
reference: REFERENCE_BACKEND
|
|
244
|
+
});
|
|
245
|
+
}
|
|
119
246
|
const reference = await skiaSource(mod, w, h, opts.modulePath);
|
|
120
247
|
const sources = /* @__PURE__ */ new Map();
|
|
121
248
|
for (const b of compared) sources.set(b, await makeSource(b, mod, w, h, fps, opts.modulePath));
|
|
@@ -124,6 +251,9 @@ async function parityCommand(opts) {
|
|
|
124
251
|
let belowFloor = 0;
|
|
125
252
|
let worstMean = Infinity;
|
|
126
253
|
let worstAt = null;
|
|
254
|
+
let regressed = 0;
|
|
255
|
+
let newComparisons = 0;
|
|
256
|
+
let improved = 0;
|
|
127
257
|
for (const frame of frames) {
|
|
128
258
|
const t = frame / fps;
|
|
129
259
|
const refRgba = await reference(t);
|
|
@@ -153,6 +283,18 @@ async function parityCommand(opts) {
|
|
|
153
283
|
writeFileSync(hp, hb.encodePng());
|
|
154
284
|
pair.heatmap = hp;
|
|
155
285
|
}
|
|
286
|
+
if (baseline) {
|
|
287
|
+
const expected = baseline.frames[String(frame)]?.[backend];
|
|
288
|
+
const status = compareToBaseline(map.mean, expected, tolerance);
|
|
289
|
+
pair.status = status;
|
|
290
|
+
if (expected !== void 0) {
|
|
291
|
+
pair.expected = expected.mean;
|
|
292
|
+
pair.delta = map.mean - expected.mean;
|
|
293
|
+
}
|
|
294
|
+
if (status === "regressed") regressed++;
|
|
295
|
+
else if (status === "new") newComparisons++;
|
|
296
|
+
else if (status === "improved") improved++;
|
|
297
|
+
}
|
|
156
298
|
pairs.push(pair);
|
|
157
299
|
}
|
|
158
300
|
results.push({
|
|
@@ -172,12 +314,85 @@ async function parityCommand(opts) {
|
|
|
172
314
|
belowFloor,
|
|
173
315
|
ok: belowFloor === 0
|
|
174
316
|
};
|
|
317
|
+
if (opts.updateBaseline === true) {
|
|
318
|
+
if (opts.baselinePath === void 0) throw new ParityBaselineError("gs parity --update-baseline needs --baseline <file> to write to");
|
|
319
|
+
const emitted = buildBaseline(name, w, h, fps, results);
|
|
320
|
+
let prior;
|
|
321
|
+
if (existsSync(opts.baselinePath)) try {
|
|
322
|
+
prior = loadParityBaseline(opts.baselinePath);
|
|
323
|
+
} catch {}
|
|
324
|
+
let added = 0;
|
|
325
|
+
let repinned = 0;
|
|
326
|
+
for (const [frame, backends] of Object.entries(emitted.frames)) for (const backend of Object.keys(backends)) {
|
|
327
|
+
const before = prior?.frames[frame]?.[backend];
|
|
328
|
+
if (before === void 0) added++;
|
|
329
|
+
else if (Math.abs(before.mean - backends[backend].mean) > tolerance) repinned++;
|
|
330
|
+
}
|
|
331
|
+
saveParityBaseline(opts.baselinePath, emitted);
|
|
332
|
+
const updated = {
|
|
333
|
+
...result,
|
|
334
|
+
improved,
|
|
335
|
+
baselineWritten: opts.baselinePath,
|
|
336
|
+
baselineAdded: added
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
...updated,
|
|
340
|
+
regressed: repinned,
|
|
341
|
+
report: formatReport(name, updated, { repinned })
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (gating) {
|
|
345
|
+
const gated = {
|
|
346
|
+
...result,
|
|
347
|
+
regressed,
|
|
348
|
+
newComparisons,
|
|
349
|
+
improved,
|
|
350
|
+
gateOk: regressed === 0 && newComparisons === 0
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
...gated,
|
|
354
|
+
report: formatReport(name, gated)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
175
357
|
return {
|
|
176
358
|
...result,
|
|
177
359
|
report: formatReport(name, result)
|
|
178
360
|
};
|
|
179
361
|
}
|
|
180
|
-
|
|
362
|
+
/** Build a baseline document from the live frames (used by --update-baseline). */
|
|
363
|
+
function buildBaseline(name, w, h, fps, frames) {
|
|
364
|
+
const out = {};
|
|
365
|
+
for (const f of frames) {
|
|
366
|
+
const perBackend = {};
|
|
367
|
+
for (const p of f.pairs) perBackend[p.backend] = {
|
|
368
|
+
mean: p.mean,
|
|
369
|
+
min: p.min,
|
|
370
|
+
minTile: {
|
|
371
|
+
tx: p.minTile.tx,
|
|
372
|
+
ty: p.minTile.ty
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
out[String(f.frame)] = perBackend;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
name,
|
|
379
|
+
width: w,
|
|
380
|
+
height: h,
|
|
381
|
+
fps,
|
|
382
|
+
reference: REFERENCE_BACKEND,
|
|
383
|
+
frames: out
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/** gate/update status → the report mark appended to a pair line. */
|
|
387
|
+
const GATE_MARK = {
|
|
388
|
+
ok: " ✓ expected-drop",
|
|
389
|
+
regressed: " ⚠ REGRESSION",
|
|
390
|
+
new: " + NEW",
|
|
391
|
+
improved: " ▲ improved"
|
|
392
|
+
};
|
|
393
|
+
function formatReport(name, r, extra) {
|
|
394
|
+
const update = r.baselineWritten !== void 0;
|
|
395
|
+
const gate = !update && r.gateOk !== void 0;
|
|
181
396
|
const lines = [];
|
|
182
397
|
lines.push(`gs parity '${name}' — ${REFERENCE_BACKEND} reference vs ${r.backends.join(", ")} — ${r.frames.length} frame${r.frames.length === 1 ? "" : "s"} @ ${r.width}×${r.height}, floor ${r.floor}`);
|
|
183
398
|
for (const f of r.frames) {
|
|
@@ -186,10 +401,23 @@ function formatReport(name, r) {
|
|
|
186
401
|
const tile = `@ tile ${p.minTile.tx},${p.minTile.ty}`;
|
|
187
402
|
const mark = p.belowFloor ? " ⚠ BELOW FLOOR" : "";
|
|
188
403
|
const heat = p.heatmap ? ` → ${p.heatmap}` : "";
|
|
189
|
-
|
|
404
|
+
let gateSuffix = "";
|
|
405
|
+
if (gate && p.status !== void 0) gateSuffix = `${p.expected !== void 0 ? ` exp ${p.expected.toFixed(4)} Δ${p.delta >= 0 ? "+" : ""}${p.delta.toFixed(4)}` : ""}${GATE_MARK[p.status]}`;
|
|
406
|
+
lines.push(` ${fno} ${p.backend.padEnd(6)} ssim ${p.mean.toFixed(4)} (min ${p.min.toFixed(3)} ${tile})${gateSuffix}${mark}${heat}`);
|
|
190
407
|
}
|
|
191
408
|
}
|
|
192
409
|
if (r.worstAt) lines.push(` worst: f${String(r.worstAt.frame).padStart(4, "0")} ${r.worstAt.backend} ssim ${r.worstMean.toFixed(4)}`);
|
|
410
|
+
if (update) {
|
|
411
|
+
lines.push(` wrote baseline → ${r.baselineWritten}`);
|
|
412
|
+
lines.push(` ${r.baselineAdded ?? 0} added, ${extra?.repinned ?? 0} re-pinned (moved > tol), ${r.improved ?? 0} improved`);
|
|
413
|
+
return lines.join("\n");
|
|
414
|
+
}
|
|
415
|
+
if (gate) {
|
|
416
|
+
const parts = [`${r.regressed ?? 0} regressed`, `${r.newComparisons ?? 0} new`];
|
|
417
|
+
if ((r.improved ?? 0) > 0) parts.push(`${r.improved} improved`);
|
|
418
|
+
lines.push(r.gateOk ? ` PASS — every comparison matched its expected drop (${parts.join(", ")})` : ` FAIL — ${parts.join(", ")} vs the baseline`);
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
}
|
|
193
421
|
lines.push(r.ok ? ` PASS — every frame ≥ floor ${r.floor}` : ` FAIL — ${r.belowFloor} comparison${r.belowFloor === 1 ? "" : "s"} below floor ${r.floor}`);
|
|
194
422
|
return lines.join("\n");
|
|
195
423
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0-pre.0",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"@napi-rs/canvas": "^0.1.65",
|
|
29
29
|
"esbuild": "0.28.0",
|
|
30
30
|
"jiti": "^2.4.2",
|
|
31
|
-
"@glissade/backend-skia": "0.
|
|
32
|
-
"@glissade/core": "0.
|
|
33
|
-
"@glissade/interact": "0.
|
|
34
|
-
"@glissade/lottie": "0.
|
|
35
|
-
"@glissade/narrate": "0.
|
|
36
|
-
"@glissade/player": "0.
|
|
37
|
-
"@glissade/scene": "0.
|
|
38
|
-
"@glissade/sfx": "0.
|
|
39
|
-
"@glissade/svg": "0.
|
|
31
|
+
"@glissade/backend-skia": "0.50.0-pre.0",
|
|
32
|
+
"@glissade/core": "0.50.0-pre.0",
|
|
33
|
+
"@glissade/interact": "0.50.0-pre.0",
|
|
34
|
+
"@glissade/lottie": "0.50.0-pre.0",
|
|
35
|
+
"@glissade/narrate": "0.50.0-pre.0",
|
|
36
|
+
"@glissade/player": "0.50.0-pre.0",
|
|
37
|
+
"@glissade/scene": "0.50.0-pre.0",
|
|
38
|
+
"@glissade/sfx": "0.50.0-pre.0",
|
|
39
|
+
"@glissade/svg": "0.50.0-pre.0"
|
|
40
40
|
},
|
|
41
41
|
"repository": {
|
|
42
42
|
"type": "git",
|