@jokerized/decksmith 0.2.0 → 0.3.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.
@@ -0,0 +1,2 @@
1
+ import type { Emitter } from "../kit.js";
2
+ export declare const equationMorph: Emitter<"equation-morph">;
@@ -1,2 +1,64 @@
1
- import type { Emitter } from "../kit.js";
1
+ /**
2
+ * An equation, walked one symbol at a time.
3
+ *
4
+ * This is the most valuable thing the vocabulary does: the reader is never shown
5
+ * a wall of TeX and left to find the symbol being discussed. Each term is wrapped
6
+ * in `\htmlClass{term t-<tone>}{...}` so KaTeX emits a real element for it, which
7
+ * GSAP then tints and swells in step with its legend row.
8
+ */
9
+ import type { Term } from "../../types.js";
10
+ import type { Emitter, Theme } from "../kit.js";
11
+ export declare const OPTS = "{ displayMode: true, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
12
+ export declare const INLINE_OPTS = "{ displayMode: false, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
13
+ /** Where `term` sits in `tex`, comparing normal forms. Null when it is absent. */
14
+ export declare function locate(tex: string, term: string): {
15
+ start: number;
16
+ end: number;
17
+ } | null;
18
+ /**
19
+ * Wrap each term where it first occurs, and report which ones were wrapped.
20
+ *
21
+ * Segments are tracked as raw/wrapped so a later term cannot match inside an
22
+ * earlier term's `\htmlClass{...}` and produce nested markup that highlights the
23
+ * wrong span.
24
+ *
25
+ * A term that cannot be found is DROPPED rather than thrown on, and the caller
26
+ * drops its legend row with it — the two go together, which is what keeps this
27
+ * honest. Silently highlighting nothing is the failure this archetype exists to
28
+ * avoid, and a legend line pointing at an unhighlighted symbol is that failure;
29
+ * a shorter legend is not. Losing an entire deck to one mis-spelled term is a
30
+ * worse answer than either. If NOTHING matches, the beat has no work to do and
31
+ * that is still an error.
32
+ */
33
+ export declare function wrapTerms(tex: string, terms: Term[], beatId: string,
34
+ /** The class the wrapper carries; the morph adds its key to the walk's tint. */
35
+ cls?: (t: Term) => string): {
36
+ tex: string;
37
+ used: Term[];
38
+ missing: Term[];
39
+ };
40
+ /**
41
+ * Display equations want to live between 68px and 108px.
42
+ *
43
+ * The equation is the whole argument of this archetype and it was the smallest
44
+ * thing on the slide — 72px of TeX centred in a 1700px box, measured at 46% fill
45
+ * with a 1920x316 band under it. It is sized off the source length rather than
46
+ * off `textWidth` because TeX is not the string that gets set: `\mathcal{W}(F)`
47
+ * is fourteen characters and three glyphs.
48
+ *
49
+ * This is the *wanted* size, not the final one. The old comment here claimed
50
+ * "KaTeX's own `\displaystyle` box will shrink to the container if the estimate
51
+ * runs wide". It does not — it overflows, silently, because the composition is
52
+ * the size it says it is and no gate reads past the canvas edge. At 9:16 that
53
+ * truncated `X = \mathcal{W}(F)` to "X =" and then left the legend explaining a
54
+ * symbol the viewer could not see, which is worse than a clip: the slide
55
+ * asserted something false. `statements` and the `size` cap below are what make
56
+ * the claim true.
57
+ */
58
+ export declare function equationSize(tex: string): number;
59
+ /** Estimated rendered width of a display, in ems. */
60
+ export declare function texUnits(tex: string): number;
61
+ /** One row per term: a chip KaTeX fills in `setup`, and the label. Shared with the morph. */
62
+ export declare function legendRows(sid: string, terms: Term[], theme: Theme): string;
63
+ export declare function legendCss(theme: Theme): string;
2
64
  export declare const equationWalk: Emitter<"equation-walk">;
@@ -12,7 +12,7 @@ export declare const emitters: {
12
12
  };
