@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
@@ -1,7 +1,2 @@
1
- /**
2
- * Labelled panels side by side — the archetype for the things a paper does not
3
- * put in a figure: a contradiction between two tables, a caveat, a limit on what
4
- * was actually tested. Panels appear one at a time so each can be spoken to.
5
- */
6
1
  import type { Emitter } from "../kit.js";
7
2
  export declare const callout: Emitter<"callout">;
@@ -1,2 +1,17 @@
1
+ import { type Fit } from "../fit.js";
1
2
  import type { Emitter } from "../kit.js";
3
+ type Mode = "tall" | "wide" | "beside";
4
+ /** One way to draw the beat, predicted. */
5
+ export interface Arrangement {
6
+ mode: Mode;
7
+ claimSize: number;
8
+ plate: {
9
+ w: number;
10
+ h: number;
11
+ };
12
+ fit: Fit;
13
+ }
14
+ /** The arrangement to draw: the classic mode unless another fills clearly more. */
15
+ export declare function choose(options: readonly Arrangement[]): Arrangement;
2
16
  export declare const claimFigure: Emitter<"claim-figure">;
17
+ export {};
@@ -1,16 +1,2 @@
1
- /**
2
- * A field, and the windows that move over it.
3
- *
4
- * Attention spans, receptive fields, patches, pooling kernels, token sequences —
5
- * every paper draws these as a rectangle over a lattice, and every slide deck
6
- * then writes "8x8 shifted windows" in a bullet and calls it explained. The
7
- * content of the idea is *which cells* and *how many of them*, and a sentence
8
- * cannot show that; a lattice with the rectangle lit on it shows nothing else.
9
- *
10
- * Everything here is solved from the box that is actually free after the chrome
11
- * and the note, so a 24x16 feature map and a 2x1 pair of patches are the same
12
- * code. A constant cell size would be right for one grid and wrong for the other
13
- * twenty-three, and nobody would ever find out which.
14
- */
15
1
  import type { Emitter } from "../kit.js";
16
2
  export declare const grid: Emitter<"grid">;
@@ -1,10 +1,3 @@
1
- /**
2
- * The explanatory vocabulary, indexed by archetype.
3
- *
4
- * The mapped type is the point: adding an archetype to `Beat` without an emitter
5
- * beside it fails to compile, which is the only guarantee that keeps the union in
6
- * `types.ts` honest.
7
- */
8
1
  import type { Archetype, Beat } from "../../types.js";
9
2
  import type { EmitContext, Emitter, Scene } from "../kit.js";
