@jokerized/decksmith 0.9.2 → 0.9.4

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 (60) hide show
  1. package/README.md +449 -90
  2. package/dist/animate/LICENSE +21 -0
  3. package/dist/animate/NOTICE +32 -0
  4. package/dist/animate/core.js +530 -0
  5. package/dist/animate/cut-paper.js +206 -0
  6. package/dist/animate/morph.js +131 -0
  7. package/dist/cli.js +13464 -8876
  8. package/dist/ds-animate.js +1 -0
  9. package/dist/index.js +13245 -8657
  10. package/dist/mcp.js +9778 -7477
  11. package/dist/types/bespoke/art.d.ts +172 -6
  12. package/dist/types/bespoke/cache.d.ts +10 -0
  13. package/dist/types/bespoke/cards.d.ts +66 -0
  14. package/dist/types/bespoke/contract.d.ts +0 -8
  15. package/dist/types/bespoke/databuild.d.ts +50 -0
  16. package/dist/types/bespoke/depth.d.ts +127 -0
  17. package/dist/types/bespoke/grammar.d.ts +73 -0
  18. package/dist/types/bespoke/inspect.d.ts +40 -1
  19. package/dist/types/bespoke/pipeline.d.ts +195 -11
  20. package/dist/types/bespoke/probe.d.ts +2 -2
  21. package/dist/types/bespoke/prompt.d.ts +110 -34
  22. package/dist/types/bespoke/references.d.ts +13 -14
  23. package/dist/types/bespoke/repair.d.ts +23 -29
  24. package/dist/types/bespoke/scene.d.ts +55 -10
  25. package/dist/types/bespoke/select.d.ts +28 -21
  26. package/dist/types/bespoke/sheet.d.ts +66 -1
  27. package/dist/types/bespoke/shots.d.ts +225 -22
  28. package/dist/types/build/files.d.ts +7 -0
  29. package/dist/types/build/piece.d.ts +8 -0
  30. package/dist/types/emit/animate-runtime.d.ts +149 -0
  31. package/dist/types/emit/archetypes/claim-figure.d.ts +84 -1
  32. package/dist/types/emit/archetypes/equation-walk.d.ts +2 -2
  33. package/dist/types/emit/archetypes/hero-number.d.ts +21 -0
  34. package/dist/types/emit/archetypes/index.d.ts +2 -2
  35. package/dist/types/emit/archetypes/kinetic.d.ts +2 -0
  36. package/dist/types/emit/archetypes/pipeline.d.ts +14 -2
  37. package/dist/types/emit/archetypes/stage.d.ts +57 -0
  38. package/dist/types/emit/backdrop.d.ts +114 -0
  39. package/dist/types/emit/kit.d.ts +8 -0
  40. package/dist/types/emit/look.d.ts +14 -0
  41. package/dist/types/emit/motion.d.ts +78 -46
  42. package/dist/types/emit/type.d.ts +48 -5
  43. package/dist/types/images/providers.d.ts +6 -0
  44. package/dist/types/plan/codex.d.ts +4 -2
  45. package/dist/types/plan/direct.d.ts +1 -1
  46. package/dist/types/plan/duration.d.ts +1 -1
  47. package/dist/types/plan/prompt.d.ts +1 -1
  48. package/dist/types/plan/refs.d.ts +2 -2
  49. package/dist/types/plan/variety.d.ts +74 -0
  50. package/dist/types/prefs.d.ts +20 -1
  51. package/dist/types/render/captions.d.ts +4 -5
  52. package/dist/types/render/capture.d.ts +7 -0
  53. package/dist/types/source/fonts.d.ts +22 -0
  54. package/dist/types/types.d.ts +680 -4
  55. package/dist/types/verify/drift.d.ts +5 -0
  56. package/dist/types/verify/fidelity.d.ts +15 -1
  57. package/dist/types/verify/index.d.ts +27 -4
  58. package/dist/types/verify/scenes.d.ts +185 -5
  59. package/package.json +1 -1
  60. package/dist/types/bespoke/callouts.d.ts +0 -76
@@ -26,15 +26,14 @@
26
26
  * this module) all still agree. Emphasis lives strictly inside a hold window
27
27
  * and is back at rest before the window closes, so every frame a gate
28
28
  * captures at a hold is the frame v0.8.0 would have captured there.
