@jokerized/decksmith 0.8.0 → 0.9.1

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 (64) hide show
  1. package/README.md +333 -4
  2. package/dist/cli.js +14057 -4749
  3. package/dist/deck-player-element.js +1 -1
  4. package/dist/deck-player.js +1 -1
  5. package/dist/deck-runtime.js +70 -2
  6. package/dist/index.js +13309 -4178
  7. package/dist/mcp.js +5131 -765
  8. package/dist/types/bespoke/art.d.ts +173 -0
  9. package/dist/types/bespoke/cache.d.ts +69 -0
  10. package/dist/types/bespoke/callouts.d.ts +76 -0
  11. package/dist/types/bespoke/contract.d.ts +57 -0
  12. package/dist/types/bespoke/inspect.d.ts +69 -0
  13. package/dist/types/bespoke/pipeline.d.ts +207 -0
  14. package/dist/types/bespoke/probe.d.ts +34 -0
  15. package/dist/types/bespoke/prompt.d.ts +182 -0
  16. package/dist/types/bespoke/references.d.ts +73 -0
  17. package/dist/types/bespoke/repair.d.ts +121 -0
  18. package/dist/types/bespoke/scene.d.ts +97 -0
  19. package/dist/types/bespoke/select.d.ts +52 -0
  20. package/dist/types/bespoke/sheet.d.ts +26 -0
  21. package/dist/types/bespoke/shots.d.ts +128 -0
  22. package/dist/types/bespoke/untangle.d.ts +5 -0
  23. package/dist/types/build/files.d.ts +13 -1
  24. package/dist/types/deck/motion.d.ts +64 -0
  25. package/dist/types/deck/playback.d.ts +206 -0
  26. package/dist/types/deck/repack.d.ts +10 -0
  27. package/dist/types/deck/runtime.d.ts +96 -1
  28. package/dist/types/deck/subtitles.d.ts +29 -1
  29. package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
  30. package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
  31. package/dist/types/emit/archetypes/callout.d.ts +0 -5
  32. package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
  33. package/dist/types/emit/archetypes/grid.d.ts +0 -14
  34. package/dist/types/emit/archetypes/index.d.ts +0 -7
  35. package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
  36. package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
  37. package/dist/types/emit/archetypes/stack.d.ts +1 -1
  38. package/dist/types/emit/composition.d.ts +83 -3
  39. package/dist/types/emit/faces.d.ts +22 -0
  40. package/dist/types/emit/fit.d.ts +127 -0
  41. package/dist/types/emit/kit.d.ts +121 -2
  42. package/dist/types/emit/look.d.ts +138 -0
  43. package/dist/types/emit/motion.d.ts +244 -0
  44. package/dist/types/emit/svg.d.ts +54 -16
  45. package/dist/types/emit/theme.d.ts +1 -1
  46. package/dist/types/emit/themes/index.d.ts +19 -1
  47. package/dist/types/emit/themes/packs.d.ts +15 -0
  48. package/dist/types/emit/themes/pick.d.ts +107 -0
  49. package/dist/types/emit/type.d.ts +111 -0
  50. package/dist/types/index.d.ts +52 -1
  51. package/dist/types/plan/codex.d.ts +36 -0
  52. package/dist/types/plan/direct.d.ts +66 -0
  53. package/dist/types/prefs.d.ts +25 -2
  54. package/dist/types/render/capture.d.ts +6 -0
  55. package/dist/types/render/timing.d.ts +3 -0
  56. package/dist/types/source/fonts.d.ts +11 -1
  57. package/dist/types/types.d.ts +109 -0
  58. package/dist/types/verify/check.d.ts +1 -1
  59. package/dist/types/verify/fidelity.d.ts +34 -0
  60. package/dist/types/verify/fill.d.ts +205 -0
  61. package/dist/types/verify/index.d.ts +14 -0
  62. package/dist/types/verify/motion.d.ts +33 -0
  63. package/dist/types/verify/scenes.d.ts +325 -0
  64. package/package.json +5 -1
