@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.
- package/README.md +333 -4
- package/dist/cli.js +14057 -4749
- package/dist/deck-player-element.js +1 -1
- package/dist/deck-player.js +1 -1
- package/dist/deck-runtime.js +70 -2
- package/dist/index.js +13309 -4178
- package/dist/mcp.js +5131 -765
- package/dist/types/bespoke/art.d.ts +173 -0
- package/dist/types/bespoke/cache.d.ts +69 -0
- package/dist/types/bespoke/callouts.d.ts +76 -0
- package/dist/types/bespoke/contract.d.ts +57 -0
- package/dist/types/bespoke/inspect.d.ts +69 -0
- package/dist/types/bespoke/pipeline.d.ts +207 -0
- package/dist/types/bespoke/probe.d.ts +34 -0
- package/dist/types/bespoke/prompt.d.ts +182 -0
- package/dist/types/bespoke/references.d.ts +73 -0
- package/dist/types/bespoke/repair.d.ts +121 -0
- package/dist/types/bespoke/scene.d.ts +97 -0
- package/dist/types/bespoke/select.d.ts +52 -0
- package/dist/types/bespoke/sheet.d.ts +26 -0
- package/dist/types/bespoke/shots.d.ts +128 -0
- package/dist/types/bespoke/untangle.d.ts +5 -0
- package/dist/types/build/files.d.ts +13 -1
- package/dist/types/deck/motion.d.ts +64 -0
- package/dist/types/deck/playback.d.ts +206 -0
- package/dist/types/deck/repack.d.ts +10 -0
- package/dist/types/deck/runtime.d.ts +96 -1
- package/dist/types/deck/subtitles.d.ts +29 -1
- package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
- package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
- package/dist/types/emit/archetypes/callout.d.ts +0 -5
- package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
- package/dist/types/emit/archetypes/grid.d.ts +0 -14
- package/dist/types/emit/archetypes/index.d.ts +0 -7
- package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
- package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
- package/dist/types/emit/archetypes/stack.d.ts +1 -1
- package/dist/types/emit/composition.d.ts +83 -3
- package/dist/types/emit/faces.d.ts +22 -0
- package/dist/types/emit/fit.d.ts +127 -0
- package/dist/types/emit/kit.d.ts +121 -2
- package/dist/types/emit/look.d.ts +138 -0
- package/dist/types/emit/motion.d.ts +244 -0
- package/dist/types/emit/svg.d.ts +54 -16
- package/dist/types/emit/theme.d.ts +1 -1
- package/dist/types/emit/themes/index.d.ts +19 -1
- package/dist/types/emit/themes/packs.d.ts +15 -0
- package/dist/types/emit/themes/pick.d.ts +107 -0
- package/dist/types/emit/type.d.ts +111 -0
- package/dist/types/index.d.ts +52 -1
- package/dist/types/plan/codex.d.ts +36 -0
- package/dist/types/plan/direct.d.ts +66 -0
- package/dist/types/prefs.d.ts +25 -2
- package/dist/types/render/capture.d.ts +6 -0
- package/dist/types/render/timing.d.ts +3 -0
- package/dist/types/source/fonts.d.ts +11 -1
- package/dist/types/types.d.ts +109 -0
- package/dist/types/verify/check.d.ts +1 -1
- package/dist/types/verify/fidelity.d.ts +34 -0
- package/dist/types/verify/fill.d.ts +205 -0
- package/dist/types/verify/index.d.ts +14 -0
- package/dist/types/verify/motion.d.ts +33 -0
- package/dist/types/verify/scenes.d.ts +325 -0
- 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.
|
|
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",
|