29
- * 4. Up, never down. Emphasis grows a part (`LIFT`) and never shrinks one: the
30
- * type floor reads declared sizes, so a part scaled below 1 at a hold would
31
- * pass at a size it is not drawn at.
29
+ * 4. Nothing is scaled. Emphasis is light (`glow`), so the type floor's declared
30
+ * sizes are the drawn ones at every hold.
32
31
  *
33
32
  * INTERFACE, for the other v2 tracks (player, fit, style, layout):
34
33
  *
35
- * - `planMotion(seed, beats)` → which entrance verb each scene uses, which seam
36
- * joins each pair, and the order emphasis kinds are tried in. A style pack
37
- * that wants to bias the grammar should filter `ENTRANCES`/`SEAMS` before
34
+ * - `planMotion(seed, beats)` → which entrance verb each scene uses (a fade),
35
+ * which seam joins each pair, and the order emphasis kinds are tried in. A
36
+ * style pack that wants to bias the grammar should filter `SEAMS` before
38
37
  * calling it rather than post-edit its answer, so the no-repeat rules still
39
38
  * hold.
40
39
  * - `restyleEntrance(scene, sid, verb)` rewrites an emitted scene's own
@@ -50,26 +49,20 @@
50
49
  import type { Archetype, BeatRole } from "../types.js";
51
50
  import { type Scene, type Tween } from "./kit.js";
52
51
  /**
53
- * How a scene's parts arrive. Six, so the no-repeat rule always has at least
54
- * four to choose from after excluding the previous scene's verb and a capped
55
- * `rise`.
56
- *
57
- * | verb | what moves | ease |
58
- * |-------|----------------------------------------------|---------------|
59
- * | rise | fade up from below (v0.8.0's, kept, capped) | power3.out |
60
- * | slide | in from the reading side | expo.out |
61
- * | snap | scale up from 0.9 with a small overshoot | back.out(1.7) |
62
- * | focus | out of a blur (HTML) / short rise (SVG) | circ.out |
63
- * | wipe | clip-path opens left to right (HTML) | power4.inOut |
64
- * | mask | clip-path opens bottom-up while rising (HTML)| power4.out |
65
- *
66
- * `focus`, `wipe` and `mask` touch `filter`/`clip-path`, which are only applied
67
- * to HTML elements this module can SEE are HTML (an id whose tag it read in the
68
- * scene's markup). An SVG part or a class selector gets the verb's transform
69
- * fallback instead, because clip-path on an SVG child clips against a reference
70
- * box nobody measured.
71
- */
72
- export declare const ENTRANCES: readonly ["rise", "slide", "snap", "focus", "wipe", "mask"];
52
+ * How a scene's parts arrive: ONE verb, a quiet opacity fade, in place.
53
+ *
54
+ * There were six — rise, slide, snap (a pop from 0.9 with overshoot), focus
55
+ * (out of a blur), wipe and mask (clip-path sweeps) — hashed per scene so no
56
+ * two adjacent scenes arrived alike. The founder's verdict on the result
57
+ * (2026-10-10): "graphic animation by animated UI elements is old-fashioned".
58
+ * Plates, chips, cards and labels sliding, popping or being swept in WERE the
59
+ * motion, and that is the thing he named. So a v2 archetype's parts now simply
60
+ * appear, and what moves is the picture: the backdrop's drift and the camera
61
+ * (src/emit/backdrop.ts, src/emit/camera.ts), the seams between scenes, and
62
+ * things inside a figure that are its content (a probe travelling a curve, a
63
+ * pulse travelling a pipeline). Those are not entrances and are never touched.
64
+ */
65
+ export declare const ENTRANCES: readonly ["fade"];
73
66
  export type Entrance = (typeof ENTRANCES)[number];
74
67
  /**
75
68
  * How one scene hands off to the next. `dive` is the existing camera move and is
@@ -80,20 +73,34 @@ export type Entrance = (typeof ENTRANCES)[number];
80
73
  * | dissolve | opacity → 0, power2.in (v0.8.0) | — |
81
74
  * | push | xPercent 0 → −4, opacity → 0 | xPercent 4 → 0 |
82
75
  * | lift | yPercent 0 → −4, opacity → 0 | yPercent 4 → 0 |
83
- * | wipe | clip-path closes left to right | — |
84
76
  * | zoom | scale 1 → 1.04, opacity → 0 | scale 0.96 → 1 |
85
77
  *
78
+ * There was a fifth, `wipe`: a clip-path closing left to right. It was cut
79
+ * (2026-10-10) because a hard edge sweeping a whole slide is a graphic, not a
80
+ * picture: at 4fps the r3 deck showed it as half a robot and a lone chip on an
81
+ * empty slide (s4→s5), a bare blue rectangle (s8→s9). Every seam left is a
82
+ * fade the eye reads as a cut or a camera move.
83
+ *
84
+ * Each seam also clears the outgoing scene's TYPE first (`chromeOut`), so two
85
+ * headlines are never on screen together.
86
+ *
86
87
  * 4% is under the scene's own padding at every format (110px of 1920 is 5.7%,
87
88
  * 84px of 1080 is 7.8%), so a mid-seam frame never puts content off the canvas
88
89
  * for the layout gate's grid samples to find.
89
90
  */