@@ -0,0 +1,205 @@
1
+ /**
2
+ * THE FILL GATE — at each scene's last hold, how much of its region did the
3
+ * body actually paint, and did the build predict that?
4
+ *
5
+ * WHY THIS EXISTS. The founder's complaint was "the content doesn't fill the
6
+ * screen", and v0.8.0 passes every gate while doing exactly that: the solvers
7
+ * could only shrink, so a two-bar comparison drew two 96px bars in a ~700px
8
+ * region and `.scene` centred the rest into air. The v2 archetypes grow into
9
+ * their region and PREDICT the result (`Scene.fit`, `src/emit/fit.ts`); a
10
+ * prediction nobody checks is the green-gate-over-wrong-output shape this
11
+ * project keeps producing, so the browser measures the same ratio here, on the
12
+ * frame `fidelity` has already seeked and captured.
13
+ *
14
+ * WHAT IS MEASURED: MAIN-AXIS FILL, IN PIXELS. The region is the room the body
15
+ * was given: `.scene`'s content box less the height of its `.headline` and
16
+ * `.eyebrow` (see `FillRegion` for why that is not "everything under the
17
+ * headline"). The body's extent is the first to the last pixel ROW, from the
18
+ * chrome's bottom edge down to the frame's bottom edge, that holds at least
19
+ * `MIN_ROW_INK` pixels off the frame's modal background (the same
20
+ * background-relative test `fidelity` uses, so it is theme-independent). Fill is
21
+ * that extent over the region's height. Scanning past the content box is
22
+ * deliberate: a body that spills into the padding reads above 1, which is the
23
+ * SPILLAGE band rather than a silently clipped 1.0.
24
+ *
25
+ * PIXELS, NOT DOM BOXES, for the reason `fidelity.ts` gives at length: a
26
+ * wrapper's box is the size of its slot, not of what it shows — `.titleslide` is
27
+ * `height:100%`, `.panels` is `flex:1` — and visibility read off the DOM has been
28
+ * wrong in this project before. A panel wash too close to the background to
29
+ * clear `INK_DELTA` does not count; its border and its text do.
30
+ *
31
+ * ALSO REPORTED, for the eval and never graded: `cross`, the same ratio across
32
+ * the region's width, and `canvas`, the union box of ALL ink on the frame
33
+ * (chrome included) over the frame's area — the "bbox" share the 2026-10-07
34
+ * density audit quoted for bar-compare and pipeline (51%).
35
+ *
36
+ * AND TWO AREA MEASURES, because `fill` is an EXTENT and cannot see area: two
37
+ * thin bars spanning a 1700px axis, a white figure plate, or cards with empty
38
+ * bottom halves all read full (review, 2026-10-08). `occupancy` cuts the whole
39
+ * frame (less a 3% border) into 40px cells, at 1080p scale, and reports the
40
+ * share holding ink (`cells`) and the share whose pixels VARY (`detail`): a
41
+ * flat run of colour — a plate's empty white, a panel's wash, a bar's inside —
42
+ * is ink but not detail. Both are placement-independent: they do not move when
43
+ * a look redefines where its body region is.
44
+ *
45
+ * GRADED ONLY ON A V2 DECK, i.e. one whose build wrote `fit.json`. A classic deck
46
+ * is measured (so the eval can compare the two designs on the same storyboard)
47
+ * but never gets a finding: v0.8.0's verdicts do not move. Both findings are
48
+ * WARNINGS, as the plan's M1 sets them — a new gate starts by reporting:
49
+ *
50
+ * hollow_at_hold measured fill under 0.70, the EMPTY band
51
+ * fill_model_disagrees |predicted − measured| over 0.20 — the build-time
52
+ * arithmetic and the browser disagree, which is a bug
53
+ * in the model, not a matter of taste
54
+ */
55
+ import { type FitManifest } from "../emit/fit.js";
56
+ import type { Finding } from "../types.js";
57
+ /** A row counts as painted from this many off-background pixels: one is a stray fringe. */
58
+ export declare const MIN_ROW_INK = 2;
59
+ /** Predicted and measured fill may differ by this much before the model is called wrong. */
60
+ export declare const FILL_TOLERANCE = 0.2;
61
+ /** Device px, in the frame's own coordinates. */
62
+ export interface Box {
63
+ left: number;
64
+ right: number;
65
+ top: number;
66
+ bottom: number;
67
+ }
68
+ /** The pixel test `fidelity` uses: what counts as ink, and against what. */
69
+ export interface InkTest {
70
+ bg: readonly [number, number, number];
71
+ delta: number;
72
+ /**
73
+ * The page's background as a FRAME — every scene hidden, the body's own paint
74
+ * left — compared pixel for pixel instead of against `bg`. A v2 style pack
75
+ * paints its ground with gradients, a grid or dots (src/emit/themes/packs.ts),
76
+ * so no single colour is the background: a 7% cyan grid line on blueprint is
77
+ * 12 levels off its navy, and atlas's vignette darkens the corners by far more.
78
+ * Against one colour those read as ink and a sparse slide measured full. When
79
+ * its size differs from the frame's it is ignored and `bg` is used.
80
+ */
81
+ plate?: FrameLike;
82
+ }
83
+ interface FrameLike {
84
+ width: number;
85
+ height: number;
86
+ channels: number;
87
+ pixels: Uint8Array;
88
+ }
89
+ /**
90
+ * The extent of ink inside `box`, or null when there is none. Pure, so what the
91
+ * gate counts can be tested without a browser.
92
+ *
93
+ * A row is painted when it holds `MIN_ROW_INK` ink pixels between `box.left` and
94
+ * `box.right`; the vertical extent is the first and last such row. The
95
+ * horizontal extent is then read over those rows only, by the same rule turned
96
+ * ninety degrees.
97
+ */
98
+ export declare function inkExtent(frame: FrameLike, test: InkTest, box: Box): Box | null;
99
+ /**
100
+ * A scene's body region, as the page reports it: where to LOOK for the body's
101
+ * ink (`top` is the chrome's bottom edge, `bottom` the content box's), and how
102
+ * tall the region IS (`height`).
103
+ *
104
+ * THE TWO DIFFER ON PURPOSE, and the difference is the defect being measured.
105
+ * `.scene` centres its column, so a short body pushes the headline DOWN by half
106
+ * the slack — and a region drawn from the headline's bottom edge to the content
107
+ * bottom silently drops the other half, the band of nothing ABOVE the headline.
108
+ * The first version did exactly that and called a two-bar chart with 250px of
109
+ * empty canvas over its eyebrow 70% full. So the height is the content box less
110
+ * the chrome's own height, wherever the centring put it: the room the body was
111
+ * given, which is the same number `bodyBudget` hands the emitter.
112
+ */
113
+ export interface FillRegion extends Box {
114
+ height: number;
115
+ /**
116
+ * How far down body ink is looked for. Absent: the frame's bottom edge, so a
117
+ * body that spills past its region is measured, not clipped. A `foot` slide
118
+ * sets it to the region's bottom, because what lies under that region is the
119
+ * slide's own headline, not spill.
120
+ */
121
+ scanBottom?: number;
122
+ /**
123
+ * The page's own background, `[r, g, b]`, or null when it is not an opaque
124
+ * colour. What "ink" is measured against — see `fillInk`.
125
+ */
126
+ bg: [number, number, number] | null;
127
+ }
128
+ /**
129
+ * Run IN THE PAGE: the scene's body region, in device px. Null when the scene
130
+ * is not in the document.
131
+ *
132
+ * The content box is `.scene`'s padding box, read off computed style rather
133
+ * than restated from `kit.ts`, so a deck built with a caption reserve (bottom
134
+ * padding grows) is measured against the box it actually laid out in.
135
+ */
136
+ export declare function collectFillRegion(sid: string): FillRegion | null;
137
+ /**
138
+ * The ink test for a fill measurement: the page's declared background when the
139
+ * region reported one, else the frame's modal colour, at `delta`.
140
+ *
141
+ * NOT the modal colour by default, and the reason is this gate's own success.
142
+ * `fidelity` takes the background to be the frame's most common colour, which
143
+ * holds while a slide is mostly air — "80–98% of every frame measured". A v2
144
+ * callout whose panels grew into their region covers MORE of the frame with
145
+ * panel wash than with background; the modal colour is then the panel's, the
146
+ * real background reads as ink, and a 0.95 hold measured 1.107 with a canvas
147
+ * share of 1.0 (HypePaper ko deck 894a874a, s18, 2026-10-07). The body
148
+ * stylesheet says what the background is, so that is what is asked.
149
+ */
150
+ export declare function fillInk(region: FillRegion | null, modal: readonly [number, number, number], delta: number, plate?: FrameLike | null): InkTest;
151
+ /**
152
+ * Run IN THE PAGE: hide (`true`) or restore (`false`) every scene, so one
153
+ * screenshot between the two is the deck's bare background — `InkTest.plate`.
154
+ * Opacity on the scene roots rather than visibility, because GSAP's `autoAlpha`
155
+ * writes `visibility:visible` on descendants and would show through.
156
+ */
157
+ export declare function hideScenes(on: boolean): void;
158
+ /** One scene's last hold, measured. */
159
+ export interface FillRow {
160
+ sid: string;
161
+ t: number;
162
+ /** Main-axis (vertical) fill: body ink extent over region height. Above 1 spills. */
163
+ fill: number;
164
+ /** The same across the region's width. Reported, not graded. */
165
+ cross: number;
166
+ /** Union box of every ink pixel on the frame, chrome included, over the frame's area. */
167
+ canvas: number;
168
+ /** Region height in device px — 0 when the scene had no region to measure. */
169
+ region: number;
170
+ /** Share of the frame's cells holding ink, and of cells whose pixels vary. See `occupancy`. */
171
+ cells: number;
172
+ detail: number;
173
+ }
174
+ /**
175
+ * How much of the frame carries content, by area. Pure.
176
+ *
177
+ * `cells`: share of cells with at least `CELL_MIN_INK` of their pixels off the
178
+ * ground (the ink test `fidelity` uses — the bare-background plate when there is
179
+ * one). `detail`: share of cells that hold ink AND whose pixels span more than
180
+ * the ink threshold in luma — an edge, a glyph, a picture — so a flat slab of
181
+ * one colour counts as ink and not as detail.
182
+ */
183
+ export declare function occupancy(frame: FrameLike, test: InkTest): {
184
+ cells: number;
185
+ detail: number;
186
+ };
187
+ /**
188
+ * Fill at one stop, from a decoded frame and the region the page reported.
189
+ * Pure. A scene with no region (not found) or no ink reads 0, which grades as
190
+ * hollow — "nothing painted below the headline" is exactly what that is.
191
+ */
192
+ export declare function measureFill(frame: FrameLike, test: InkTest, region: FillRegion | null, stop: {
193
+ sid: string;
194
+ t: number;
195
+ }): FillRow;
196
+ /** The last declared stop of each scene: the final hold the fill is judged at. */
197
+ export declare function finalStops<S extends {
198
+ sid: string;
199
+ t: number;
200
+ }>(stops: readonly S[]): S[];
201
+ /** Parse `fit.json`. Null when the deck has none — a classic build — or it does not parse. */
202
+ export declare function readFitManifest(text: string | null): FitManifest | null;
203
+ /** Findings for a v2 deck's measured fills. Nothing at all for a classic deck. */
204
+ export declare function gradeFill(rows: readonly FillRow[], manifest: FitManifest | null): Finding[];
205
+ export {};
@@ -44,6 +44,20 @@ export { scanTypeFloor, TYPE_FLOOR_PX } from "./typefloor.js";
44
44
  export interface VerifyOptions extends CheckOptions {
45
45
  /** Default true. `false` skips the frame measurement entirely. */
46
46
  fidelity?: boolean;
47
+ /**
48
+ * Default true. The motion gates in ./scenes.ts: all three, as errors, on the
49
+ * scenes `bespoke.json` says were generated; `seek_order` alone, as a
50
+ * warning, on every other scene of a v2 deck. A classic deck runs none.
51
+ * The bespoke pass turns this off on its probe decks, which it gates itself.
52
+ */
53
+ scenes?: boolean;
54
+ /**
55
+ * Generated scenes whose motion gates the bespoke pass already ran on these
56
+ * exact bytes at this same scene id (`bespoke.json`'s `gatedAt`): `build`
57
+ * seeks them like any other scene instead of repeating the full probe (~16s
58
+ * a scene). `decksmith verify <dir>` never passes it.
59
+ */
60
+ probed?: ReadonlySet<string>;
47
61
  }
