@glissade/cli 0.37.0-pre.0 → 0.37.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.
Files changed (3) hide show
  1. package/README.md +20 -1
  2. package/dist/repin.js +34 -6
  3. package/package.json +10 -10
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `gs` — headless rendering and the dev/capture loop. `gs render` evaluates every frame, rasterizes on Skia (no browser anywhere), writes a PNG sequence or muxes mp4/webm via FFmpeg with sample-accurate audio. v2: `gs dev --record` serves a scene with its state machines mounted and writes input-trace sidecars; `gs render --trace/--state` are the deterministic export routes for interactive scenes.
4
4
 
5
- The full command set: `render`, `dev`, `import` (Lottie `.json` / `.svg`), `build`, `mcp` (an MCP server over the engine), `describe`, `migrate`, `narrate`, `sfx`, `prepare`, `measure-loudness`, `fonts`, `cache`, `diff`, and `verify-determinism`.
5
+ The full command set: `render`, `dev`, `import` (Lottie `.json` / `.svg`), `build`, `mcp` (an MCP server over the engine), `describe`, `migrate`, `repin` (narration-aware golden reviewer), `narrate`, `sfx`, `prepare`, `measure-loudness`, `fonts`, `cache`, `diff`, and `verify-determinism`. Run `gs` with no arguments for the full usage.
6
6
 
7
7
  ```sh
8
8
  npm i -D @glissade/cli
@@ -14,6 +14,25 @@ gs dev scene.ts --record # capture a take
14
14
  gs render scene.ts --trace scene.button.take1.trace.json --out take.mp4
15
15
  ```
16
16
 
17
+ ## Reviewing goldens (`gs repin`)
18
+
19
+ When a re-narration re-flows a project's beats, every downstream golden goes
20
+ stale at once. `gs repin` renders the current scene vs the committed golden PNGs
21
+ and, per changed frame, reports a perceptual SSIM delta (mean + worst tile) plus a
22
+ one-line **cause** — it diffs the `*.narration.timing.json` sibling against a git
23
+ ref to attribute the edit site and trace the pushed beats back to it. Default is a
24
+ dry run; `--write` re-pins, and `--floor <ssim>` refuses a bigger-than-expected
25
+ drop until `--force` — the confidence gate before you bless a batch re-pin.
26
+
27
+ ```sh
28
+ gs repin scene.ts --golden test/golden # dry-run: what changed & why
29
+ gs repin scene.ts --golden test/golden --write --floor 0.98 # re-pin, guarded
30
+ ```
31
+
32
+ The SSIM machinery is also exported from `@glissade/backend-skia`
33
+ (`ssim` / `ssimMap` / `heatmapRgba`) for your own golden tooling. Full guide:
34
+ [Reviewing goldens](https://github.com/tyevco/glissade/blob/main/docs/golden-review.md).
35
+
17
36
  ## Part of glissade
18
37
 
19
38
  *(glide & slide)* — programmatic motion graphics for TypeScript: realtime-first in any web page, deterministic headless video export from the same code, a visual studio over the same document. No generator functions.
package/dist/repin.js CHANGED
@@ -80,6 +80,7 @@ function diffTiming(a, b) {
80
80
  id: s.id,
81
81
  start: s.start,
82
82
  deltaStart: prev ? s.start - prev.start : 0,
83
+ deltaDuration: prev ? s.duration - prev.duration : 0,
83
84
  added: !prev,
84
85
  removed: false
85
86
  });
@@ -89,6 +90,7 @@ function diffTiming(a, b) {
89
90
  id,
90
91
  start: s.start,
91
92
  deltaStart: 0,
93
+ deltaDuration: 0,
92
94
  added: false,
93
95
  removed: true
94
96
  });
@@ -96,14 +98,32 @@ function diffTiming(a, b) {
96
98
  }
97
99
  const EPS = 1e-4;
98
100
  const sign = (n) => `${n >= 0 ? "+" : ""}${n.toFixed(2)}s`;