13
13
  /**
14
14
  * Dispatch a beat to its emitter. The cast is the one place the pairing is taken
15
- * on trust: `emitters[beat.archetype]` is a union of twelve emitters and TypeScript
15
+ * on trust: `emitters[beat.archetype]` is a union of thirteen emitters and TypeScript
16
16
  * will not narrow the key and the beat together. The table above already proves
17
17
  * every archetype has exactly one emitter of the right shape.
18
18
  */
@@ -25,7 +25,16 @@ import type { BeatOf, Format } from "../../types.js";
25
25
  import type { Emitter } from "../kit.js";
26
26
  import { type Face } from "../svg.js";
27
27
  type Params = BeatOf<"stack">["params"];
28
+ /** Where the numerals' right edge lands once the type floor has moved. */
29
+ export declare function numSpine(floor: number): number;
28
30
  export interface StackLayout {
31
+ /**
32
+ * The type floor this layout solved against, which is `MIN_FONT` for a flat
33
+ * beat and larger for a tilted one. The emitter reads it back so the notes it
34
+ * draws are set at the same floor the solver reserved room for — they were
35
+ * pinned to `MIN_FONT` directly, which under a tilt drew them below it.
36
+ */
37
+ floor: number;
29
38
  /**
30
39
  * False when the rise the canvas allows is shorter than the tallest label
31
40
  * block — i.e. adjacent labels would overlap. `stackLayout` re-composes rather
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Tilting a plane, and paying for it in type size.
3
+ *
4
+ * Gap 2 wants depth as exposition — a diagram tilting to reveal a layer. The
5
+ * expensive half is not the transform. It is that perspective shrinks the type on
6
+ * the far half of the plane, and invariant 5 is about what the AUDIENCE sees, so
7
+ * a tilt that leaves a 40px label drawing 31px has broken the floor whatever the
8
+ * source says. `src/verify/apparent.ts` is the gate that catches it; this module
9
+ * is how an archetype avoids tripping it, by declaring bigger type up front.
10
+ *
11
+ * WHAT IS AND IS NOT SUPPORTED HERE. One transform on one plane — the whole
12
+ * scene's content tilts as a unit. NOT per-element depth: measured in Chrome,
13
+ * `translateZ` on an SVG `<g>` under a `preserve-3d` ancestor is ignored
14
+ * completely (z=0, z=+200 and z=-200 paint the identical rect, while an HTML
15
+ * sibling in the same test moves and scales correctly). An exploding stack needs
16
+ * sibling HTML planes and is a later slice; a tilting plate is this one.
17
+ *
18
+ * THE MODEL. For a plane rotated by `t` about its horizontal centre line under
19
+ * CSS `perspective: d`, a point `dy` below the plane's centre is drawn at
20
+ *
21
+ * scale(dy) = cos(t) * (d / (d - dy * sin(t)))^2
22
+ *
23
+ * — `cos(t)` for the foreshortening and the square for the perspective divide
24
+ * applying to both the glyph's height and its distance from the eye. Fitted
25
+ * against Chrome at `rotateX(30deg)`, `perspective: 1400px`:
26
+ *
27
+ * dy = -340 (near the top) model 0.6886 Chrome 0.6820 +0.97%
28
+ * dy = +360 (near the bottom) model 1.1404 Chrome 1.1260 +1.28%
29
+ *
30
+ * The model runs about 1% OPTIMISTIC, so `worstScale` rounds its answer down by
31
+ * 2% before anyone divides by it. Erring large costs a couple of type px; erring
32
+ * small ships a slide under the floor, which is the whole failure this exists to
33
+ * prevent.
34
+ */
35
+ /** How a scene's plane is tilted. Degrees, and CSS `perspective` in px. */
36
+ export interface Pose {
37
+ /** Rotation about the horizontal centre line. Positive tips the top away. */
38
+ rotateX: number;
39
+ /** CSS `perspective`, in reference px. Smaller is a stronger effect. */
40
+ perspective: number;
41
+ }
42
+ /**
43
+ * The default a depth archetype gets when it asks for one.
44
+ *
45
+ * 12 degrees, not the 30 the spike reached for. The table in `worstScale`'s note
46
+ * is why: at 30 degrees a 40px audience floor needs 67px declared, which is more
47
+ * than a headline can spend and still fit its own line. At 12 it needs 48.7px,
48
+ * which archetypes can and do pay. Depth here is a lean, not a dive.
49
+ */
50
+ export declare const DEFAULT_POSE: Pose;
51
+ /** Apparent scale at `dy` reference px below the plane's centre. */
52
+ export declare function scaleAt(pose: Pose, dy: number): number;
53
+ /**
54
+ * The smallest scale anywhere on a `height`-tall plane, already discounted.
55
+ *
56
+ * The smallest scale is at the TOP edge, furthest from the eye. The BOTTOM edge
57
+ * is the one that can swing through the eye entirely, and when it does the plane
58
+ * is not being tilted any more — it is being turned inside out — so the pose is
59
+ * refused rather than priced. Checking only the top would have returned a
60
+ * perfectly reasonable-looking floor for a pose that cannot be drawn.
61
+ *
62
+ * What it buys, at `perspective: 1400` on a 1080 canvas, AFTER the 2% discount:
63
+ *
64
+ * rotateX(6) 0.901 -> a 40px floor needs 44.4px declared
65
+ * rotateX(12) 0.821 -> 48.7px
66
+ * rotateX(18) 0.744 -> 53.8px
67
+ * rotateX(24) 0.669 -> 59.8px
68
+ * rotateX(30) 0.597 -> 67.0px
69
+ *
70
+ * That table is the tilt budget. Depth in this project is bounded by the type
71
+ * floor rather than by taste, and the bound is tighter than it looks.
72
+ */
73
+ export declare function worstScale(pose: Pose, height: number): number;
74
+ /**
75
+ * The type floor an archetype must solve against to LAND on `floor` once tilted.
76
+ *
77
+ * Archetypes already solve against a floor — `stack` passes `MIN_FONT` into its
78
+ * own fit — so depth costs them one substitution rather than a second solver.
79
+ */
80
+ export declare function tiltedFloor(pose: Pose, height: number, floor: number): number;
81
+ /**
82
+ * The CSS a tilted scene needs, scoped to its own id (invariant 3).
83
+ *
84
+ * `perspective` sits on the scene and the rotation on ONE part of it, named by
85
+ * `part`, because tilting a whole scene tilts its chrome too. Built that way
86
+ * first and it was obviously wrong the moment a frame was opened: the headline
87
+ * came out sheared, leaning like a mistake rather than a design, and the stack's
88
+ * probe marker — a straight rule with a dot under it — bent into a hook. The
89
+ * diagram is what gains by leaning; the words are what the audience reads.
90
+ *
91
+ * Nothing here animates: a pose is a static property of the beat, and a tilt that
92
+ * moved would put every label at a different apparent size on every frame while
93
+ * `apparent` only measures at the declared stops.
94
+ */
95
+ export declare function depthCss(sid: string, pose: Pose, part: string): string;
@@ -285,6 +285,13 @@ export interface Scene {
285
285
  * values.
286
286
  */
287
287
  measure?: string[];
288
+ /**
289
+ * Vendored runtimes this scene's tweens need registered before its script
290
+ * runs — `"dsMorph"` for the equation morph. The shell loads a runtime only
291
+ * when some scene names it, so a deck that names none is byte-for-byte what
292
+ * it was.
293
+ */
294
+ plugins?: readonly string[];
288
295
  /**
289
296
  * Hold points in seconds from the scene's start — where a presenter should
290
297
  * pause. The shell converts these to absolute island fragment times.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The keyed equation morph — TransformMatchingTex, for the DOM.
3
+ *
4
+ * Two KaTeX renderings in, one seek-evaluable plan out. `build()` runs once in
5
+ * the browser, inside the ready gate (Seam B), and bakes every number; the
6
+ * plan is then driven by a GSAP PLUGIN — `render(ratio)` is part of being
7
+ * seeked, so it fires under `suppressEvents` where an `onUpdate` would not
8
+ * (invariant 11). Nothing here runs on a callback.
9
+ *
10
+ * Four phases, kept separate on purpose:
11
+ * 1. LIFT — pull every ink-bearing leaf out of KaTeX's nested boxes into a
12
+ * flat absolutely-positioned overlay. Removes clipping, vlist
13
+ * stacking and transform-on-inline entirely.
14
+ * 2. GROUP — collect leaves into the units that move as one body. An author
15
+ * key makes a group; an unkeyed leaf is its own group.
16
+ * 3. MATCH — decide which group of A becomes which group of B.
17
+ * 4. PLAN — a similarity transform per group, as per-leaf segments.
18
+ *
19
+ * `evaluate()` turns a plan and a progress into styles. It is a pure function
20
+ * of its arguments — two seeks to one time write the same strings — which is
21
+ * what makes the render byte-identical across browser processes, measured on
22
+ * the spike this is ported from (`experiments/013-vocabulary/morph/`).
23
+ *
24
+ * Bundled to an IIFE by `scripts/build.mjs`, vendored beside GSAP by the CLI,
25
+ * and loaded only by a deck that has a morph in it. The pure halves — `group`,
26
+ * `match`, `plan`, `evaluate` — are exported for the tests, which is why the
27
+ * window registration at the bottom is guarded.
28
+ */
29
+ /** The class prefix an author key is carried on: `\htmlClass{ds-k-<key>}{...}`. */
30
+ export declare const KEY_PREFIX = "ds-k-";
31
+ export interface Leaf {
32
+ el: HTMLElement;
33
+ key: string;
34
+ sig: string;
35
+ text: string;
36
+ /** Font size in layout px. */
37
+ fs: number;
38
+ /** Centre and box, in the host's own layout px. */
39
+ cx: number;
40
+ cy: number;
41
+ w: number;
42
+ h: number;
43
+ }
44
+ export interface Group {
45
+ ident: string;
46
+ key: string;
47
+ leaves: Leaf[];
48
+ cx: number;
49
+ cy: number;
50
+ w: number;
51
+ h: number;
52
+ fs: number;
53
+ text: string;
54
+ }
55
+ /**
56
+ * The unit that moves as one body.
57
+ *
58
+ * This is the whole quality argument of the file. Matching GLYPH BY GLYPH is
59
+ * what a naive reading of TransformMatchingTex suggests and it is visibly
60
+ * wrong: in `a^2+b^2=c^2 -> c^2-b^2=a^2` the three `2`s are interchangeable
61
+ * under any text-plus-class identity, so they pair left-to-right and stay put
62
+ * while their bases cross over. The audience sees the letters swap and the
63
+ * exponents refuse to follow, which asserts something false about the algebra.
64
+ *
65
+ * Manim does not avoid this by being cleverer — `MathTex("a^2", "+", "b^2",
66
+ * ...)` is the AUTHOR splitting the expression into the parts meant to
67
+ * survive. `\htmlClass{ds-k-<key>}{...}` is the same act, and it is the only
68
+ * mechanism here that produces a correct morph on an ambiguous pair.
69
+ *
70
+ * A group's identity is its key alone, not its contents, so `ds-k-exp` can
71
+ * carry `x^2` onto `x^3`: the body moves and its contents dissolve.
72
+ */
73
+ export declare function group(items: Leaf[]): Group[];
74
+ export interface Match {
75
+ pairs: [number, number][];
76
+ dropA: number[];
77
+ addB: number[];
78
+ }
79
+ /**
80
+ * Bucket by identity, then pair NEAREST FIRST within a bucket.
81
+ *
82
+ * Document order — the spike's rule — sends the wrong twin across the slide:
83
+ * `F = E(I), X = W(F)` has two `=` and its successor one, and pairing the
84
+ * first `=` with it flew a glyph 800px over the equation while the `=` sitting
85
+ * 40px from its destination faded out. Nearest-first is what a viewer expects
86
+ * of a symbol that did not move. Ties fall to document order, so it is still a
87
+ * pure function of the two layouts.
88
+ */
89
+ export declare function match(A: Group[], B: Group[]): Match;
90
+ export type Prop = "x" | "y" | "s" | "o";
91
+ export type Ease = "none" | "power1.in" | "power1.out" | "power2.in" | "power2.out" | "power2.inOut";
92
+ /**
93
+ * One property of one leaf, over one slice of the morph.
94
+ *
95
+ * Times are FRACTIONS of the morph, not seconds: the plan is evaluated against
96
+ * the plugin tween's ratio, so `pace()` scaling the tween's `duration` scales
97
+ * all of this with it, and the plan never has to know how long it takes.
98
+ */
99
+ export interface Step {
100
+ el: HTMLElement;
101
+ prop: Prop;
102
+ from: number;
103
+ to: number;
104
+ at: number;
105
+ dur: number;
106
+ ease: Ease;
107
+ }
108
+ /**
109
+ * One similarity transform per matched group, expressed per leaf.
110
+ *
111
+ * A group's leaves move RIGIDLY: every leaf gets the same scale and the
112
+ * translation that the group's own centre-to-centre move implies for its
113
+ * position within the body. `a^2` therefore arrives with its exponent still
114
+ * attached, which is the whole reason groups exist.
115
+ *
116
+ * The B side is placed by the INVERSE transform — B's leaves start where they
117
+ * would sit inside A's box — so when the contents differ the two renderings
118
+ * dissolve into each other while sharing one trajectory.
119
+ *
120
+ * `arc` bows the path. Two glyphs swapping ends of an equation travel the same
121
+ * straight line in opposite directions and pile up in the middle, which reads
122
+ * as a collision rather than an exchange. The bow is two segments on `y` — out
123
+ * over the first half, back over the second — against one on `x` that runs
124
+ * the whole way.
125
+ */
126
+ export declare function plan(A: Group[], B: Group[], m: Match, opt: {
127
+ arc?: boolean;
128
+ }): Step[];
129
+ export interface MorphPlan {
130
+ leaves: {
131
+ el: HTMLElement;
132
+ side: "a" | "b";
133
+ }[];
134
+ steps: Step[];
135
+ }
136
+ /**
137
+ * Write the morph at progress `v` in [0, 1] into every leaf's style.
138
+ *
139
+ * Every leaf is written every time, from its resting state forward — A drawn,
140
+ * B hidden, nothing moved — so a cold seek to any progress produces the same
141
+ * strings as a walk to it. That is the property the capture depends on.
142
+ */
143
+ export declare function evaluate(p: MorphPlan, v: number): void;
144
+ export interface Stats {
145
+ groupsA: number;
146
+ groupsB: number;
147
+ leavesA: number;
148
+ leavesB: number;
149
+ matched: number;
150
+ dropped: number;
151
+ added: number;
152
+ }
153
+ /**
154
+ * Build the morph inside `host`, which must already contain two rendered KaTeX
155
+ * roots marked `data-morph="a"` and `data-morph="b"`. Runs in `measure`, after
156
+ * fonts; the plan is kept for the plugin tween on the same host to find.
157
+ */
158
+ export declare function build(host: HTMLElement, opt?: {
159
+ arc?: boolean;
160
+ }): Stats;
161
+ interface PluginState {
162
+ plan: MorphPlan;
163
+ end: number;
164
+ }
165
+ /**
166
+ * `dsMorph` as a GSAP property: `tl.fromTo(host, { dsMorph: 0 }, { dsMorph: 1 })`.
167
+ *
168
+ * A plugin's `render` is called by the timeline as part of being seeked, so
169
+ * this runs under `suppressEvents` — the capture path — where an `onUpdate`
170
+ * does not. The tween's own ease must be "none": the plan carries its eases
171
+ * per segment, and a second ease on top would warp every one of them.
172
+ */
173
+ export declare const DSMorphPlugin: {
174
+ name: string;
175
+ init(this: PluginState, target: Element, value: unknown): void;
176
+ render(ratio: number, d: PluginState): void;
177
+ };
178
+ export {};
@@ -111,7 +111,7 @@ export { verify } from "./verify/index.js";
111
111
  * deck was asked for, which a built directory does not carry — and
112
112
  * `scanUnusedFigures` folds in only where the source was passed alongside.
113
113
  */
114
- export { scanBeatCount, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
114
+ export { scanBeatCount, scanNarrationDrift, scanPaperArc, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
115
115
  export { check, parseCheckReport, sampleTimes } from "./verify/check.js";
116
116
  export type { CheckOptions } from "./verify/check.js";
117
117
  /**
@@ -26,6 +26,10 @@ export declare const settingsSchema: z.ZodObject<{
26
26
  normal: "normal";
27
27
  dense: "dense";
28
28
  }>>;
29
+ genre: z.ZodOptional<z.ZodEnum<{
30
+ general: "general";
31
+ paper: "paper";
32
+ }>>;
29
33
  duration: z.ZodOptional<z.ZodNumber>;
30
34
  slides: z.ZodOptional<z.ZodInt>;
31
35
  animation_speed: z.ZodOptional<z.ZodNumber>;
@@ -63,6 +67,10 @@ export declare const estimateSchema: z.ZodObject<{
63
67
  normal: "normal";
64
68
  dense: "dense";
65
69
  }>>;
70
+ genre: z.ZodOptional<z.ZodEnum<{
71
+ general: "general";
72
+ paper: "paper";
73
+ }>>;
66
74
  duration: z.ZodOptional<z.ZodNumber>;
67
75
  slides: z.ZodOptional<z.ZodInt>;
68
76
  animation_speed: z.ZodOptional<z.ZodNumber>;
@@ -101,6 +109,10 @@ export declare const createSchema: z.ZodObject<{
101
109
  normal: "normal";
102
110
  dense: "dense";
103
111
  }>>;
112
+ genre: z.ZodOptional<z.ZodEnum<{
113
+ general: "general";
114
+ paper: "paper";
115
+ }>>;
104
116
  duration: z.ZodOptional<z.ZodNumber>;
105
117
  slides: z.ZodOptional<z.ZodInt>;
106
118
  animation_speed: z.ZodOptional<z.ZodNumber>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The paper arc: what a research-talk deck must open and close with.
3
+ *
4
+ * A research talk has a shape a general explainer does not. It opens on the
5
+ * problem and on the ground the work stands on, and it closes on what the work
6
+ * does NOT do and then on what to take away. DeckSmith's planner writes the
7
+ * opening well already — 13 of 15 committed full-deck runs open with a `title` —
8
+ * and the ending badly: 10 of 15 close with a `callout`, but ZERO of 15 carry
9
+ * two, because the limitation arrives as a subordinate clause inside the
10
+ * conclusion ("The idea works, but the strongest lightweight models remain
11
+ * ahead") rather than as its own slide. Splitting that clause into a slide is
12
+ * the actual behaviour change this module asks for.
13
+ *
14
+ * WHY THIS FILE EXISTS AT ALL, rather than the rule living where it is enforced:
15
+ * three directories read this definition and none of them can own it — the
16
+ * prompt (src/plan/prompt.ts) asks the model for the arc, the scan
17
+ * (src/verify/index.ts) reports where a plan missed it, and the cut
18
+ * (src/plan/select.ts) refuses to delete a beat that carries a role. Restating
19
+ * the rule three times is how the three fall out of step; deriving it from one
20
+ * table is how they cannot.
21
+ *
22
+ * WHAT THIS MODULE DELIBERATELY DOES NOT DO. It never decides that a document is
23
+ * a research paper. `prefs.genre` is declared by the author and nothing here
24
+ * sniffs it — see the field's own note in src/types.ts for the measurement that
25
+ * settled that. And it never requires a beat the source cannot support: RULE 3
26
+ * forbids inventing a baseline or a result, so a source that says nothing about
27
+ * prior work honestly yields no `background` beat, and this file's job is then
28
+ * to report the gap rather than to have manufactured one.
29
+ */
30
+ import type { Prefs } from "../prefs.js";
31
+ import type { Beat, BeatRole, Storyboard } from "../types.js";
32
+ /** Every structural job, in the order a deck performs them. */
33
+ export declare const ARC_ROLES: readonly BeatRole[];
34
+ /**
35
+ * Roles required at a given deck length, and why the list shortens.
36
+ *
37
+ * Four reserved slides out of five is not a deck, it is a table of contents. At
38
+ * `--duration 60` a deck is about five beats, so asking for the full arc there
39
+ * is asking for something nobody could deliver, and a gate that fires on the
40
+ * impossible is one people learn to ignore. The thresholds:
41
+ *
42
+ * n >= 8 the full arc. 8 is this project's own definition of a full-deck
43
+ * run — the corpus count of "15 committed full-deck plans" is beats
44
+ * >= 8 — not a number chosen here.
45
+ * 5..7 the ending only. An `intro` is what the planner already writes
46
+ * unprompted 13 times in 15, and `background` is the beat most likely
47
+ * to have no source material behind it, so those two are the ones to
48
+ * give up first when the budget is short.
49
+ * n < 5 nothing. There is no room for a shape.
50
+ *
51
+ * Read off the beat count the plan actually came back with, not off
52
+ * `prefs.slides`: the floor the author asked for and the deck the planner
53
+ * returned are different numbers, and `scanBeatCount` already owns the gap
54
+ * between them.
55
+ */
56
+ export declare function requiredRoles(beatCount: number): readonly BeatRole[];
57
+ /** Whether the paper arc was asked for at all. Declared, never sniffed. */
58
+ export declare function paperArcRequested(prefs: Pick<Prefs, "genre">): boolean;
59
+ /** The beats carrying a structural role, by role. Later duplicates are reported, not kept. */
60
+ export declare function arcBeats(storyboard: Storyboard): Map<BeatRole, Beat[]>;
61
+ /**
62
+ * Ids of every beat carrying a role — what the cut refuses to release.
63
+ *
64
+ * Takes a beat list rather than a Storyboard because the cut works over the
65
+ * surviving beats, not over the plan.
66
+ */
67
+ export declare function arcIds(beats: readonly Beat[]): Set<string>;
68
+ /**
69
+ * Where a plan departs from the arc it was asked for, as sentences.
70
+ *
71
+ * ORDER IS CHECKED, NOT JUST PRESENCE, and only where order is the point. The
72
+ * user's requirement is specifically that the deck END on the conclusion with
73
+ * limitations immediately before it — an ending is a position, not a topic — so
74
+ * those two are checked against the last two slots. The opening pair is checked
75
+ * for presence and for being early, because "the first couple of slides" is a
76
+ * region rather than an index, and a deck that opens title, problem, background
77
+ * is not wrong.
78
+ *
79
+ * Returns an empty array when the arc was not requested, when the deck is too
80
+ * short to carry it, or when the plan satisfied it.
81
+ */
82
+ export declare function arcProblems(storyboard: Storyboard, prefs: Pick<Prefs, "genre" | "slides">): string[];
@@ -1,5 +1,11 @@
1
1
  import type { Prefs } from "../prefs.js";
2
2
  import { type Source, type Storyboard } from "../types.js";
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;
5
+ /**
6
+ * The default-preferences schema, unchanged and still exported: `general` hides
7
+ * `role`, so these bytes are what they have always been.
8
+ */
3
9
  export declare const SCHEMA: unknown;
4
10
  export interface CodexOptions {
5
11
  /** Left unset by default: use whatever model the user's Codex is configured for. */
@@ -39,6 +39,7 @@ export interface PrefFlags {
39
39
  lang?: string;
40
40
  tone?: string;
41
41
  density?: string;
42
+ genre?: string;
42
43
  duration?: string | number;
43
44
  theme?: string;
44
45
  speed?: string | number;
@@ -60,20 +60,6 @@ export interface AudioInput {
60
60
  file: string;
61
61
  delayMs: number;
62
62
  }
63
- /**
64
- * Delay every segment onto one track and sum them.
65
- *
66
- * `amix` with `normalize=0` sums rather than averaging: the default divides by
67
- * the input count, which on a 37-segment deck would render the narration 31 dB
68
- * down and sound exactly like a bug in the TTS. The segments never overlap — the
69
- * timing model gives each one the video's undivided attention — so summing is
70
- * safe. `dropout_transition=0` stops amix ramping the gain as inputs end.
71
- *
72
- * Every input is resampled and laid out identically first, because amix refuses
73
- * a mismatch and edge-tts emits 24 kHz mono while the video wants 48 kHz.
74
- * `apad` runs the track out to the video's length so the mux does not have to
75
- * choose between a short audio stream and `-shortest` truncating the picture.
76
- */
77
63
  export declare function audioGraph(inputs: readonly AudioInput[], seconds: number,
78
64
  /**
79
65
  * ffmpeg input index of the first mp3. 1 when the video is the only other