@jokerized/decksmith 0.1.4 → 0.3.0

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 (40) hide show
  1. package/README.md +161 -26
  2. package/dist/cli.js +3864 -1816
  3. package/dist/ds-morph.js +1 -0
  4. package/dist/index.js +2701 -780
  5. package/dist/mcp.js +2509 -633
  6. package/dist/types/emit/archetypes/annotated-figure.d.ts +3 -2
  7. package/dist/types/emit/archetypes/data-table.d.ts +0 -19
  8. package/dist/types/emit/archetypes/equation-morph.d.ts +2 -0
  9. package/dist/types/emit/archetypes/equation-walk.d.ts +63 -1
  10. package/dist/types/emit/archetypes/index.d.ts +1 -1
  11. package/dist/types/emit/archetypes/pipeline.d.ts +2 -2
  12. package/dist/types/emit/archetypes/stack.d.ts +20 -1
  13. package/dist/types/emit/archetypes/title.d.ts +31 -14
  14. package/dist/types/emit/camera.d.ts +25 -0
  15. package/dist/types/emit/composition.d.ts +20 -2
  16. package/dist/types/emit/depth.d.ts +95 -0
  17. package/dist/types/emit/kit.d.ts +153 -0
  18. package/dist/types/emit/morph-runtime.d.ts +178 -0
  19. package/dist/types/emit/svg.d.ts +77 -13
  20. package/dist/types/emit/theme.d.ts +1 -0
  21. package/dist/types/images/illustrate.d.ts +34 -0
  22. package/dist/types/images/providers.d.ts +108 -0
  23. package/dist/types/index.d.ts +27 -8
  24. package/dist/types/mcp/prereqs.d.ts +18 -0
  25. package/dist/types/mcp/tools.d.ts +23 -0
  26. package/dist/types/plan/codex.d.ts +25 -3
  27. package/dist/types/plan/duration.d.ts +190 -15
  28. package/dist/types/plan/prompt.d.ts +7 -1
  29. package/dist/types/plan/refs.d.ts +41 -3
  30. package/dist/types/prefs.d.ts +21 -6
  31. package/dist/types/render/capture.d.ts +99 -0
  32. package/dist/types/render/render.d.ts +18 -0
  33. package/dist/types/server/options.d.ts +7 -0
  34. package/dist/types/server/pipeline.d.ts +14 -0
  35. package/dist/types/server/queue.d.ts +1 -1
  36. package/dist/types/types.d.ts +294 -4
  37. package/dist/types/verify/apparent.d.ts +123 -0
  38. package/dist/types/verify/index.d.ts +114 -6
  39. package/dist/types/verify/typefloor.d.ts +3 -1
  40. package/package.json +2 -2
@@ -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 {};
@@ -13,7 +13,7 @@
13
13
  * that composes these primitives, and putting it here is how a primitive layer
14
14
  * turns into six hand-rolled one-offs wearing a shared import.
15
15
  */