90
- export declare const SEAMS: readonly ["dissolve", "push", "lift", "wipe", "zoom"];
91
+ export declare const SEAMS: readonly ["dissolve", "push", "lift", "zoom"];
91
92
  export type Seam = (typeof SEAMS)[number] | "dive";
92
- /** What moves while the narrator talks about a part: grow, glow, underline. */
93
- export declare const EMPHASES: readonly ["pulse", "glow", "underline"];
93
+ /**
94
+ * What moves while the narrator talks about a part: light, and only light. A
95
+ * `pulse` (the part scaled up and back) and an `underline` (a rule faded in
96
+ * under it) were the other two, and both are a UI element doing a move — the
97
+ * thing the founder called old-fashioned (2026-10-10). A glow is the part being
98
+ * lit, which is something a picture does.
99
+ */
100
+ export declare const EMPHASES: readonly ["glow"];
94
101
  export type Emphasis = (typeof EMPHASES)[number];
95
102
  /** Every ease this module writes. A test pins that nothing else appears. */
96
- export declare const MOTION_EASES: readonly ["power3.out", "expo.out", "back.out(1.7)", "circ.out", "power4.inOut", "power4.out", "power2.in", "power2.inOut", "sine.out", "sine.inOut"];
103
+ export declare const MOTION_EASES: readonly ["power3.out", "expo.out", "power2.in", "power2.inOut", "sine.out", "sine.inOut"];
97
104
  /**
98
105
  * FNV-1a, 32-bit. The seeded choice every v2 decision is made with: stable
99
106
  * across platforms and Node versions, which a `Math.random` with a seed is not.
@@ -116,23 +123,16 @@ export interface MotionPlan {
116
123
  /** Per beat, the order emphasis kinds are tried in (the first that fits wins). */
117
124
  emphases: Emphasis[][];
118
125
  }
119
- /**
120
- * At most this share of a deck's scenes open with `rise`, the one verb that
121
- * reads as v0.8.0's stock fade-up. The plan's target is "modal entrance ≤ 40%".
122
- */
123
- export declare const RISE_SHARE = 0.25;
124
126
  /**
125
127
  * The deck's motion, decided once for the whole storyboard.
126
128
  *
127
- * ENTRANCES: each beat hashes into the verbs still allowed to it — never the
128
- * previous beat's, and `rise` only while under `RISE_SHARE`. Hash-then-filter
129
- * rather than a rotation so two decks with the same archetype sequence still
130
- * move differently, which is the cross-deck sameness the critic flagged.
129
+ * ENTRANCES: every beat fades (`ENTRANCES`). Variety between scenes is the
130
+ * seams' and the pictures' job now, not the parts'.
131
131
  *
132
132
  * SEAMS are chosen by the RELATION between the two beats first, then varied:
133
133
  * same family (two quantity beats) → `push`, a lateral continuation; a role
134
134
  * boundary (`background` → `limitations`) → `zoom`, a chapter break; into or
135
- * out of a title → `lift`; any other family change → `wipe`; into the closing
135
+ * out of a title → `lift`; any other family change → `zoom` or `dissolve`; into the closing
136
136
  * beat → `dissolve`. A seam that would repeat the previous one is replaced by a
137
137
  * hashed pick from the rest. Then, for a deck of 10+ beats with fewer than three
138
138
  * kinds, the earliest replaceable seams are rotated through the missing kinds.
@@ -147,18 +147,29 @@ export declare function planMotion(seed: string, beats: readonly MotionBeat[]):
147
147
  export declare function ensureSeamKinds(seams: Seam[], want: number): void;
148
148
  /**
149
149
  * A tween this module may restyle: an opacity 0 → 1 reveal whose `from` only
150
- * moves opacity and position/scale, rendered immediately (the element's own
151
- * first appearance). Dims, restores, draw-ons, pulses and anything with
152
- * `immediateRender: false` are someone else's and stay exactly as emitted.
150
+ * moves opacity and position/scale. Dims, restores, draw-ons and colour turns
151
+ * are someone else's and stay exactly as emitted.
152
+ *
153
+ * `immediateRender: false` reveals COUNT. They used to be skipped as "someone
154
+ * else's", and they are mostly a panel's lines or a card's words staggering in
155
+ * after their card — the same sliding-in the founder objected to, one level
156
+ * down. A fade only removes keys, so it cannot become a second writer of
157
+ * anything the tween did not already write.
153
158
  */