99
- /** One-line cause for a frame at time `t`, from the timing shifts. */
101
+ /** Is this shift a re-narration ROOT a segment whose own content changed
102
+ * (duration moved) or that was newly added, i.e. an EDIT SITE that pushes
103
+ * everything downstream of it? */
104
+ function isRoot(s) {
105
+ return s.added || Math.abs(s.deltaDuration) > EPS;
106
+ }
107
+ /**
108
+ * One-line cause for a frame at time `t`. A re-narration that re-records one line
109
+ * changes that segment's DURATION (not its start) and pushes every later segment's
110
+ * start by the same amount — so attribution has two jobs:
111
+ * • name the EDIT SITE by its duration change (its start doesn't move), and
112
+ * • trace a purely-shifted downstream beat back to the ROOT that pushed it,
113
+ * rather than letting it claim its own (derived) shift.
114
+ */
100
115
  function causeFor(t, shifts) {
101
116
  const before = shifts.filter((s) => s.start <= t + EPS && !s.removed);
102
117
  const active = before[before.length - 1];
103
- if (active && active.added) return `${active.id}: new segment`;
104
- if (active && Math.abs(active.deltaStart) > EPS) return `${active.id} moved ${sign(active.deltaStart)}: re-narration`;
105
- const upstream = before.filter((s) => Math.abs(s.deltaStart) > EPS).pop();
106
- if (upstream) return `downstream of ${upstream.id} (${sign(upstream.deltaStart)}): re-narration`;
118
+ if (!active) return void 0;
119
+ if (active.added) return `${active.id}: new segment`;
120
+ if (Math.abs(active.deltaDuration) > EPS) return `${active.id} re-narrated (${sign(active.deltaDuration)} duration): re-narration`;
121
+ const root = before.slice(0, -1).reverse().find(isRoot);
122
+ if (Math.abs(active.deltaStart) > EPS) {
123
+ if (root) return `downstream of ${root.id} (${sign(active.deltaStart)}): re-narration`;
124
+ return `${active.id} moved ${sign(active.deltaStart)}: re-narration`;
125
+ }
126
+ if (root) return `downstream of ${root.id} (${sign(root.deltaDuration)}): re-narration`;
107
127
  }
108
128
  async function decodePng(buf) {
109
129
  const img = await loadImage(buf);
@@ -226,6 +246,13 @@ function formatReport(name, opts, r) {
226
246
  lines.push(`gs repin '${name}' — ${r.frames.length} frames, ${r.changed} changed [${mode}]`);
227
247
  if (r.causeSource) lines.push(` cause ← ${r.causeSource} vs ${opts.since ?? "HEAD"}`);
228
248
  else lines.push(` (no narration timing sibling diffed — perceptual delta only)`);
249
+ let worstFrame = -1;
250
+ let worstSsim = Infinity;
251
+ for (const f of r.frames) if (f.status === "changed" && f.ssim !== void 0 && f.ssim < worstSsim) {
252
+ worstSsim = f.ssim;
253
+ worstFrame = f.frame;
254
+ }
255
+ const marked = r.changed > 1 && worstFrame >= 0;
229
256
  for (const f of r.frames) {
230
257
  if (f.status === "identical") continue;
231
258
  const fno = `f${String(f.frame).padStart(4, "0")}`;
@@ -235,7 +262,8 @@ function formatReport(name, opts, r) {
235
262
  }
236
263
  const perc = f.ssim !== void 0 ? `ssim ${f.ssim.toFixed(4)} (min ${f.minSsim.toFixed(3)})` : `dimensions changed`;
237
264
  const tail = f.blocked ? "⚠ BELOW FLOOR — refused (use --force)" : f.wrote ? "→ re-pinned" : "(dry-run)";
238
- lines.push(` ${fno} ${perc} ${f.cause ? `— ${f.cause} ` : ""}${tail}`);
265
+ const editMark = marked && f.frame === worstFrame ? "◀ likely edit-site (lowest SSIM) " : "";
266
+ lines.push(` ${fno} ${perc} ${f.cause ? `— ${f.cause} ` : ""}${editMark}${tail}`);
239
267
  }
240
268
  if (!opts.write && r.changed > 0) lines.push(` ${r.changed} stale — re-run with --write to re-pin${opts.floor !== void 0 ? ` (floor ${opts.floor})` : ""}`);
241
269
  if (r.blocked > 0) lines.push(` ${r.blocked} refused below floor — inspect the heat-map, then --force if intended`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.37.0-pre.0",
3
+ "version": "0.37.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.37.0-pre.0",
32
- "@glissade/core": "0.37.0-pre.0",
33
- "@glissade/interact": "0.37.0-pre.0",
34
- "@glissade/lottie": "0.37.0-pre.0",
35
- "@glissade/narrate": "0.37.0-pre.0",
36
- "@glissade/player": "0.37.0-pre.0",
37
- "@glissade/scene": "0.37.0-pre.0",
38
- "@glissade/sfx": "0.37.0-pre.0",
39
- "@glissade/svg": "0.37.0-pre.0"
31
+ "@glissade/backend-skia": "0.37.0",
32
+ "@glissade/core": "0.37.0",
33
+ "@glissade/interact": "0.37.0",
34
+ "@glissade/lottie": "0.37.0",
35
+ "@glissade/narrate": "0.37.0",
36
+ "@glissade/player": "0.37.0",
37
+ "@glissade/scene": "0.37.0",
38
+ "@glissade/sfx": "0.37.0",
39
+ "@glissade/svg": "0.37.0"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",