16
- import { type Vars } from "./kit.js";
16
+ import { type Tween, type Vars } from "./kit.js";
17
17
  export interface Pt {
18
18
  x: number;
19
19
  y: number;
@@ -47,18 +47,54 @@ export declare function n(v: number): string;
47
47
  */
48
48
  export declare function nv(v: number): number;
49
49
  /**
50
- * The `from` vars of a stroke draw-on, with the path fully hidden.
50
+ * The `from` and `to` vars of a stroke draw-on, measured by the browser.
51
51
  *
52
- * The length is rounded UP rather than to two decimals, and then given a pixel
53
- * of slack, because GSAP writes `strokeDashoffset` back as an integer px: a
54
- * leader of length 720.21 came to rest at an offset of 720 and left 0.21px of
55
- * the dash exposed at the path start, which `stroke-linecap: round` paints as a
56
- * full-width dot. A yellow dot therefore sat on the grid one reveal before its
57
- * own region was drawn — inside the frame, above the type floor, and invisible
58
- * to every gate. Any offset between the length and twice it is still inside the
59
- * pattern's gap, so the slack costs nothing.
52
+ * The `drawFrom` this replaced needed the emitter to know the path's length at
53
+ * BUILD time
54
+ * `Math.hypot(knee.x - start.x, knee.y - start.y) + b.w` in annotated-figure,
55
+ * `perimeter(b)` in grid which is arithmetic about a shape the browser is
56
+ * going to measure anyway, kept in step by hand. DrawSVG asks the path.
57
+ *
58
+ * It also retires a bug the hand-computed version had. GSAP writes
59
+ * `strokeDashoffset` back as an integer px, so a leader of length 720.21 came to
60
+ * rest at an offset of 720 and left 0.21px of dash exposed at the path start,
61
+ * which `stroke-linecap: round` paints as a full-width dot: a yellow dot sat on
62
+ * the grid one reveal before its own region was drawn, inside the frame, above
63
+ * the type floor, and invisible to every gate. That is why the old `drawFrom`
64
+ * rounded up and added a pixel of slack. A percentage has no remainder to leave.
65
+ *
66
+ * Safe under capture because a plugin's `render()` is part of being seeked,
67
+ * unlike a callback — see `DRAWSVG_SRC` in composition.ts for the measurement.
68
+ * Anything drawn this way still obeys invariant 2: it is a `fromTo`, always.
69
+ */
70
+ export declare const DRAW_FROM: Vars;
71
+ export declare const DRAW_TO: Vars;
72
+ /**
73
+ * Something travels a polyline — a pulse along an arrow, a marker ring along a
74
+ * curve, a highlight down a divider: one `x`/`y` `fromTo` per leg, each leg's
75
+ * share of `seconds` proportional to its length, `ease: "none"` until the last
76
+ * leg, which settles with `power2.out`. Only the first leg renders
77
+ * immediately; every later one is `immediateRender: false` and starts exactly
78
+ * where the leg before it ended, so the route reads as one motion under any
79
+ * seek. `x`/`y` are TRANSLATIONS, so author the traveller at the origin —
80
+ * `<circle r="6">` — and give the route in the same user space as the parts it
81
+ * passes. A transform rather than `attr: { cx, cy }` so one verb moves an SVG
82
+ * dot and an HTML rule alike; not MotionPath, which is not loaded.
83
+ *
84
+ * ONE CALL PER ELEMENT. The first leg's immediate render is the one that
85
+ * (element, x/y) gets; a second route on the same element would need
86
+ * `immediateRender: false` and a `from` equal to the first route's end, which
87
+ * is not where a second route starts. A pulse per arrow, not one pulse reused.
88
+ *
89
+ * Every leg starts where the last one ended, printed at two places, and lint
90
+ * reads `at + duration` back in floating point: 0.1 + 0.2 is 0.30000000000000004,
91
+ * which `overlapping_gsap_tweens` reports as a 4e-17s overlap with the leg at
92
+ * 0.3 (measured on 0.7.90). So a leg whose float sum overshoots its successor
93
+ * is shortened by a hundredth — a rest at the corner under one frame long —
94
+ * and the corner itself, and the arrival at `at + seconds`, stay where they
95
+ * were scheduled.
60
96
  */
61
- export declare function drawFrom(length: number): Vars;
97
+ export declare function travel(target: string, route: readonly Pt[], at: number, seconds: number): Tween[];
62
98
  /** Scene-scoped element id. Every timeline selector is built from one of these. */
63
99
  export declare function id(sid: string, part: string, i?: number): string;
64
100
  /**
@@ -68,13 +104,36 @@ export declare function id(sid: string, part: string, i?: number): string;
68
104
  * Every archetype sizes its boxes and padding through this function. When two of
69
105
  * them disagree about how wide "Reconstruction" is, one of them clips.
70
106
  */
71
- export declare function textWidth(text: string, fontSize: number, weight?: number, tracking?: number, tabular?: boolean): number;
107
+ /**
108
+ * Which face actually draws a run.
109
+ *
110
+ * A deck that bundles a CJK family sets EVERY run in it, ASCII included, because
111
+ * `fontStack` puts that family ahead of Inter and it covers Latin. So the face is
112
+ * a property of the DECK, not of the characters in one run — and a run of pure
113
+ * ASCII inside a Korean deck is drawn by the Korean face even though nothing in
114
+ * it is Korean.
115
+ *
116
+ * `"latin"` is not "no CJK in this string"; it is "this deck bundles no CJK
117
+ * family". Passing it for a run that does contain CJK would be a lie, which is
118
+ * why the auto-detection below still runs underneath: the parameter can only
119
+ * widen what a run is charged, never narrow it.
120
+ */
121
+ export type Face = "latin" | "cjk" | "hangul";
122
+ /**
123
+ * The face a theme's stack will use, from the family `familyFor` put in front.
124
+ *
125
+ * Korean is split out because it is the one character the four bundled families
126
+ * disagree about: KR draws the middle dot at 0.561em where JP, SC and TC draw it
127
+ * on the em grid at 1.0.
128
+ */
129
+ export declare function faceOf(fontStack: string): Face;
130
+ export declare function textWidth(text: string, fontSize: number, weight?: number, tracking?: number, tabular?: boolean, face?: Face): number;
72
131
  /**
73
132
  * Greedy word wrap to `maxWidth`. A word wider than the line is broken by
74
133
  * character — which is also how Korean and Chinese wrap, since they arrive as
75
134
  * one unbroken "word".
76
135
  */
77
- export declare function wrap(text: string, fontSize: number, maxWidth: number, weight?: number, tracking?: number): string[];
136
+ export declare function wrap(text: string, fontSize: number, maxWidth: number, weight?: number, tracking?: number, face?: Face): string[];
78
137
  /** The SVG root. Width and height match the viewBox so px and user units agree. */
79
138
  export declare function svg(elementId: string, w: number, h: number, body: string): string;
80
139
  export declare function rect(b: Box, a?: Attrs): string;
@@ -100,6 +159,11 @@ export interface TextOptions {
100
159
  vAlign?: "baseline" | "middle";
101
160
  class?: string;
102
161
  id?: string;
162
+ /**
163
+ * The face the deck's stack will draw this in. Only consulted when `maxWidth`
164
+ * makes this text wrap — an unwrapped run is measured by nothing here.
165
+ */
166
+ face?: Face;
103
167
  }
104
168
  export declare function text(content: string, at: Pt, o: TextOptions): string;
105
169
  export interface ArrowOptions {
@@ -16,6 +16,7 @@ import { type Scene } from "./kit.js";
16
16
  import type { DeckTheme } from "./themes/index.js";
17
17
  export { type DeckTheme, ink, mono, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
18
18
  /** Where `ingest` writes the subsetted font bundle, relative to the deck. */
19
+ export declare const FONT_BUNDLE_DIR = "assets/fonts";
19
20
  export declare const FONT_BUNDLE_HREF = "assets/fonts/fonts.css";
20
21
  /** A luminance breath. Safe on an element an entrance tween already moves. */
21
22
  export declare const BREATHE = "ds-breathe 6s ease-in-out infinite alternate";
@@ -0,0 +1,34 @@
1
+ import type { Prefs } from "../prefs.js";
2
+ import { type Source, type Storyboard } from "../types.js";
3
+ import { type ImageProvider } from "./providers.js";
4
+ export interface IllustrateOpts {
5
+ prefs: Prefs;
6
+ /** `<dir of source.json>/assets`. Created if missing. */
7
+ assetsDir: string;
8
+ /** Tests inject; the default is `imageChain(prefs.images, resolveImageBackend())`. */
9
+ chain?: ImageProvider[];
10
+ /** Progress lines. Silent by default, for the reason `buildDeck` gives. */
11
+ onStep?: (message: string) => void;
12
+ }
13
+ export interface Illustrated {
14
+ beatId: string;
15
+ figureId: string;
16
+ /** Which rung drew it. */
17
+ provider: string;
18
+ /** The figure's `src`: a file name under `assetsDir`. */
19
+ src: string;
20
+ /** Found on disk from an earlier run; no provider was asked. */
21
+ cached: boolean;
22
+ }
23
+ /**
24
+ * Draw every pending brief. Always finishes: the tool rung is last and pure.
25
+ *
26
+ * Returns fresh objects parsed through the schemas; the ones passed in are not
27
+ * touched, so a caller that keeps the old storyboard around still holds the
28
+ * pending one.
29
+ */
30
+ export declare function illustrate(storyboard: Storyboard, source: Source, opts: IllustrateOpts): Promise<{
31
+ storyboard: Storyboard;
32
+ source: Source;
33
+ illustrated: Illustrated[];
34
+ }>;
@@ -0,0 +1,108 @@
1
+ import { type Runner } from "../plan/codex.js";
2
+ import type { ImagesPrefs } from "../types.js";
3
+ export type ImageAspect = "landscape" | "square" | "portrait";
4
+ export interface ImageRequest {
5
+ /** The scene. Never text, labels, numbers or charts — nothing in a picture can be read or checked. */
6
+ prompt: string;
7
+ /** `images.style`, folded into every prompt. */
8
+ style: string;
9
+ aspect: ImageAspect;
10
+ /**
11
+ * `images.model`, for the separate backend only; the Codex and tool rungs
12
+ * ignore it. Carried on the request rather than baked into the provider
13
+ * because the provider is resolved from the environment before any
14
+ * preferences are known, and a `--image-model` that changed only the cache
15
+ * key would be a flag that does nothing.
16
+ */
17
+ model?: string;
18
+ }
19
+ export interface ImageResult {
20
+ bytes: Buffer;
21
+ mime: "image/png" | "image/jpeg" | "image/svg+xml";
22
+ width: number;
23
+ height: number;
24
+ }
25
+ /**
26
+ * One rung. `check` is separate from `generate` for the reason `SpeechProvider`
27
+ * splits them: a missing key is reported before a picture is asked for, not
28
+ * two minutes into one.
29
+ */
30
+ export interface ImageProvider {
31
+ readonly id: string;
32
+ /** Throws with an actionable sentence. */
33
+ check(): Promise<void>;
34
+ generate(req: ImageRequest): Promise<ImageResult>;
35
+ }
36
+ /** Pixels per aspect — the backend's `size`, and the tool's viewBox. */
37
+ export declare const SIZE: Readonly<Record<ImageAspect, {
38
+ width: number;
39
+ height: number;
40
+ }>>;
41
+ export interface OpenAiImagesOptions {
42
+ apiKey: string;
43
+ /** `POST {baseUrl}/images/generations`. LocalAI and most gateways speak this. */
44
+ baseUrl?: string;
45
+ /** Default when the request names none. */
46
+ model?: string;
47
+ /** Injected by tests. */
48
+ fetch?: typeof fetch;
49
+ }
50
+ /**
51
+ * Any OpenAI-compatible images endpoint.
52
+ *
53
+ * Errors are shaped here rather than passed through: the message is status plus
54
+ * the API's `error.code` (or `type`), never its text and never a URL, so it can
55
+ * go straight into a job log a stranger reads. The key is used exactly once,
56
+ * on the generation request — a `url` in the answer is fetched bare, and only
57
+ * when it sits on the backend's own origin, because following an arbitrary URL
58
+ * with or without the key is how a gateway's answer becomes an open proxy.
59
+ */
60
+ export declare function openaiImages(opts: OpenAiImagesOptions): ImageProvider;
61
+ export interface CodexImagesOptions {
62
+ /** Injected by tests; `runCodex` otherwise. */
63
+ run?: Runner;
64
+ timeoutMs?: number;
65
+ }
66
+ /**
67
+ * The main account, drawing with its own image tool.
68
+ *
69
+ * `codex exec` runs in a scratch directory under `workspace-write`, which
70
+ * `codexCommand` fences to that directory and nothing else, and is asked to
71
+ * leave one PNG there. An account without the tool answers `ok:false` and says
72
+ * why; that reason is the error, verbatim, because it is the one thing the
73
+ * user can act on. Whether non-interactive `codex exec` exposes the tool at all
74
+ * is the hypothesis the design's live run settles — this code is written for
75
+ * either answer.
76
+ */
77
+ export declare function codexImages(opts?: CodexImagesOptions): ImageProvider;
78
+ /**
79
+ * The picture the tool draws for itself: six to ten overlapping shapes in one
80
+ * accent and the ink, seeded from the brief. Not an illustration of the scene —
81
+ * a stable, text-free composition that keeps the slide's layout honest until
82
+ * something better draws it. Same request, same bytes.
83
+ */
84
+ export declare function drawSvg(req: ImageRequest): string;
85
+ /** The size the tool wrote into its own SVG. `imageSize` is raster-only on purpose. */
86
+ export declare function svgSize(bytes: Buffer): {
87
+ width: number;
88
+ height: number;
89
+ };
90
+ /** The last rung. Pure, so it cannot fail, so `illustrate` always finishes. */
91
+ export declare function toolSvg(): ImageProvider;
92
+ /**
93
+ * The backend the environment names, or nothing.
94
+ *
95
+ * Environment only, mirroring `DECKSMITH_TTS`: a key is never a preference,
96
+ * never in a config file and never in a `.deck`. Naming a backend without its
97
+ * key, or naming one this build does not have, throws HERE — at `illustrate`
98
+ * on the CLI, in the server's preflight, in MCP `capabilities` — and nowhere
99
+ * at import, so a broken deployment is a sentence in the log rather than a
100
+ * process that will not start.
101
+ */
102
+ export declare function resolveImageBackend(env?: NodeJS.ProcessEnv): ImageProvider | undefined;
103
+ /**
104
+ * The rungs, in the order `illustrate` tries them, for one preference.
105
+ * `auto` is all three; `codex` skips the backend; `svg` is the tool alone — no
106
+ * network, no spend, and a deck whose every picture is reproducible.
107
+ */
108
+ export declare function imageChain(images: ImagesPrefs, backend?: ImageProvider): ImageProvider[];
@@ -34,7 +34,7 @@ export type { CodexOptions, Runner } from "./plan/codex.js";
34
34
  * not contain. `buildDeck` does not call it — a caller assembling a storyboard
35
35
  * by hand wants this before it spends a render on a dangling ref.
36
36
  */
37
- export { assertInsideResolves, assertRefsResolve } from "./plan/refs.js";
37
+ export { assertInsideResolves, assertRefsResolve, hasIllustrations } from "./plan/refs.js";
38
38
  /** The prompt, so a consumer driving its own model can reproduce our planning. */
39
39
  export { renderSource, systemPrompt } from "./plan/prompt.js";
40
40
  /**
@@ -42,8 +42,22 @@ export { renderSource, systemPrompt } from "./plan/prompt.js";
42
42
  * turned into the two derived ones. Pure arithmetic over preferences, so a
43
43
  * caller can ask what a target costs before spending a plan on it.
44
44
  */
45
- export { COMFORTABLE_CPS, durationPlan, slidesFor, LAST_HOLD_SECONDS, MAX_PLAYBACK, MIN_SENTENCE_CHARS, MOTION_SHARE, p95CueRate, playbackFactor, playbackWarning, SPEAKING_STOPS, SPEECH_CPS, STOPS_PER_BEAT, tempoChain, } from "./plan/duration.js";
45
+ export { COMFORTABLE_CPS, durationPlan, slidesFor, LAST_HOLD_SECONDS, MAX_PLAYBACK, MIN_SENTENCE_CHARS, MOTION_SHARE, p95CueRate, playbackFactor, playbackRefusal, playbackWarning, SPEAKING_STOPS, SPEECH_CPS, STOPS_PER_BEAT, tempoChain, } from "./plan/duration.js";
46
46
  export type { DurationPlan } from "./plan/duration.js";
47
+ /**
48
+ * Pictures for the beats that asked for one. `illustrate` is a plan-time step
49
+ * like `narrate` — it writes files beside the source and rewrites both
50
+ * documents — so a server runs it between `plan` and `build` and hands the
51
+ * returned objects on; nothing after it knows a figure was generated.
52
+ * `resolveImageBackend` and `imageChain` are the two halves of "where the
53
+ * pictures come from": the environment names a backend, the preference says
54
+ * where the chain starts, and a caller that wants neither injects its own
55
+ * `ImageProvider[]` — which is how a server's tests never spawn Codex.
56
+ */
57
+ export { illustrate } from "./images/illustrate.js";
58
+ export type { Illustrated, IllustrateOpts } from "./images/illustrate.js";
59
+ export { imageChain, resolveImageBackend } from "./images/providers.js";
60
+ export type { ImageAspect, ImageProvider, ImageRequest, ImageResult, } from "./images/providers.js";
47
61
  /**
48
62
  * Text-to-speech over a storyboard. Needs the `edge-tts` binary on PATH, which
49
63
  * is why it is a separate call and not a `buildDeck` option.
@@ -87,12 +101,17 @@ export type { DeckTheme } from "./emit/themes/index.js";
87
101
  */
88
102
  export { verify } from "./verify/index.js";
89
103
  /**
90
- * The storyboard advisory `plan` prints and `verify` folds in: a headline that
91
- * recites the visual's own labels as a list. Exported because it is pure over a
92
- * storyboard no browser, no built deck so a caller can run it on a plan
93
- * before spending anything on it, which is the only moment it is worth acting on.
94
- */
95
- export { scanHeadlines, scanNarrationLead, scanRepeatedObject } from "./verify/index.js";
104
+ * The storyboard advisories `plan` prints: a headline that recites the visual's
105
+ * own labels as a list, one object drawn twice, a name spoken before it is drawn,
106
+ * a source figure the deck never uses, and a plan that came back under the slide
107
+ * count it was asked for. Exported because they are pure over a storyboard no
108
+ * browser, no built deck — so a caller can run them on a plan before spending
109
+ * anything on it, which is the only moment they are worth acting on. All but
110
+ * `scanBeatCount` also fold into `verify`; that one needs the preferences the
111
+ * deck was asked for, which a built directory does not carry — and
112
+ * `scanUnusedFigures` folds in only where the source was passed alongside.
113
+ */
114
+ export { scanBeatCount, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
96
115
  export { check, parseCheckReport, sampleTimes } from "./verify/check.js";
97
116
  export type { CheckOptions } from "./verify/check.js";
98
117
  /**
@@ -15,6 +15,24 @@ export interface Prereq {
15
15
  * probe is therefore advisory — hence the honest `neededFor` naming both stages.
16
16
  */
17
17
  export declare function prereqs(): Promise<Prereq[]>;
18
+ /**
19
+ * Where pictures would come from, asked the same way and for the same reason.
20
+ *
21
+ * Not a `Prereq`, because nothing has to be installed: the last rung is an SVG
22
+ * the tool draws itself, so a request for illustrations never fails for lack of
23
+ * a backend. What CAN be wrong is a backend the environment names and cannot
24
+ * use — DECKSMITH_IMAGES set, the key not — and that is a misconfiguration to
25
+ * report before the plan is paid for, not a stage to skip. `why` is the
26
+ * library's own sentence, which names the variable and never carries the key.
27
+ *
28
+ * `env` is injected in tests, like `probe`: which backend this machine happens
29
+ * to name is not what a test is about.
30
+ */
31
+ export declare function imageBackend(env?: NodeJS.ProcessEnv): {
32
+ backend: string | null;
33
+ ok: boolean;
34
+ why?: string;
35
+ };
18
36
  /** The ones a job with these options actually needs, and which are missing. */
19
37
  export declare function missingFor(found: readonly Prereq[], opts: {
20
38
  narrate: boolean;
@@ -39,6 +39,7 @@ export declare const settingsSchema: z.ZodObject<{
39
39
  low: "low";
40
40
  }>>;
41
41
  video: z.ZodOptional<z.ZodBoolean>;
42
+ images: z.ZodOptional<z.ZodBoolean>;
42
43
  }, z.core.$strip>;
43
44
  export type Settings = z.infer<typeof settingsSchema>;
44
45
  export declare const capabilitiesSchema: z.ZodObject<{}, z.core.$strip>;
@@ -75,6 +76,7 @@ export declare const estimateSchema: z.ZodObject<{
75
76
  low: "low";
76
77
  }>>;
77
78
  video: z.ZodOptional<z.ZodBoolean>;
79
+ images: z.ZodOptional<z.ZodBoolean>;
78
80
  }, z.core.$strip>;
79
81
  }, z.core.$strip>;
80
82
  export declare const createSchema: z.ZodObject<{
@@ -112,6 +114,7 @@ export declare const createSchema: z.ZodObject<{
112
114
  low: "low";
113
115
  }>>;
114
116
  video: z.ZodOptional<z.ZodBoolean>;
117
+ images: z.ZodOptional<z.ZodBoolean>;
115
118
  }, z.core.$strip>>;
116
119
  wait_seconds: z.ZodOptional<z.ZodNumber>;
117
120
  }, z.core.$strip>;
@@ -132,6 +135,13 @@ export interface McpOptions {
132
135
  * probe, so the server itself is unchanged.
133
136
  */
134
137
  probe?: () => Promise<Prereq[]>;
138
+ /**
139
+ * The environment the image backend is resolved from, for `capabilities` and
140
+ * the refusal in `create`. Injected for the reason `probe` is. The pipeline
141
+ * itself still reads `process.env` when its `illustrate` stage runs — in a
142
+ * server the two are the same object, and a test never lets a job get there.
143
+ */
144
+ env?: NodeJS.ProcessEnv;
135
145
  }
136
146
  /**
137
147
  * The four tools, over one queue.
@@ -143,6 +153,11 @@ export interface McpOptions {
143
153
  export declare function deckTools(opts: McpOptions): {
144
154
  /** Formats, themes, every setting's range, and what is actually installed. */
145
155
  capabilities(): Promise<{
156
+ images: {
157
+ backend: string | null;
158
+ ok: boolean;
159
+ why?: string;
160
+ };
146
161
  prerequisites: Prereq[];
147
162
  root: string;
148
163
  work: string;
@@ -155,6 +170,14 @@ export declare function deckTools(opts: McpOptions): {
155
170
  * the author what their settings cost ("60s over 12 slides leaves 85
156
171
  * characters a slide, which is one sentence"), and an MCP that swallowed
157
172
  * them would ship the exact failure this project keeps having.
173
+ *
174
+ * EVERY NUMBER HERE IS AT THE REQUESTED SLIDE COUNT, which is the only one a
175
+ * pre-flight has — there is no plan yet, so there are no beats to strike the
176
+ * budget at. `create` below builds the deck at the count the planner returns
177
+ * (see `durationPlan`'s header), so a plan that comes back short reports
178
+ * different numbers than this did, and `scanBeatCount` says by how much. The
179
+ * field names carry it: `slides` is the request, and everything `_per_slide`
180
+ * is per requested slide.
158
181
  */
159
182
  estimate(input: z.infer<typeof estimateSchema>): {
160
183
  slides: number;
@@ -13,12 +13,34 @@ export interface CodexOptions {
13
13
  /** Swappable so a test can drive the real parse path without spawning anything. */
14
14
  run?: Runner;
15
15
  }
16
- /** What the planner needs from the outside world: a prompt in, a final message out. */
17
- export type Runner = (args: {
16
+ export interface RunnerArgs {
18
17
  prompt: string;
19
18
  schemaPath: string;
20
19
  outPath: string;
21
20
  model?: string;
22
21
  timeoutMs: number;
23
- }) => Promise<void>;
22
+ /** Where the agent runs. Absent means wherever this process is, which planning never cares about. */
23
+ cwd?: string;
24
+ /**
25
+ * `read-only` unless said otherwise. `workspace-write` exists for the one
26
+ * caller that needs a file back — `illustrate`'s Codex rung — and needs a
27
+ * `cwd` to fence the writes to.
28
+ */
29
+ sandbox?: "read-only" | "workspace-write";
30
+ }
31
+ /** What the planner needs from the outside world: a prompt in, a final message out. */
32
+ export type Runner = (args: RunnerArgs) => Promise<void>;
24
33
  export declare function codexPlanner(source: Source, opts?: CodexOptions): Promise<Storyboard>;
34
+ /**
35
+ * The `codex` command line for a run, and the environment it needs.
36
+ *
37
+ * Pure, and exported for that reason: the argv is a contract with a binary
38
+ * nothing in the test suite may spawn, so it is pinned by reading this rather
39
+ * than by running it.
40
+ */
41
+ export declare function codexCommand(args: RunnerArgs): {
42
+ argv: string[];
43
+ env?: NodeJS.ProcessEnv;
44
+ };
45
+ /** The production `Runner`: `codex exec` on PATH, stdin in, a file out. */
46
+ export declare function runCodex(args: RunnerArgs): Promise<void>;