154
159
  export declare function isEntrance(t: Tween): boolean;
155
- /** The tag of the element an `#id` selector names in this markup, if it is one. */
156
- export declare function tagOf(html: string, target: string): string | undefined;
157
160
  /**
158
161
  * One scene's entrances, re-voiced in `verb`. Positions and durations are
159
162
  * untouched (rule 3 at the top), so this cannot move a hold or the voice.
160
163
  */
161
164
  export declare function restyleEntrance(scene: Scene, sid: string, verb: Entrance): Scene;
165
+ /**
166
+ * `restyleEntrance` for the CHROME alone: the eyebrow and headline the shell
167
+ * draws around a bespoke scene with the stock `chromeIn` (src/bespoke/scene.ts).
168
+ * The scene's own tweens are its author's and are left exactly as written; the
169
+ * chrome is ours, and unrestyled it slid up from y:14/22 starting at 0.15s,
170
+ * straight over the outgoing scene's headline (r3, s13→s14).
171
+ */
172
+ export declare function restyleChrome(scene: Scene, sid: string, verb: Entrance): Scene;
162
173
  /**
163
174
  * When the incoming chrome may start, in unpaced seconds: once the outgoing
164
175
  * scene's handoff (`HANDOFF_SECONDS`, 0.4) has taken it below half opacity,
@@ -180,6 +191,27 @@ export declare const SEAM_CLEAR = 0.3;
180
191
  * tween, so a v2 deck whose plan says dissolve hands off as v0.8.0 did.
181
192
  */
182
193
  export declare function seamOut(kind: Exclude<Seam, "dive">, sid: string, at: number, over: number): Tween[];
194
+ /**
195
+ * The share of a handoff the outgoing scene's TYPE takes to fade out. Its
196
+ * eyebrow and headline are gone by `CHROME_OUT * over` (0.16s of 0.4), well
197
+ * before the incoming chrome may start at `SEAM_CLEAR` (0.3), so the two
198
+ * headlines never share a frame. The picture keeps the whole `over` to
199
+ * dissolve under the next scene.
200
+ *
201
+ * WHY. The root fade alone left the outgoing headline at 23-75% while the
202
+ * incoming one came up at the same place: r3's s12→s13 and s13→s14 seams
203
+ * showed both headlines and both eyebrows overprinted for about two frames at
204
+ * 4fps. `power2.in` on the root holds the old type high exactly when the new
205
+ * type arrives, so no ease on the root alone can fix it.
206
+ */
207
+ export declare const CHROME_OUT = 0.4;
208
+ /**
209
+ * The outgoing half of a seam for the scene's own type: `#sid-e` and
210
+ * `#sid-h`, whichever of them `html` draws (a title-less scene draws no
211
+ * eyebrow, and a tween on an absent id is a GSAP warning). `immediateRender:
212
+ * false`, so it never paints at build time over the chrome's own entrance.
213
+ */
214
+ export declare function chromeOut(sid: string, html: string, at: number, over: number): Tween[];
183
215
  /**
184
216
  * The INCOMING half, on the next scene's own timeline from its 0. Settles by
185
217
  * `firstHold - 0.05`, so the frame at the first stop is the frame v0.8.0 drew
@@ -71,6 +71,46 @@ export declare function familyOf(face: LatinFace): string;
71
71
  * draws a glyph; it is there so (body, display) pairs give distinct stacks.
72
72
  */
73
73
  export declare function stackFor(body: LatinFace, display: LatinFace): string;