48
62
  /**
49
63
  * Run every gate DeckSmith owns over a built project directory.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Motion variety, measured off a built composition — the M4 exit metrics.
3
+ *
4
+ * Read from the emitted `index.html` rather than from the in-memory plan, so the
5
+ * number is about what ships: a plan that said "push" and an emitter that wrote
6
+ * a dissolve would read as a dissolve here. The three numbers are the ones the
7
+ * redesign plan set against v0.8.0's audit of 124 HypePaper scenes:
8
+ *
9
+ * | metric | v0.8.0 | v2 target |
10
+ * |--------------------------------|--------|-----------|
11
+ * | modal entrance (share) | 92% | ≤ 40% |
12
+ * | seam kinds (deck of 10+ beats) | 1 | ≥ 3 |
13
+ * | top-2 ease share of tweens | 85% | ≤ 60% |
14
+ *
15
+ * MODAL is deliberately generous to the old look: a scene "opens modal" when its
16
+ * headline enters as a bare opacity + y fade-up, whatever the ease. v2's `rise`
17
+ * verb therefore counts as modal, which is why `planMotion` caps it.
18
+ */
19
+ export interface MotionStats {
20
+ scenes: number;
21
+ /** Scenes whose headline (`#sN-h`, or the title's `#sN-t`) enters as opacity + y only. */
22
+ modalScenes: number;
23
+ /** Scenes with at least one emphasis (glow, underline, or a sine-eased pulse) after the build. */
24
+ emphasisScenes: number;
25
+ /** Seam kind per handoff, in scene order. */
26
+ seams: string[];
27
+ /** Tween count per ease name; a tween naming none is `"default"`. */
28
+ eases: Record<string, number>;
29
+ tweens: number;
30
+ }
31
+ export declare function motionStats(composition: string): MotionStats;
32
+ /** The share of tweens carried by the two most common eases. */
33
+ export declare function topTwoEaseShare(stats: MotionStats): number;
@@ -0,0 +1,325 @@
1
+ import type { DeckPage } from "../render/capture.js";
2
+ import type { Timing } from "../render/timing.js";
3
+ import type { Finding } from "../types.js";
4
+ import { type Frame } from "./fidelity.js";
5
+ /** A pixel counts as changed when its RGB moved by more than this, summed. Same as the spike's. */
6
+ export declare const PIXEL_DELTA = 24;
7
+ /**
8
+ * Fewer changed pixels than this between two seeks of one instant is rasterising,
9
+ * not state. MEASURED: a never-seeked page against a warm one differs by up to
10
+ * 644px on a scene that looks identical — antialiasing along the headline's
11
+ * glyphs, which the shell's own entrance had moved — and a KaTeX glyph by 529px.
12
+ * The leaks this gate exists for are an order of magnitude past it: 13,958 and
13
+ * 22,037px for the equation-walk bug, a dimmed label in the test at several
14
+ * thousand.
15
+ */
16
+ export declare const SEEK_TOLERANCE_PX = 1500;
17
+ /** Bump whenever a gate's verdict can change: cached rejections from another version are retried. */
18
+ export declare const GATES_VERSION = "gates-5";
19
+ /**
20
+ * A cue whose picture changes by less than this share of the frame held still:
21
+ * about 310 px at 1080p. A frame that does not move renders to the same bytes,
22
+ * so the floor only has to clear antialiasing; a 14px dot crossing a wire
23
+ * changes ~1,200 px and passes, which is right — that dot IS the explanation.
24
+ * Whether the motion is worth watching is the critique round's question and a
25
+ * person's, not a pixel count's.
26
+ */
27
+ export declare const MIN_CHANGE = 0.00015;
28
+ /** Cues shorter than this are exempt from `static_hold`: there is no room to show anything. */
29
+ export declare const MIN_CUE_SECONDS = 1.2;
30
+ /** Below this the text is not text the audience can read (AGENTS.md invariant 5). */
31
+ export declare const TYPE_FLOOR_PX = 39.5;
32
+ /** One narration cue on a scene's own clock. */
33
+ export interface SceneCue {
34
+ t0: number;
35
+ t1: number;
36
+ text: string;
37
+ }
38
+ /** A scene to probe, with the cues spoken over it. */
39
+ export interface SceneWindow {
40
+ sid: string;
41
+ beatId?: string;
42
+ /** Absolute composition seconds. */
43
+ start: number;
44
+ duration: number;
45
+ cues: SceneCue[];
46
+ }
47
+ export interface CueChange {
48
+ sid: string;
49
+ cue: number;
50
+ t0: number;
51
+ t1: number;
52
+ changed: number;
53
+ total: number;
54
+ }
55
+ export declare function gradeStillCues(rows: readonly CueChange[], min?: number): Finding[];
56
+ /**
57
+ * The settled frame's drawing must span this share of the body box (the
58
+ * bounding box of everything drawn, over the box's area). Round 1's scenes sat
59
+ * at 55-70%: a diagram in the top-left two thirds with the right third or the
60
+ * bottom band empty, at a size the founder called small.
61
+ */
62
+ export declare const STAGE_FILL = 0.8;
63
+ /** The settled frame's largest label, px at 1080p: one thing on the stage is read first. */
64
+ export declare const KEY_TYPE_PX = 64;
65
+ /** A cue group may appear this long before its cue starts (the prompt's own sync window). */
66
+ export declare const EARLY_SLACK = 0.5;
67
+ /**
68
+ * At the settled frame, at most this share of a scene's parts may be ones it
69
+ * showed lit (>= `LIT`) earlier and has left dimmed (< `DIM`). Round 2 kept
70
+ * three scenes that ended on a frame of ghosts; the founder's rule is that the
71
+ * last cue ends with the whole scene lit. A part that was never lit — a halo, a
72
+ * translucent cone — is not counted: it is drawn that way, not dimmed.
73
+ */
74
+ export declare const END_DIMMED = 0.1;
75
+ export declare const LIT = 0.85;
76
+ export declare const DIM = 0.6;
77
+ /** A label as the repair pass sees it: its address, the unit a nudge moves, its box in body px. */
78
+ export interface GeoLabel {
79
+ /** `tag:index` among the body's elements, in document order. */
80
+ a: string;
81
+ /** The unit a nudge would wrap (the text, or its plate group); null when it cannot move. */
82
+ u: string | null;
83
+ /** Document order (paint order). */
84
+ o: number;
85
+ /** x, y, w, h in body px. */
86
+ b: [number, number, number, number];
87
+ /** Screen px per unit of the unit's parent (a camera zoom makes it > 1). */
88
+ s: number;
89
+ fs: number;
90
+ }
91
+ /** What one frame of a bespoke scene looks like to the repair pass. */
92
+ export interface Geo {
93
+ w: number;
94
+ h: number;
95
+ labels: GeoLabel[];
96
+ /** Filled shapes and pictures: address, owning label unit, paint order, box. */
97
+ boxes: Array<{
98
+ a: string;
99
+ u: string | null;
100
+ o: number;
101
+ b: [number, number, number, number];
102
+ }>;
103
+ /** Visible stroke samples, body px, with the label unit they belong to. */
104
+ points: Array<[number, number, string | null]>;
105
+ /** Each visible part's opacity and what dims it (`tag:index`, `#id` appended when it has one). */
106
+ parts: Array<{
107
+ a: string;
108
+ al: number;
109
+ dim: string[];
110
+ }>;
111
+ }
112
+ /** What the page measured at one frame of one scene. */
113
+ export interface Layout {
114
+ sid: string;
115
+ key: string;
116
+ t: number;
117
+ crossings: string[];
118
+ occlusions: string[];
119
+ overlaps: string[];
120
+ small: string[];
121
+ off: string[];
122
+ /** Markers painted where their line is not (an arrowhead on an undrawn path). */
123
+ strays?: string[];
124
+ /** Bounding box of what is drawn in the body box, over the box's area. Bespoke scenes only. */
125
+ fill?: number;
126
+ /** Share of a 6x4 grid over the body box that something is drawn in. Reported, not graded. */
127
+ cells?: number;
128
+ /** The largest visible label in the body box, px. */
129
+ maxType?: number;
130
+ /** Share of the body box the settled drawing paints (`paintedShare`). Reported; the rubric probe reads it. */
131
+ mass?: number;
132
+ /** Share of the drawn labels and shapes in the body box at under 0.6 opacity. */
133
+ dimmed?: number;
134
+ /** Distinct `data-cue` numbers the scene's groups declare. */
135
+ groups?: number[];
136
+ /** The cue groups showing at this instant. */
137
+ revealed?: Array<{
138
+ id: string;
139
+ cue: number;
140
+ }>;
141
+ /** Scene seconds each cue starts at, so `early_reveal` can be graded off this row alone. */
142
+ cueStarts?: number[];
143
+ /** Where the camera is when it is not home (bespoke scenes). Graded at the end only. */
144
+ camOff?: string;
145
+ /** Repair geometry, when the probe asked for it. */
146
+ geo?: Geo;
147
+ /** End row: the share of parts lit earlier and left dimmed (`END_DIMMED`). */
148
+ litDimmed?: number;
149
+ /** End row: the elements dimming those parts, as `tag:index[#id]`. */
150
+ relight?: string[];
151
+ /** The shell's camera at this instant: scale, x, y, and the box's width and height. */
152
+ shot?: [number, number, number, number, number];
153
+ /** How many subjects the scene's illustration has (0 or absent: not an illustrated scene). */
154
+ subjects?: number;
155
+ /** Each visible label that names a subject: its gap to that subject (box px) and how much of it another subject covers. */
156
+ anchors?: Array<{
157
+ id: string;
158
+ k: number;
159
+ d: number;
160
+ o: number;
161
+ }>;
162
+ /** Labels with digits in them sitting on the illustration. */
163
+ onPicture?: number;
164
+ }
165
+ /**
166
+ * A label that names a subject sits within this many box px of that
167
+ * subject's box (0 = on it). A leader line may bridge the gap; a row of plates
168
+ * along the top of the box, round 3's habit, is 150-400px away.
169
+ */
170
+ export declare const ANCHOR_PX = 96;
171
+ /** A label may cover at most this share of itself with ANOTHER subject's box. */
172
+ export declare const ANCHOR_OVERLAP = 0.25;
173
+ /** This many numbers on the picture is a table or a chart painted over it (ja s12, round 3: nine). */
174
+ export declare const NUMBERS_ON_PICTURE = 5;
175
+ /** A shot at this scale or closer is a push-in; under `WIDE` it is the whole picture. */
176
+ export declare const CLOSE = 1.5;
177
+ export declare const WIDE = 1.1;
178
+ /** The camera is sampled this often (scene seconds) for `shot_variety`: no screenshot, a seek and a read. */
179
+ export declare const CAM_STEP = 0.5;
180
+ /** The shell's camera at one instant of one scene (`probeScenes` samples it every `CAM_STEP`). */
181
+ export interface CamSample {
182
+ sid: string;
183
+ t: number;
184
+ /** scale, x, y, and the box's width and height. */
185
+ shot: [number, number, number, number, number];
186
+ /** How many subjects the scene's illustration has. */
187
+ subjects: number;
188
+ }
189
+ /**
190
+ * The distinct shots a scene's camera HOLDS: a push-in counts when two
191
+ * consecutive samples (one `CAM_STEP` apart) are both at `CLOSE` or closer and
192
+ * aimed within `SAME_SHOT` of each other — a camera passing through on its way
193
+ * somewhere is not a shot. Push-ins aimed within `SAME_SHOT` of one already
194
+ * counted are the same shot.
195
+ */
196
+ export declare function shotsOf(samples: readonly CamSample[]): {
197
+ wide: boolean;
198
+ close: Array<[number, number]>;
199
+ };
200
+ /**
201
+ * `shot_variety`: an illustrated scene is staged — it OPENS on the whole
202
+ * picture (every sample in the first second of its first cue is wide), and the
203
+ * camera HOLDS push-ins on at least `min(2, subjects)` different subjects
204
+ * (`shotsOf`); that it comes back is `camera_end`'s. `opens` is each scene's
205
+ * first cue start.
206
+ */
207
+ export declare function gradeShots(samples: readonly CamSample[], opens?: ReadonlyMap<string, number>): Finding[];
208
+ export declare function gradeLayout(rows: readonly Layout[]): Finding[];
209
+ /**
210
+ * What the page did that a scene must not, pinned to the scenes it implicates.
211
+ * A bespoke script that throws is caught by the shell and logged with its scene
212
+ * id (`bespokeCall`), so it lands on that scene alone. A request, a navigation
213
+ * or a CSP refusal names no scene, so it fails every scene probed with it —
214
+ * none of them can be shown to be innocent from inside one page.
215
+ */
216
+ /**
217
+ * Console errors the MACHINE raises, not the page: no scene can cause or fix
218
+ * them. MEASURED 2026-10-09: "The AudioContext encountered an error from the
219
+ * audio device or the WebAudio renderer" appeared on one probe deck in three
220
+ * and failed all five of its scenes — five critique calls, two fallbacks.
221
+ * Round 1 saw the same line once on bytes that had passed an hour earlier.
222
+ */
223
+ export declare const ENVIRONMENT_ERRORS: RegExp[];
224
+ export declare function gradeErrors(errors: readonly string[], sids: readonly string[]): Finding[];
225
+ export interface SeekDiff {
226
+ sid: string;
227
+ key: string;
228
+ t: number;
229
+ /** Which comparison: `descending`, `scrambled`, `cold`. */
230
+ order: string;
231
+ px: number;
232
+ }
233
+ export declare function gradeSeekOrder(rows: readonly SeekDiff[], severity?: Finding["severity"], tolerance?: number): Finding[];
234
+ /**
235
+ * The share of a box's pixels that differ between two frames — with and without
236
+ * a scene's drawing — i.e. how much of its stage the drawing actually paints.
237
+ * `box` is in CSS px of a 1920-wide page; frames may be any scale of it.
238
+ */
239
+ export declare function paintedShare(shown: Frame, hidden: Frame, box: {
240
+ x: number;
241
+ y: number;
242
+ w: number;
243
+ h: number;
244
+ }, delta?: number): number;
245
+ /**
246
+ * Changed pixels that are not on an EDGE of the change: a pixel counts only
247
+ * when it and its 8 neighbours all changed. What `seek_order` compares with.
248
+ *
249
+ * WHY. A picture's first paint can be rasterised a hair differently from a
250
+ * later one (MEASURED 2026-10-09: 12,698px on one illustrated scene's first
251
+ * frame, 2 runs in 3, identical DOM, the difference a 1-2px outline round every
252
+ * subject of the picture). That is the raster, not the scene's state, and it
253
+ * is all edges. A state leak — a term left swollen, a part left lit — changes
254
+ * AREAS, which survive this nearly whole (a 20x20 patch keeps 324 of 400).
255
+ */
256
+ export declare function changedArea(a: Frame, b: Frame, delta?: number): number;
257
+ export declare function changedPixels(a: Frame, b: Frame, delta?: number): number;
258
+ /**
259
+ * Each scene's cues, scene-relative, from the manifest `build` wrote. Segment
260
+ * cues are relative to their own audio; the segment's `start` is where that
261
+ * audio sits on the composition clock (which is the output clock — nothing is
262
+ * frozen, see `framePlan`).
263
+ */
264
+ export declare function sceneWindows(timing: Timing, sids?: ReadonlySet<string>): SceneWindow[];
265
+ /**
266
+ * The instants a scene is photographed at: just after each cue starts, 0.6s in,
267
+ * just before it ends, and the settled end. `sparse` keeps the first cue's start,
268
+ * the cue ends and the end — what `seek_order` on a deck's other scenes needs,
269
+ * at about a third of the screenshots.
270
+ */
271
+ export declare function probeTimes(w: SceneWindow, sparse?: boolean): Array<{
272
+ key: string;
273
+ t: number;
274
+ cue?: number;
275
+ edge?: "a" | "s" | "z";
276
+ }>;
277
+ export interface ProbedFrame {
278
+ sid: string;
279
+ key: string;
280
+ /** Scene-relative seconds. */
281
+ t: number;
282
+ png: Buffer;
283
+ }
284
+ export interface Probe {
285
+ findings: Finding[];
286
+ /** Ascending-pass frames, for a contact sheet. */
287
+ frames: ProbedFrame[];
288
+ cueChanges: CueChange[];
289
+ layout: Layout[];
290
+ seek: SeekDiff[];
291
+ /** The shell's camera every `CAM_STEP` through each illustrated scene (`shot_variety`). */
292
+ cams: CamSample[];
293
+ }
294
+ export interface ProbeOptions {
295
+ /** Which gates. Default all three. `seek` alone is what `verify` runs on plain v2 scenes. */
296
+ gates?: ReadonlyArray<"motion" | "layout" | "seek">;
297
+ /** `seek_order` severity; `verify` passes `warning` for scenes nobody generated. */
298
+ seekSeverity?: Finding["severity"];
299
+ /** Keep the PNGs (the critique round needs them). */
300
+ keepFrames?: boolean;
301
+ /** Also compare a never-seeked page per scene. Default true; a fresh browser each. */
302
+ cold?: boolean;
303
+ /** Photograph cue ends only (see `probeTimes`). */
304
+ sparse?: boolean;
305
+ /** Measure the repair geometry at every graded frame (bespoke scenes). */
306
+ geometry?: boolean;
307
+ /** Opens a fresh page; injected so the graders' wiring is testable. */
308
+ open: () => Promise<DeckPage>;
309
+ /** The array `open` hands to `openDeck({ watch })`; read once the probe ends. */
310
+ errors?: readonly string[];
311
+ }
312
+ /**
313
+ * Photograph `windows` and grade them. One warm page for the ordered passes, one
314
+ * cold page per scene for the first-seek comparison.
315
+ */
316
+ export declare function probeScenes(windows: readonly SceneWindow[], opts: ProbeOptions): Promise<Probe>;
317
+ /**
318
+ * The end row's `litDimmed` and `relight`, from every graded frame of one
319
+ * scene: a part lit (>= `LIT`) at some earlier frame and dimmed (< `DIM`, but
320
+ * visible) at the end, and the elements above it that dim it. A part that was
321
+ * never lit is drawn translucent on purpose and is left alone.
322
+ */
323
+ export declare function endState(rows: Layout[]): void;
324
+ /** `timing.json`, or nothing. */
325
+ export declare function readTimingFile(dir: string): Promise<Timing | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,9 +58,13 @@
58
58
  "demo": "node demo/build.mjs && node dist/cli.js build demo/storyboard.json --source demo/source.json -o demo/deck && python3 -m http.server 8080 --directory demo/deck --bind 127.0.0.1"
59
59
  },
60
60
  "dependencies": {
61
+ "@fontsource-variable/ibm-plex-sans": "5.3.0",
61
62
  "@fontsource-variable/inter": "5.3.0",
63
+ "@fontsource-variable/source-serif-4": "5.3.0",
64
+ "@fontsource-variable/space-grotesk": "5.3.0",
62
65
  "@modelcontextprotocol/sdk": "^1.30.0",
63
66
  "@puppeteer/browsers": "^3.0.6",
67
+ "acorn": "^8.19.0",
64
68
  "commander": "^14.0.1",
65
69
  "fflate": "^0.8.3",
66
70
  "gsap": "^3.14.2",