10
3
  export declare const emitters: {
@@ -1,11 +1,3 @@
1
- /**
2
- * A chart the source never drew.
3
- *
4
- * Papers report sweeps as tables and leave the shape of the curve to the reader.
5
- * Drawing it is the whole point of this archetype, so everything here is derived
6
- * from the data — the scale especially. A hardcoded axis is how a chart ends up
7
- * asserting something the numbers do not.
8
- */
9
1
  import type { Emitter } from "../kit.js";
10
2
  export interface Scale {
11
3
  min: number;
@@ -43,6 +43,8 @@ type Loop = {
43
43
  export interface PipeLayout {
44
44
  /** Label type size. Never below `MIN_FONT`, whatever the labels cost. */
45
45
  size: number;
46
+ /** A stage note's type size: `NOTE` in classic, grown with the label in v2. */
47
+ note: number;
46
48
  /**
47
49
  * Position and extent along the flow axis: `x`/`w` read as x/width across a
48
50
  * row, and as y/height down a column. One field rather than two so the emitter
@@ -69,6 +71,18 @@ export interface PipeLayout {
69
71
  loopW: number;
70
72
  svgH: number;
71
73
  }
74
+ /**
75
+ * v2: how far the diagram may grow, and into what. Absent is v0.8.0 exactly.
76
+ *
77
+ * `budget` is the height the SVG may take (the region less the wrapper's margin
78
+ * and the note); `region` is the whole body region, which bounds a box's height
79
+ * at `GROWN_BOX_H` of it — the plan's `MAX_BOX_H = 0.8·region.h`. The label's
80
+ * preferred size grows by `GROWTH` and `fitBoxes` still shrinks it to the width.
81
+ */
82
+ export interface PipeGrow {
83
+ budget: number;
84
+ region: number;
85
+ }
72
86
  /**
73
87
  * All the geometry, with no SVG in sight.
74
88
  *
@@ -76,6 +90,6 @@ export interface PipeLayout {
76
90
  * archetype must never get wrong, and asserting it against numbers beats
77
91
  * asserting it against a rendered string.
78
92
  */
79
- export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop, face?: Face): PipeLayout;
93
+ export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop, face?: Face, grow?: PipeGrow): PipeLayout;
80
94
  export declare const pipeline: Emitter<"pipeline">;
81
95
  export {};
@@ -109,6 +109,6 @@ export interface StackLayout {
109
109
  * composition changes rather than the type: the note moves beside its label, on
110
110
  * the same line, right-aligned against the spine.
111
111
  */
112
- export declare function stackLayout(p: Params, format: Format, face?: Face): StackLayout;
112
+ export declare function stackLayout(p: Params, format: Format, face?: Face, grow?: boolean): StackLayout;
113
113
  export declare const stack: Emitter<"stack">;
114
114
  export {};
@@ -11,9 +11,12 @@
11
11
  * timeline, wraps whatever the emitter returns, and closes the document.
12
12
  */
13
13
  import type { z } from "zod";
14
+ import { type BespokeMap } from "../bespoke/scene.js";
15
+ import { type Direction } from "../plan/direct.js";
14
16
  import { type Cut } from "../plan/select.js";
15
- import type { Format, NarrationCanvas, Source, Storyboard, segmentSchema } from "../types.js";
16
- import { type Scene } from "./kit.js";
17
+ import type { Beat, Design, Format, NarrationCanvas, Source, Storyboard, segmentSchema } from "../types.js";
18
+ import type { FitManifest } from "./fit.js";
19
+ import { type EmitContext, type Scene } from "./kit.js";
17
20
  /** A built deck. `page` is present only for navigable formats. */
18
21
  export interface Deck {
19
22
  composition: string;
@@ -27,6 +30,17 @@ export interface Deck {
27
30
  * PASS. `build` prints every casualty's reason; see src/cli.ts.
28
31
  */
29
32
  cut: Cut;
33
+ /**
34
+ * v2 only: each drawn scene's predicted fill, for `fit.json`. Absent on a
35
+ * classic build, which writes no manifest and so is not graded for fill.
36
+ */
37
+ fit?: FitManifest;
38
+ /**
39
+ * `--design v2` only: the look the Director chose for each kept beat, and the
40
+ * deck's sameness numbers. `build` writes it to `out/look.json`. Absent on a
41
+ * classic build.
42
+ */
43
+ looks?: Direction;
30
44
  }
31
45
  type Segment = z.infer<typeof segmentSchema>;
32
46
  /**
@@ -57,6 +71,15 @@ export interface DeckNarration {
57
71
  export interface DeckOptions {
58
72
  /** Overrides `storyboard.theme`. Any name in the registry. */
59
73
  theme?: string;
74
+ /**
75
+ * Overrides `storyboard.design`. Absent or `classic` emits v0.8.0's bytes.
76
+ * `v2` marks deck.html for the v2 player (`markV2`), lets the archetypes grow
77
+ * into their regions and fills `Deck.fit` (see `../emit/fit.ts`), and runs
78
+ * the Director (src/plan/direct.ts) over the kept beats so each is drawn in
79
+ * the look it chose (src/emit/look.ts), and turns on the motion grammar in
80
+ * `motion.ts`: varied entrances, seams and emphasis during narration.
81
+ */
82
+ design?: Design;
60
83
  /** Multiplies every duration, hold, and beat length. 1 leaves bytes untouched. */
61
84
  speed?: number;
62
85
  narration?: DeckNarration;
@@ -107,6 +130,12 @@ export interface DeckOptions {
107
130
  * (`vendorInter` in src/build/files.ts), for the same reason.
108
131
  */
109
132
  fontCss?: string;
133
+ /**
134
+ * `--design v2 --bespoke`: beats drawn by a generated scene, keyed by beat id
135
+ * (src/bespoke/). Absent on every other build, which is what keeps them
136
+ * byte-identical. A deck that carries one also carries `BESPOKE_CSP`.
137
+ */
138
+ bespoke?: BespokeMap;
110
139
  }
111
140
  /**
112
141
  * Both artifacts from one layout pass.
@@ -118,7 +147,28 @@ export interface DeckOptions {
118
147
  * and because HyperFrames' own navigation is dead at 0.7.71/0.7.72 and still
119
148
  * dead at 0.8.27 as at 0.7.90 (EXPERIMENT-003), so the presented one has to be ours.
120
149
  */
121
- export declare function emitDeck(storyboard: Storyboard, source: Source, format: Format, runtimeJs: string, opts?: DeckOptions): Deck;
150
+ export declare function emitDeck(storyboard: Storyboard, source: Source, format: Format, runtimeJs: string, options?: DeckOptions): Deck;
151
+ /**
152
+ * How a v2 deck breaks and tracks CJK text. Empty for any other language.
153
+ *
154
+ * KOREAN BREAKS BETWEEN WORDS. Browsers break Hangul between any two syllables
155
+ * by default, and v2's narrow rail headlines and grown text made that happen on
156
+ * most rail slides: "생성 조건/인", "가장 높은 종/합 점수", "서로 다/른" (review,
157
+ * 2026-10-08: 8 of 8 rail headlines on one deck, against 1 break in total on
158
+ * the classic layouts). `keep-all` breaks only at spaces — which is also how
159
+ * `wrap` in svg.ts has always MEASURED Korean, so the layout's line counts and
160
+ * the browser's now agree — and `break-word` still breaks a single word wider
161
+ * than its line rather than letting it overflow, as `wrap` does.
162
+ *
163
+ * JAPANESE AND CHINESE keep breaking between characters, under the strict
164
+ * kinsoku rules, so a line never opens on small kana or closing punctuation.
165
+ *
166
+ * NO TRACKING ON A CJK EYEBROW. Letter-spacing made for Latin capitals pulls
167
+ * Hangul and kanji apart until "생성 결과의 실패" reads "생 성 결 과 의 실 패".
168
+ * Tracking only ever made the eyebrow wider, so dropping it never adds a line
169
+ * the chrome was not charged for.
170
+ */
171
+ export declare function cjkTypesetCss(lang: string): string;
122
172
  /**
123
173
  * The beats this format actually draws, and why the rest are not there.
124
174
  *
@@ -219,7 +269,35 @@ export declare function speechPlan(open: number, holds: readonly number[], segme
219
269
  starts: number[];
220
270
  end: number;
221
271
  };
272
+ /**
273
+ * Where a bespoke scene's stops go and when its sentences are spoken, worked out
274
+ * with the same functions `layout` stages with — so the cue times the model is
275
+ * given are the times the deck will have. See `bespokeHolds` for why the voice
276
+ * does not move.
277
+ *
278
+ * `ctx` must not carry the bespoke map: this stages the ARCHETYPE first.
279
+ */
280
+ export declare function bespokeStaging(beat: Beat, ctx: EmitContext, segments: readonly Segment[], speed: number): {
281
+ holds: number[];
282
+ open: number;
283
+ };
222
284
  export declare function emitComposition(storyboard: Storyboard, source: Source, format: Format, opts?: DeckOptions): string;
285
+ /**
286
+ * The policy a composition carrying a generated scene runs under.
287
+ *
288
+ * The static walk in src/bespoke/contract.ts refuses every NAME that reaches the
289
+ * network, eval, storage or navigation; this refuses the BEHAVIOUR, so a scene
290
+ * that spelled its way past the walk still gets nothing. No `connect-src`, no
291
+ * frames, no plugins, and no `unsafe-eval` — `Function("…")` assembled from
292
+ * fragments throws here. `'unsafe-inline'` stays because every scene's timeline
293
+ * is an inline script, as it is in every deck; what it buys an attacker is
294
+ * nothing a scene could not already write. Local files only (`'self'`), which is
295
+ * every vendored script, stylesheet, font and figure a deck ships.
296
+ *
297
+ * Only on a deck with a bespoke scene: a `<meta>` in every head would move the
298
+ * bytes of every deck ever built.
299
+ */
300
+ export declare const BESPOKE_CSP: string;
223
301
  /** Shipped beside the deck so a built artifact needs no network to navigate. */
224
302
  export declare const PLAYER_FILE = "hyperframes-player.global.js";
225
303
  /**
@@ -228,4 +306,6 @@ export declare const PLAYER_FILE = "hyperframes-player.global.js";
228
306
  * determinism scan above all — do not apply to it.
229
307
  */
230
308
  export declare const DECK_PAGE = "deck.html";
309
+ /** A literal `</script>` inside the bundle would close the tag that carries it. */
310
+ export declare function closeSafe(code: string): string;
231
311
  export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * GENERATED by `node scripts/measure-faces.mjs --write`. Do not edit by hand.
3
+ *
4
+ * Advance tables for the Latin faces the v2 style packs set, measured in Chrome
5
+ * from the vendored @fontsource-variable packages — the files a built deck
6
+ * ships. Same recipe as Inter's `ADVANCE` in svg.ts: isolated advances with
7
+ * kerning off, max over 400/500/600/700 after dividing by that weight's factor,
8
+ * rounded up. `test/svg.test.ts` holds the result against drawn widths read
9
+ * out of the same browser. Re-run the script, never re-type a number.
10
+ */
11
+ export interface FaceMetrics {
12
+ /** The CSS family a deck names it by — what `vendorFace` declares. */
13
+ family: string;
14
+ /** Mean advance ratio against 400, per weight the type scales use. */
15
+ weight: Readonly<Record<400 | 500 | 600 | 700, number>>;
16
+ tabularFigure: number;
17
+ tabularSeparator: number;
18
+ kernSlack: number;
19
+ advance: Readonly<Record<string, number>>;
20
+ }
21
+ export type MeasuredFace = "source-serif-4" | "space-grotesk" | "ibm-plex-sans";
22
+ export declare const FACE_METRICS: Readonly<Record<MeasuredFace, FaceMetrics>>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * THE FIT ENGINE'S VOCABULARY — what "full" means, in one place, for both the
3
+ * emitter that predicts it and the gate that measures it.
4
+ *
5
+ * WHY THIS EXISTS. Every archetype's solver could only SHRINK: `BAR_MAX = 96`,
6
+ * `MAX_BOX_H = 340`, a 50px claim, panels capped at their content plus a fifth.
7
+ * Whatever the region had left over, `.scene`'s `justify-content: center` turned
8
+ * into dead air above and below the slide. Measured on v0.8.0 decks (redesign
9
+ * plan 2026-10-07 §1): bar-compare and pipeline fill 51% of the canvas, and
10
+ * about 39% of final holds are hollow. Under `design: "v2"` an archetype GROWS
11
+ * into its region instead — type, marks and gaps, each up to its own cap — and
12
+ * reports how full it predicts the result to be.
13
+ *
14
+ * THE MEASURE: MAIN-AXIS FILL. The body region is the content box below the
15
+ * chrome: `contentW` wide, `contentH - chromeHeight` tall (`bodyRegion`). The
16
+ * main axis is the VERTICAL one, because that is the axis `.scene` stacks the
17
+ * chrome and the body along and therefore the axis every pixel of centring slack
18
+ * lands on — width is already spent by every landscape archetype (bar rails, a
19
+ * pipeline row, split columns all run the full measure), which is exactly why a
20
+ * width-or-height maximum called those slides FULL while they read as a thin
21
+ * band through the middle. So:
22
+ *
23
+ * fill = (painted extent of the body, top to bottom) / (region height)
24
+ *
25
+ * The body's own top margin is not ink, so a body that uses every pixel it is
26
+ * given reads a little under 1. `verify/fill.ts` measures the same ratio on the
27
+ * rendered frame — pixels, not DOM boxes — and `fit.json` carries this file's
28
+ * prediction beside it, so the model is checked against the browser on every v2
29
+ * build rather than trusted (AGENTS.md: a gate passing is not evidence).
30
+ *
31
+ * THE BANDS are ResearchStudio-Reel's (2607.04438 §A2), with the action the
32
+ * plan assigns each: EMPTY < 0.70 (grow, or the next variant), SPARSE < 0.90
33
+ * (grow to the caps, spread gaps), FULL ≤ 1.00 (leave it), SPILLAGE ≤ 1.10
34
+ * (shrink gaps, then type, toward 40), OVERFLOW beyond (refuse, as today).
35
+ *
36
+ * PURE ARITHMETIC, AND IMPORTS NOTHING FROM AN ARCHETYPE. `title.ts` owns the
37
+ * chrome's type scale and every archetype imports it; a helper here that
38
+ * imported it back would be a cycle. Callers pass the region they computed with
39
+ * `bodyBudget`.
40
+ */
41
+ import type { Design } from "../types.js";
42
+ /** Below this the frame reads as hollow: a band through the middle of the slide. */
43
+ export declare const EMPTY_BELOW = 0.7;
44
+ /** At or above this the region is used; growth stops being worth its cost. */
45
+ export declare const FULL_AT = 0.9;
46
+ /** Past the region's bottom edge, into the padding. */
47
+ export declare const SPILL_ABOVE = 1;
48
+ /** Past this a layout is not tight, it is wrong. */
49
+ export declare const OVERFLOW_ABOVE = 1.1;
50
+ export type FillBand = "empty" | "sparse" | "full" | "spillage" | "overflow";
51
+ /** Which of the five bands a main-axis fill falls in. */
52
+ export declare function fillBand(fill: number): FillBand;
53
+ /**
54
+ * What an archetype predicts about its own final hold, in reference px.
55
+ *
56
+ * In memory only — `Scene.fit`, like `Scene.parts`, is never serialised into the
57
+ * composition, so a classic build cannot move a byte because of it. The shell
58
+ * collects it into `fit.json` for v2 builds (`FIT_FILE`).
59
+ */
60
+ export interface Fit {
61
+ /** `ink / region`, rounded to three places (invariant 10's precision). */
62
+ fill: number;
63
+ /** Height of the body region: content box below the chrome. */
64
+ region: number;
65
+ /** Predicted painted extent of the body, top of its first mark to bottom of its last. */
66
+ ink: number;
67
+ }
68
+ /** A prediction, rounded once here so two builds of one storyboard agree to the byte. */
69
+ export declare function fitOf(ink: number, region: number): Fit;
70
+ /**
71
+ * The growth factor the plan names for type and marks: `min(constant × G, …)`.
72
+ *
73
+ * 1.6, from density §2.1 — a 50px claim may set at 72 at most (50 × 1.44 is
74
+ * under it), a 96px bar at 153, a 46px bar label at 73. Each archetype applies
75
+ * it to its OWN caps and then still has to fit the region; G is a ceiling on
76
+ * how far a short beat may be blown up, not a target.
77
+ */
78
+ export declare const GROWTH = 1.6;
79
+ /**
80
+ * The largest scale in `[lo, hi]` whose layout height fits `budget`.
81
+ *
82
+ * `height(s)` must be monotone non-decreasing in `s`, which every layout here is:
83
+ * bigger type wraps to at least as many lines and each line is taller. Bisection
84
+ * to 1/128 of a step, then FLOORED onto that grid, so the answer is a short
85
+ * decimal that prints identically on every build (invariant 10) and never
86
+ * exceeds the budget it was solved for. `lo` is returned when even `lo` does not
87
+ * fit — the caller's own overflow refusal is what handles that case, exactly as
88
+ * it did before growth existed.
89
+ */
90
+ export declare function growToFit(height: (scale: number) => number, budget: number, lo?: number, hi?: number): number;
91
+ /**
92
+ * The share of its real measure v2 sets HTML text against when it predicts a
93
+ * line count it is about to GROW into.
94
+ *
95
+ * `wrap` is a width table, and the browser is a line breaker with rules the
96
+ * table does not know: kinsoku keeps `、` and `。` off the start of a line, so a
97
+ * Japanese sentence the table sets on three lines of eleven sets on four of ten.
98
+ * Classic got away with it because nothing it drew was sized to the last line —
99
+ * a 50px claim in a 560px column had air. Growth removes the air on purpose, so
100
+ * a miss of one line becomes a caption pushed 105px through the bottom of the
101
+ * slide: MEASURED on six ja/zh claim-figure and callout beats at 1.08–1.11 fill
102
+ * before this existed. SVG text is exempt — `text()` places its own lines from
103
+ * the same `wrap`, so there the prediction IS the drawing.
104
+ */
105
+ export declare const MEASURE_SLACK = 0.92;
106
+ /** Whether this emit is the v2 vocabulary. Absent means classic: every test context, every older caller. */
107
+ export declare function isV2(ctx: {
108
+ design?: Design;
109
+ }): boolean;
110
+ /**
111
+ * The manifest a v2 build writes beside `index.html`, and `verify` reads back.
112
+ *
113
+ * Its PRESENCE is what marks a deck as v2 to the gate: `verify` runs on a deck
114
+ * directory, and the only trustworthy statement about how that deck was built is
115
+ * the one the build wrote into it — the same reasoning `readReserve` gives for
116
+ * `timing.json`.
117
+ */
118
+ export declare const FIT_FILE = "fit.json";
119
+ export interface FitManifest {
120
+ design: "v2";
121
+ scenes: {
122
+ id: string;
123
+ beat: string;
124
+ archetype: string;
125
+ fit?: Fit;
126
+ }[];
127
+ }
@@ -6,7 +6,10 @@
6
6
  * and nothing else. Adding a domain means adding an emitter — the shell never
7
7
  * learns what a camera frustum or an orderbook is.
8
8
  */
9
- import type { Archetype, BeatOf, Format, Source } from "../types.js";
9
+ import type { BespokeMap } from "../bespoke/scene.js";
10
+ import type { Archetype, BeatOf, Design, Format, Source } from "../types.js";
11
+ import type { Fit } from "./fit.js";
12
+ import type { Look } from "./look.js";
10
13
  /**
11
14
  * `.scene`'s padding, in reference px. `contentW`/`contentH` are what is left.
12
15
  *
@@ -158,6 +161,35 @@ export interface Theme {
158
161
  d: string;
159
162
  };
160
163
  fontStack: string;
164
+ /**
165
+ * The chrome's stack — eyebrow, headline, the title slide's headline — when a
166
+ * v2 style pack sets it in a face of its own. Absent means `fontStack`, which
167
+ * is every classic theme. Which spec it belongs to is read off `fontStack`
168
+ * (`faceOf`), so the two are declared together by the pack (`src/emit/type.ts`).
169
+ */
170
+ displayStack?: string;
171
+ /**
172
+ * A v2 style pack's component forms: how it draws the parts every deck has,
173
+ * so two packs differ in FORM and not only in colour (review, 2026-10-08:
174
+ * "one template in four colours"). Absent on every classic theme, which is
175
+ * what keeps a classic build's bytes v0.8.0's.
176
+ */
177
+ forms?: PackForms;
178
+ }
179
+ /** How a pack marks a list item. `tick` is the classic coloured bar. */
180
+ export type ListForm = "tick" | "number" | "dot" | "card" | "rule";
181
+ export interface PackForms {
182
+ list?: ListForm;
183
+ /**
184
+ * Which looks this pack leans towards, as score bonuses the Director adds
185
+ * (src/plan/direct.ts): by placement, and by `archetype:variant`. So the
186
+ * same storyboard opens differently under two packs instead of every deck's
187
+ * second slide being the same two-column comparison.
188
+ */
189
+ affinity?: {
190
+ placement?: Partial<Record<"top" | "rail" | "foot", number>>;
191
+ variant?: Readonly<Record<string, number>>;
192
+ };
161
193
  }
162
194
  export interface EmitContext {
163
195
  source: Source;
@@ -202,6 +234,32 @@ export interface EmitContext {
202
234
  * pass exists to measure holds, and every scene it emits is thrown away.
203
235
  */
204
236
  start: number;
237
+ /**
238
+ * `prefs.design`. ABSENT MEANS CLASSIC, and that is the contract that keeps
239
+ * v0.8.0's bytes: every hand-built test context, `narrate`, `refs` and
240
+ * `timing` omit it, and an emitter reads it only through `isV2` in `./fit.ts`.
241
+ * Those last three only count stops, and stop counts are identical under both
242
+ * designs by construction (test/fit.test.ts holds it), so they do not need it.
243
+ */
244
+ design?: Design;
245
+ /**
246
+ * `--design v2` only: the arrangement the Director chose for this beat — a
247
+ * body variant and where the chrome sits. See src/emit/look.ts.
248
+ *
249
+ * ABSENT ON EVERY CLASSIC BUILD, and absent means the classic scene byte for
250
+ * byte. The measuring passes (`planCut`, `narrate`, `timing`, `refs`) never set
251
+ * it either, which is safe because a look may change geometry and never time:
252
+ * the Director rejects any look whose holds or chrome landing differ.
253
+ */
254
+ look?: Look;
255
+ /**
256
+ * `--design v2 --bespoke` only: the beats drawn by a generated scene instead
257
+ * of their archetype, keyed by beat id (src/bespoke/scene.ts). `emitScene`
258
+ * reads it, so every caller that stages a beat — `planCut`, `layout`, the
259
+ * timing manifest — sees the same scene. ABSENT ON EVERY OTHER BUILD, and
260
+ * absent means the archetype, byte for byte.
261
+ */
262
+ bespoke?: BespokeMap;
205
263
  }
206
264
  /**
207
265
  * A value in a GSAP vars payload that is JavaScript rather than data.
@@ -317,6 +375,14 @@ export interface Scene {
317
375
  * seeking that deck navigation performs.
318
376
  */
319
377
  tl: Tween[];
378
+ /**
379
+ * A bespoke scene's generated script, and nothing else's. Run inside this
380
+ * scene's timeline closure as `function (tl, root) { "use strict"; … }`, after
381
+ * `tl` exists and before the shell's own tweens are appended to it. Checked
382
+ * by src/bespoke/contract.ts before it can get here; an archetype never sets
383
+ * it, so every archetype scene is the bytes it was.
384
+ */
385
+ script?: string;
320
386
  /** Statements run as the document parses, before anything else, e.g.
321
387
  * `katex.render(...)`. Anything that CHANGES layout belongs here, so that a
322
388
  * `measure` below reads the finished document. */
@@ -389,6 +455,29 @@ export interface Scene {
389
455
  holds: number[];
390
456
  /** CSS this archetype needs. Deduplicated by the shell, emitted once. */
391
457
  css?: string;
458
+ /**
459
+ * The archetype's own prediction of how full its final hold is — v2 only.
460
+ * In memory, never serialised into the composition (like `parts`); the shell
461
+ * collects it into `fit.json` and `verify/fill.ts` holds it against the
462
+ * browser. See `./fit.ts` for what the number means.
463
+ */
464
+ fit?: Fit;
465
+ /**
466
+ * How much of its body box this scene's body fills along the axis it grows
467
+ * on, 0–1: the drawn height over the height it was given. Optional, in-memory
468
+ * only — never serialised, so it moves no byte. The `--design v2` Director
469
+ * prefers the look that fills more (src/plan/direct.ts); an emitter that does
470
+ * not report it is scored as neither full nor hollow.
471
+ */
472
+ fill?: number;
473
+ /**
474
+ * The area, in reference px², at which this scene paints the paper's own
475
+ * figure — the image, not the white plate around it. In-memory only. The
476
+ * Director refuses a look that shrinks it well below what the classic look
477
+ * gives (`FIGURE_FLOOR` in src/plan/direct.ts): a figure is often the most
478
+ * informative thing in the deck, and v2 was trading it for a rail headline.
479
+ */
480
+ figureArea?: number;
392
481
  }
393
482
  export type Emitter<A extends Archetype> = (beat: BeatOf<A>, ctx: EmitContext) => Scene;
394
483
  /** Escape text destined for HTML text content or a quoted attribute. */
@@ -496,4 +585,34 @@ export declare function settle(target: string, at: number, seconds?: number, ori
496
585
  * transform on an inline box is a no-op, and the rise would render as a plain
497
586
  * fade with every gate green.
498
587
  */
499
- export declare function words(text: string, cls?: string): string;
588
+ export declare function words(text: string, cls?: string, opts?: {
589
+ unspaced?: boolean;
590
+ }): string;
591
+ /**
592
+ * A word-by-word stagger that lands by the stop, however many atoms there are.
593
+ *
594
+ * The rise is timed against a fixed hold, so its last atom must start in the
595
+ * window eight words used to: a CJK claim split per character is 30-60 atoms,
596
+ * and at a flat 0.05s the second half of the sentence would still be invisible
597
+ * when the deck stopped on it. Past `full` atoms the spacing shrinks so the
598
+ * spread stays `base · (full − 1)`. v2 only; classic keeps its flat stagger.
599
+ */
600
+ export declare function staggerFor(count: number, base: number, full?: number): number;
601
+ /**
602
+ * The units `words(…, { unspaced: true })` sets, each an `inline-block` that
603
+ * rises on its own and is never broken across a line.
604
+ *
605
+ * WHY A CJK RUN IS NOT ONE ATOM. Split on whitespace alone, "每个查询只选取最相关的"
606
+ * is a single inline-block: it cannot wrap inside itself, so it jumps whole to
607
+ * its own line, and a mixed claim came out "每个查询只选取最 / 相关的 / k 个历史帧;不同 /
608
+ * token / …" (review, 2026-10-08, zh s10). Han and kana break between any two
609
+ * characters, so here each character is its own atom, Latin inside the run
610
+ * ("token", "k") stays one, and kinsoku punctuation rides on its neighbour.
611
+ * Hangul is spaced like Latin and stays word by word.
612
+ *
613
+ * `spaced`: whether whitespace separated this atom from the one before it.
614
+ */
615
+ export declare function wordAtoms(text: string): {
616
+ text: string;
617
+ spaced: boolean;
618
+ }[];