74
+ /**
75
+ * THE V2 TYPE SCALE in px at 1920x1080 — the founder's, confirmed 2026-10-10,
76
+ * after "the fonts are too large": a 56px headline, 40-44px body and labels, a
77
+ * 40px kicker. No 160-560px hero numeral and no 64px+ kinetic type; the
78
+ * narration and its subtitles carry the words, so what is ON the slide is
79
+ * minimal and quiet.
80
+ *
81
+ * One table, read by every v2 emitter (the chrome through `TYPES` below, each
82
+ * archetype through `v2Text`), by src/bespoke's prompt, labels and gates, and —
83
+ * restated, because that file may import nothing from here — by the burned
84
+ * caption in src/types.ts, so a size cannot be lowered in one place and left
85
+ * large in another. Never under `floor`: AGENTS.md invariant 5. Classic never
86
+ * reads it — `--design classic` is still the complete rollback.
87
+ */
88
+ export declare const TYPE_SCALE: {
89
+ /** The line that says the slide: headline, title slide, a hero statement. */
90
+ readonly headline: 56;
91
+ /** Body copy and the largest a label is set: labels run `label`..`body`. */
92
+ readonly body: 44;
93
+ /** The smallest a label is set; with `body`, the 40-44px label band. */
94
+ readonly label: 40;
95
+ /** Eyebrow / kicker. */
96
+ readonly kicker: 40;
97
+ /** A burned subtitle (src/types.ts `captionFontSize` restates it). */
98
+ readonly caption: 40;
99
+ /** Never under this (invariant 5). */
100
+ readonly floor: 40;
101
+ /**
102
+ * The largest a display equation is asked at. Not the headline's 56: KaTeX
103
+ * sets a sub- or superscript at 0.7em, and at 64 that is 44.8px — on the body
104
+ * scale, clear of the floor. Classic asked up to 108; v2 grew it to 140.
105
+ */
106
+ readonly math: 64;
107
+ };
108
+ /**
109
+ * A text size as v2 sets it: `px` held inside `[floor, cap]`. Classic passes
110
+ * through untouched. `cap` defaults to body/label size; a role that IS the
111
+ * headline passes `TYPE_SCALE.headline`.
112
+ */
113
+ export declare function v2Text(v2: boolean, px: number, cap?: number): number;
74
114
  /** Today's chrome, exactly. `title.ts` exported these numbers before specs existed. */
75
115
  export declare const CLASSIC_TYPE: TypeSpec;
76
116
  /**
@@ -84,11 +124,14 @@ export declare const CLASSIC_TYPE: TypeSpec;
84
124
  * - `grotesk-plex`: Space Grotesk headline over a Plex body.
85
125
  * - `plex-serif`: a Plex headline over a serif body, the inverse of `serif-inter`.
86
126
  *
87
- * HEIGHT. A one-line eyebrow plus a one-line headline costs at most classic's
88
- * 146px in every spec, so a pack's bigger type comes out of its leading and
89
- * gaps rather than out of the body's room. Measured why: `serif` at 44/70 cost
90
- * a real deck a six-bar chart (`costsNothing` in themes/pick.ts now refuses
91
- * such a pack, but a pack that rarely needs refusing is the better pack).
127
+ * SIZE IS NOT A VOICE ANY MORE. Every spec sets the eyebrow at `TYPE_SCALE.kicker`
128
+ * and the headline — the title slide's included — at `TYPE_SCALE.headline`; the
129
+ * pairs differ in face, weight, tracking, case and leading. They used to differ
130
+ * in size too (60-70px headlines, 144-168px titles), and the founder's verdict
131
+ * on those was "the fonts are too large".
132
+ *
133
+ * HEIGHT. A one-line eyebrow plus a one-line headline costs well under classic's
134
+ * 146px in every spec, so nothing here takes room from the body.
92
135
  *
93
136
  * FLOORS. Every eyebrow is at least 40px (invariant 5). Title tracking is never
94
137
  * positive: `fitText` measures untracked, so only a tightening is free.
@@ -7,6 +7,12 @@ export interface ImageRequest {
7
7
  /** `images.style`, folded into every prompt. */
8
8
  style: string;
9
9
  aspect: ImageAspect;
10
+ /**
11
+ * The picture covers the frame — a stage or a backdrop — rather than sitting
12
+ * on a plate. Absent means a plate, which is every request made before this
13
+ * existed, so their prompts and cache keys do not move.
14
+ */
15
+ bleed?: boolean;
10
16
  /**
11
17
  * `images.model`, for the separate backend only; the Codex and tool rungs
12
18
  * ignore it. Carried on the request rather than baked into the provider
@@ -1,7 +1,7 @@
1
- import type { Prefs } from "../prefs.js";
1
+ import { type Prefs } from "../prefs.js";
2
2
  import { type Source, type Storyboard } from "../types.js";
3
3
  /** The schema for one run. `role` is present only when the paper arc was asked for. */
4
- export declare function schemaFor(prefs: Pick<Prefs, "genre">): unknown;
4
+ export declare function schemaFor(prefs: Pick<Prefs, "genre" | "bespoke" | "design">): unknown;
5
5
  /**
6
6
  * The default-preferences schema, unchanged and still exported: `general` hides
7
7
  * `role`, so these bytes are what they have always been.
@@ -18,6 +18,8 @@ export interface CodexOptions {
18
18
  prefs?: Prefs;
19
19
  /** Swappable so a test can drive the real parse path without spawning anything. */
20
20
  run?: Runner;
21
+ /** Told what the first plan broke, before the one repair round is asked for. */
22
+ onRepair?: (broken: readonly string[]) => void;
21
23
  }
22
24
  export interface RunnerArgs {
23
25
  prompt: string;
@@ -42,7 +42,7 @@ export interface Direction {
42
42
  export interface LookSummary {
43
43
  beats: number;
44
44
  distinct: number;
45
- /** Share of beats whose chrome sits on top of the slide (title excluded from the numerator). */
45
+ /** Share of beats whose chrome sits on top of the slide (title and chromeless beats excluded from the numerator). */
46
46
  modalChrome: number;
47
47
  /** Adjacent pairs with the same signature. */
48
48
  adjacentRepeats: number;
@@ -43,7 +43,7 @@
43
43
  * planner, no TTS and no ffmpeg.
44
44
  */
45
45
  import type { Prefs } from "../prefs.js";
46
- import type { Source } from "../types.js";
46
+ import { type Source } from "../types.js";
47
47
  /**
48
48
  * Characters of text per second of speech.
49
49
  *
@@ -1,4 +1,4 @@
1
- import type { Prefs } from "../prefs.js";
1
+ import { type Prefs } from "../prefs.js";
2
2
  import { type Source } from "../types.js";
3
3
  /**
4
4
  * How many sentences each archetype wants, because it is how many stops it has.
@@ -6,8 +6,8 @@ export interface PendingIllustration {
6
6
  where: string;
7
7
  }
8
8
  /**
9
- * Every pending slot, in beat order. A claim-figure without a `figureId` is
10
- * pending by construction — the schema insists on one of the two — and a
9
+ * Every pending slot, in beat order. A claim-figure or stage without a
10
+ * `figureId` is pending by construction — the schema insists on one of the two — and a
11
11
  * split-compare side is pending only when it carries a brief, because a side
12
12
  * with neither is a list.
13
13
  */
@@ -0,0 +1,74 @@
1
+ /**
2
+ * VARIETY: the plan-level rule that a deck must not be one layout repeated.
3
+ *
4
+ * The measured failure this answers (2026-10-09, a 14-beat Korean paper deck,
5
+ * no figures, planned with `--images`): 5 split-compare, 5 pipeline, 2 callout,
6
+ * 1 title, 1 bar-compare, ZERO illustrations — and beats 2 to 12 alternated
7
+ * split-compare / pipeline / split-compare / pipeline for nine slides running.
8
+ * RULE 1's "not the same archetype twice in a row" held at every pair, and the
9
+ * deck still read, slide after slide, as the same two cards on a pale ground.
10
+ * The prompt said a stage MAY carry an illustration; the planner never did.
11
+ *
12
+ * So the rule is a minimum, not an invitation, and it is CHECKED: the prompt
13
+ * (src/plan/prompt.ts) states these numbers by importing them from here, and
14
+ * `codexPlanner` (src/plan/codex.ts) runs `varietyFindings` on what comes back,
15
+ * asks once for a repair, and refuses a plan that still breaks it. One table,
16
+ * read by both, so the sentence the model is given and the check it is held to
17
+ * cannot drift apart.
18
+ *
19
+ * WHAT IT DELIBERATELY DOES NOT DO. It never rewrites a beat. Turning a pipeline
20
+ * into a stage needs a picture brief only the planner can write honestly, and
21
+ * reordering beats breaks the narration's argument. A plan that breaks the rule
22
+ * goes back to the planner with the reasons, which is the only repair that
23
+ * keeps the deck's content the planner's.
24
+ */
25
+ import type { Prefs } from "../prefs.js";
26
+ import type { Archetype, Beat, Design, Storyboard } from "../types.js";
27
+ /** A deck this long must carry stage beats. Below it there is no room to spend one. */
28
+ export declare const STAGE_MIN_BEATS = 8;
29
+ /** At least one stage per this many beats, when the deck may ask for pictures. */
30
+ export declare const STAGE_EVERY = 4;
31
+ /** Longest A/B/A/B run allowed: three beats (A, B, A) is a return; four is a rut. */
32
+ export declare const MAX_ALTERNATION = 3;
33
+ /**
34
+ * At least this share of a deck's beats are SCENES — a picture owns or backs
35
+ * them — when the deck may ask for pictures. Three stages in fourteen still left
36
+ * nine beats of cards on the pack's pale ground in the 2026-10-09 ko e2e; a
37
+ * diagram with a `backdrop` is drawn over a picture, so "most beats are scenes"
38
+ * no longer costs the diagram.
39
+ */
40
+ export declare const SCENE_SHARE = 0.6;
41
+ /**
42
+ * The archetypes that draw boxes, cards or rows of them — what the founder
43
+ * means by slideware. A backdrop puts them in a scene but they are still
44
+ * panels: round 1 of the 2026-10-09 ko e2e had ten of fourteen beats as
45
+ * scenes, and nine of fourteen were still panels over pictures.
46
+ */
47
+ export declare const PANEL_ARCHETYPES: ReadonlySet<Archetype>;
48
+ /** At most this share of a picture deck's beats are panels: five of fourteen. */
49
+ export declare const PANEL_SHARE = 0.36;
50
+ /** How many panel beats a deck of `beats` may carry, given whether it may ask for pictures. */
51
+ export declare function panelsAllowed(beats: number, images: Prefs["images"]): number;
52
+ /** How many stage beats a deck of `beats` must carry, given the picture cap. */
53
+ export declare function stagesRequired(beats: number, images: Prefs["images"]): number;
54
+ /** How many beats of a deck of `beats` must be scenes (`SCENE_SHARE`), given the picture cap. */
55
+ export declare function scenesRequired(beats: number, images: Prefs["images"]): number;
56
+ /**
57
+ * Whether a picture owns or backs this beat: a stage, a split-compare side
58
+ * with a picture, or a diagram over a `backdrop`. A brief counts as a figure
59
+ * does — it is a picture by the time anyone sees the deck. A hero number and a
60
+ * kinetic claim are scenes with or without one: with none they fill the frame
61
+ * with a field of colour, never the pale ground — and so, in v2, is a
62
+ * bar-compare.
63
+ *
64
+ * NOT a claim-figure or an annotated-figure: both draw their figure as a card
65
+ * on the pack's pale ground (neither goes through `overBackdrop` or
66
+ * `overField`), so counting them let a plan meet `SCENE_SHARE` with exactly
67
+ * the cards-on-paper the share exists to refuse.
68
+ */
69
+ export declare function isScene(beat: Beat, design?: Design): boolean;
70
+ /**
71
+ * Every way `storyboard` breaks the variety rule, as sentences the planner can
72
+ * act on. Empty is a pass.
73
+ */
74
+ export declare function varietyFindings(storyboard: Storyboard, images: Prefs["images"], design?: Design): string[];
@@ -44,6 +44,16 @@ export declare function prefsFromFlags(flags: PrefFlags): PrefsPatch;
44
44
  * storyboard planned under v2.
45
45
  */
46
46
  export declare function designFor(prefs: Pick<Prefs, "design">, recorded?: Design): Design;
47
+ /**
48
+ * The bespoke pass a build runs, with every default filled in, or undefined.
49
+ * ON BY DEFAULT UNDER V2: every beat is drawn by a generated scene and the
50
+ * archetype is only its fallback. `--no-bespoke` (`"bespoke": {"enabled":
51
+ * false}`) turns it off; classic never runs it, so a classic deck keeps its
52
+ * bytes.
53
+ */
54
+ export declare function bespokeFor(prefs: Pick<Prefs, "bespoke">, design: Design): {
55
+ prefs: NonNullable<Prefs["bespoke"]>;
56
+ } | undefined;
47
57
  /** The flag surface the CLI exposes. Every one optional: absent means unstated. */
48
58
  export interface PrefFlags {
49
59
  slides?: string | number;
@@ -71,7 +81,7 @@ export interface PrefFlags {
71
81
  imageModel?: string;
72
82
  imageStyle?: string;
73
83
  imageMax?: string | number;
74
- /** `--bespoke`: generated scenes for a few mechanism beats (v2 only). */
84
+ /** `--bespoke` / `--no-bespoke`: generated scenes for every beat (v2 only; on by default there). */
75
85
  bespoke?: boolean;
76
86
  bespokeCalls?: string | number;
77
87
  bespokeSeconds?: string | number;
@@ -80,3 +90,12 @@ export interface PrefFlags {
80
90
  bespokeCache?: string;
81
91
  bespokeArt?: string | number;
82
92
  }
93
+ /**
94
+ * Commander gives a plain `--flag` `undefined` until it is passed, which is
95
+ * exactly the "unstated" that `prefsFromFlags` needs — except for `--no-x`,
96
+ * which defaults to `true` and would therefore outrank the config file on every
97
+ * run. So the negation is read as a negation and nothing else. `--images` is
98
+ * the other one read by hand: it is a boolean, and the loop below only knows
99
+ * strings.
100
+ */
101
+ export declare function flagsFromOptions(o: Record<string, unknown>): PrefFlags;
@@ -27,11 +27,10 @@ export interface CaptionBand {
27
27
  * cues. Hidden by default and revealed one at a time, so what is measured is
28
28
  * exactly what is captured.
29
29
  *
30
- * The scrim is `box-decoration-break: clone`, which gives each LINE its own box
31
- * — the same treatment ASS's `BorderStyle=4` produces, and the only one that
32
- * reads as a caption rather than as a letterbox: a single box around a block
33
- * whose second line is two words wide is a wide black bar with a short line
34
- * floating in it.
30
+ * There is no scrim (2026-10-10): the founder asked for a quiet lower third,
31
+ * not a box, so each line is white type with a close dark halo. The span keeps
32
+ * `box-decoration-break: clone` and its padding so every line's halo lies
33
+ * inside the rectangle `measure` reports and the band is captured with it.
35
34
  *
36
35
  * `line-height` is set from a measurement, not from a ratio. The inline box is
37
36
  * as tall as the font's content area plus the padding, and that is a number only
@@ -103,6 +103,13 @@ export interface DeckPage {
103
103
  shoot(): Promise<Buffer>;
104
104
  /** For callers that need to read the DOM at a stop, as `fidelity` does. */
105
105
  page: Page;
106
+ /**
107
+ * Every uncaught error the page has raised since it opened, in order. The
108
+ * runtime swallows a throw inside a seek, so this is the only place an
109
+ * animate piece's drawing error (`reportError`) surfaces: without it a seek
110
+ * resolves over a stale or half-painted canvas.
111
+ */
112
+ errors: string[];
106
113
  close(): Promise<void>;
107
114
  }
108
115
  export interface OpenOptions {
@@ -23,3 +23,25 @@ export declare function familyFor(lang: string): string | null;
23
23
  export declare function bundleFont(lang: string, glyphs: string, dir: string, opts?: {
24
24
  serif?: boolean;
25
25
  }): Promise<FontBundle | null>;
26
+ /**
27
+ * Inter's vertical metrics as `@font-face` descriptors: ascent 0.969em,
28
+ * descent 0.241em, no line gap.
29
+ *
30
+ * Every vertical number in the emitters (line boxes, the SVG label `dy`, the
31
+ * centring of a tspan block) and every box the gates measure was tuned on Inter.
32
+ * `src/build/files.ts` declares a v2 pack's Latin faces with these for that
33
+ * reason, and the CJK bundle needs them for the same one. Noto Sans SC as Google
34
+ * serves it is 1.160 / 0.288 (hhea, read from the woff2), so its line box is
35
+ * 1.448em where the emitters budget 1.21. MEASURED in Chrome on a zh-Hans deck
36
+ * (HypePaper, v0.9.2, 2026-10-11): a 56px split-compare heading spanned 65px
37
+ * above its baseline and overran the top of its svg by 11px. Every verify
38
+ * reports that as `container_overflow` on `#sN-sideI`; HypePaper's build also
39
+ * reported it as `text_box_overflow` on `#sN-labI`, failed on it, and a later
40
+ * verify of the same deck did not. The ink was never cut, because a Han glyph's
41
+ * ink sits inside 0.88em. Only the box was wrong.
42
+ *
43
+ * Advances are untouched, so the width tables in `src/emit/svg.ts` still apply.
44
+ */
45
+ export declare const INTER_METRICS = "ascent-override: 96.875%;\n descent-override: 24.1211%;\n line-gap-override: 0%";
46
+ /** Declare every `@font-face` in `css` with `INTER_METRICS`. Idempotent. */
47
+ export declare function withInterMetrics(css: string): string;