@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,123 @@
1
+ /**
2
+ * The type floor, measured on the rendered frame instead of read off the source.
3
+ *
4
+ * `typefloor.ts` scans declared sizes and says so plainly in its own header:
5
+ * "text shrunk by a `scale` below 1 at a hold reads as its unscaled size". That
6
+ * hole is not hypothetical and it is not only about `scale`. Measured in Chrome
7
+ * at `rotateX(30deg)` under `perspective: 1400px`, a run declaring 46px renders
8
+ * 31.4 apparent px near the top of the plane — invariant 5 broken by 8.6px, with
9
+ * `lint`, `check`, `drift` and the declared-size scan all green, because not one
10
+ * of them looks at how big the glyph actually came out.
11
+ *
12
+ * That matters now because Gap 2 wants depth, and every way of tilting a plane
13
+ * shrinks the type on the far half of it. A depth archetype built against a floor
14
+ * that cannot see projection would ship unreadable slides that pass. So the gate
15
+ * comes first and the archetype second.
16
+ *
17
+ * HOW APPARENT SIZE IS MEASURED. For an SVG text node, `getBBox()` is the
18
+ * untransformed box in user units and `getClientRects()` is the painted box in
19
+ * device px, so their ratio is the TOTAL scale between the two — the element's
20
+ * own transform, every ancestor transform, the 3D projection's per-point
21
+ * perspective divide, and the stage zoom, all of it, without having to know which
22
+ * of them applied. Measured against the three cases that matter:
23
+ *
24
+ * flat, declared 46 ratio 1.000 -> 46.0 apparent px
25
+ * tilted 30deg, near plane top 0.682 -> 31.4
26
+ * tilted 30deg, near plane bottom 1.126 -> 51.8 (magnified, not a fault)
27
+ * `scale(0.6)` at a hold 0.600 -> 27.6
28
+ *
29
+ * `getScreenCTM()` was tried first and rejected: it returns 0.673 for BOTH the
30
+ * top and bottom runs above, because it carries the affine part and drops the
31
+ * perspective divide that makes those two differ by 23 apparent px.
32
+ *
33
+ * WHY IT DIVIDES BY THE STAGE. A deck legitimately scales its whole stage —
34
+ * `zoomOf`/`REF_PULL` — and in portrait 40 reference px is SUPPOSED to land at 30
35
+ * canvas px, which `typefloor.ts` calls "the whole argument of REF_PULL and not a
36
+ * violation". Dividing the run's own ratio by the scene's gives apparent size
37
+ * back in REFERENCE px, so this gate agrees with the declared one on every deck
38
+ * that does nothing clever, and only diverges where something shrank one element
39
+ * relative to its own scene.
40
+ */
41
+ import type { Finding } from "../types.js";
42
+ /** One text run, as the frame actually drew it. */
43
+ export interface ApparentRun {
44
+ /** First 40 characters, for the message. */
45
+ text: string;
46
+ /** `font-size` as declared, in px. */
47
+ declared: number;
48
+ /** Painted height over untransformed height, including the stage's own zoom. */
49
+ ratio: number;
50
+ /** Its own computed opacity, multiplied down the ancestors that carry one. */
51
+ opacity: number;
52
+ }
53
+ /** What `measureApparent` hands back for one instant. */
54
+ export interface ApparentStop {
55
+ sid: string;
56
+ t: number;
57
+ runs: ApparentRun[];
58
+ /** The scene's own painted-over-declared scale, the baseline runs divide by. */
59
+ stage: number;
60
+ /**
61
+ * True at a declared stop, where the frame has arrived and everything drawn is
62
+ * being read. False at a sampled midpoint, where a run may be part-way through
63
+ * its own entrance — see `SETTLED_OPACITY`.
64
+ */
65
+ settled: boolean;
66
+ }
67
+ /**
68
+ * Serialised into the page: every visible SVG text run's apparent scale.
69
+ *
70
+ * Deliberately the same walk as `collectSvgTextRuns` in overprint.ts — same
71
+ * visibility rules, same "only inside an `<svg>`" restriction — because two gates
72
+ * that disagree about which text exists are two gates that cannot be reconciled
73
+ * when they disagree about a deck.
74
+ */
75
+ export declare function collectApparent(sid: string): {
76
+ runs: ApparentRun[];
77
+ stage: number;
78
+ };
79
+ /**
80
+ * Apparent size in reference px: what the run declared, times how much the frame
81
+ * shrank or grew it relative to its own scene.
82
+ */
83
+ export declare function apparentPx(run: ApparentRun, stage: number): number;
84
+ /**
85
+ * How opaque a run must be, at a sampled midpoint, to count as text being read.
86
+ *
87
+ * At a declared stop the frame has arrived and everything drawn counts. Between
88
+ * stops it has not: `annotated-figure` enters its labels from `scale: 0.97`, so
89
+ * a run caught half way through its own entrance is BOTH under the floor and
90
+ * fading in, and failing a build for it would be crying wolf about a frame no
91
+ * one reads. A run at 0.95 opacity is not entering any more; it has arrived.
92
+ */
93
+ export declare const SETTLED_OPACITY = 0.95;
94
+ /**
95
+ * The times to sample BETWEEN the declared stops.
96
+ *
97
+ * `fidelity` measures at stops because that is where a frame has settled, and
98
+ * the apparent floor inherited that — which left exactly the hole it was built
99
+ * to close, one interval over: text scaled DOWN between two stops is invisible
100
+ * to a gate that only looks at the stops. A camera pulling back, or an exit that
101
+ * shrinks a label, would ship.
102
+ *
103
+ * Midway between consecutive stops of the SAME scene. Not across a scene
104
+ * boundary, where the midpoint lands in a cross-fade between two compositions
105
+ * and belongs to neither. Costs a seek and a DOM read each — no screenshot,
106
+ * which is what makes doubling the sample count affordable.
107
+ */
108
+ export declare function midpoints(stops: readonly {
109
+ sid: string;
110
+ t: number;
111
+ }[]): {
112
+ sid: string;
113
+ t: number;
114
+ }[];
115
+ /**
116
+ * Runs the frame drew below the floor, whatever shrank them.
117
+ *
118
+ * An ERROR, not a warning, and for the same reason `typefloor` is: text the
119
+ * audience cannot read is not a matter of degree. The tolerance is a tenth of a
120
+ * pixel, which is rasteriser noise rather than a grace margin — a deck that wants
121
+ * 40px declares 40px and measures 40.0, as the flat control does exactly.
122
+ */
123
+ export declare function gradeApparent(stops: readonly ApparentStop[], floor?: number): Finding[];
@@ -1,4 +1,5 @@
1
- import { type Beat, type Finding, type Storyboard, type Verdict } from "../types.js";
1
+ import type { Prefs } from "../prefs.js";
2
+ import { type Beat, type Finding, type Source, type Storyboard, type Verdict } from "../types.js";
2
3
  import { type CheckOptions } from "./check.js";
3
4
  /**
4
5
  * The duration budget reads the built composition, so it needs no argument the
@@ -52,9 +53,10 @@ export interface VerifyOptions extends CheckOptions {
52
53
  * the beat-level gates as well. `kept` — the cut `build` emitted — is optional
53
54
  * for the same reason, and for a second one: without it the budget gate falls
54
55
  * back to the flat threshold, which is the right list only while the budget has
55
- * cut nothing.
56
+ * cut nothing. `source` is optional on the same terms: it is what makes the
57
+ * unused-figure advisory possible, and a built directory does not carry it.
56
58
  */
57
- export declare function verify(dir: string, opts?: VerifyOptions, storyboard?: Storyboard, kept?: readonly Beat[]): Promise<Verdict>;
59
+ export declare function verify(dir: string, opts?: VerifyOptions, storyboard?: Storyboard, kept?: readonly Beat[], source?: Source): Promise<Verdict>;
58
60
  /**
59
61
  * Every mp3 the narration island promises is actually in the deck.
60
62
  *
@@ -144,6 +146,102 @@ export declare function scanHeadlines(storyboard: Storyboard): Finding[];
144
146
  * into two holds of one beat is a rewrite only the author can perform.
145
147
  */
146
148
  export declare function scanRepeatedObject(storyboard: Storyboard): Finding[];
149
+ /**
150
+ * Warn when the source carries a figure the deck never cites.
151
+ *
152
+ * NOTHING ELSE LOOKS IN THIS DIRECTION. `assertRefsResolve` checks the other one
153
+ * — that every id a beat writes exists in the source — so a plan that ignores
154
+ * the figures entirely satisfies it perfectly. Measured on the shipped demo: two
155
+ * of four figures cited, one of the two left out being the paper's own
156
+ * architecture figure, while a beat redrew that architecture as a synthetic
157
+ * pipeline. Every gate was green, and the owner found it by watching the video.
158
+ *
159
+ * A WARNING, NEVER AN ERROR. A source may honestly hold a figure this deck does
160
+ * not need — a decorative header, a figure about related work, a plot the deck
161
+ * replaces with bars of its own. What is wrong is not using none of them; it is
162
+ * not having decided.
163
+ *
164
+ * Cited counts BOTH ways a beat can be accountable to a figure: `evidence`, and
165
+ * the params that actually put it on screen. A beat that shows a figure without
166
+ * citing it in evidence is a different defect, and not this scan's to report.
167
+ */
168
+ export declare function scanUnusedFigures(storyboard: Storyboard, source: Source): Finding[];
169
+ /**
170
+ * Warn when the plan came back with fewer beats than were asked for.
171
+ *
172
+ * `--slides` IS A FLOOR ON THE REQUEST, NOT ON THE ARTIFACT, and this is the half
173
+ * that MEASURES it. It has no teeth, and none are available — the door is closed
174
+ * three ways, so do not reopen it. Truncating to the count that came back throws
175
+ * away slides the author asked for. Padding to reach the number is what RULE 9
176
+ * forbids, and `scanRepeatedObject` above only fires on the same object drawn
177
+ * twice with the SAME archetype over identical part labels, so padding induced by
178
+ * a gate would mostly be invisible to the one scan that looks for it. Re-asking is
179
+ * a fresh multi-minute `codex exec` whose only possible second instruction is
180
+ * "return more" — a padding request with extra steps — and whose answer could only
181
+ * be preferred over the first by counting it.
182
+ *
183
+ * THE COUNT IS NOT DETERMINISTIC — which is a weaker argument than it first looks,
184
+ * and is written out here so the next reader does not have to re-derive that.
185
+ * Under ONE fixed configuration, the same command over the same source returned
186
+ * 9, 10 and 8 beats on three independent runs (the "rate + prompt floor" row of
187
+ * the table in .planning/HANDOFF-DURATION-CONTROL.md). So the number a gate would
188
+ * test is partly a draw rather than a property of the source. But all three of
189
+ * those are short of twelve, and NO configuration on record has produced both a
190
+ * twelve and a short count: the 12-beat run in the same table is a different
191
+ * prompt and a different character budget. So this is not evidence that a gate
192
+ * would flap between pass and fail on identical input — it would have failed all
193
+ * three of those runs alike. It is a reason to distrust the number, not a fourth
194
+ * reason the door is closed. The three above are that, and they stand on their
195
+ * own.
196
+ *
197
+ * The other half is the prompt's LENGTH block, which now says so in words — and
198
+ * words are not enough here for the reason `scanHeadlines` records two functions
199
+ * up: a real Codex run answered a sharpened RULE 8 by swapping one verb. Four of
200
+ * the last five plans came back short of their target (8, 9, 9 and 10 against 12)
201
+ * against a prompt that already asked for the number. Whether a source "genuinely
202
+ * will not carry twelve points" is a judgement the writer makes about their own
203
+ * output, so it can always be met cosmetically. A COUNT is not a judgement, which
204
+ * is what makes it worth MEASURING here — and measuring is the whole of what this
205
+ * is.
206
+ *
207
+ * THE FLOOR THE OWNER ASKED FOR IS ALREADY HELD, and it is a different floor: he
208
+ * asked that a short duration not be paid for in slides — a 12-slide deck under a
209
+ * minute, "keeping all twelve". That one holds structurally now. `slidesFor` is a
210
+ * DEFAULT that an explicit `--slides` overrides rather than the reverse (see
211
+ * `loadPrefs`), `durationPlan` raises the speaking rate before it says less, and
212
+ * the advisory that used to answer sixty seconds with "use nine slides" no longer
213
+ * fires there at all — under `FF_BEAT_SECONDS` the rate is what moves, and where
214
+ * it still fires it names keeping every slide beside the smaller count. A floor
215
+ * against the PLANNER'S judgement is a different object, and it is the one that
216
+ * cannot be built. What is held in its place is honesty about it: the budget is
217
+ * struck at the count that came back, the gap is priced in the numbers that moved,
218
+ * and `pack` writes `prefs.slides` and the storyboard into the same file — so a
219
+ * caller that really does want "N or nothing" compares those two numbers itself.
220
+ *
221
+ * REPORTED, NEVER REPAIRED, which is the same shape as `cut.dangling`. None of the
222
+ * three repairs above is available, so the deck is built at the count it has —
223
+ * `durationPlan` restrikes the whole budget there, which is what stops the
224
+ * shortfall becoming a video that quietly misses its duration — and the author
225
+ * is told what it cost, at `plan`, before a minute of TTS is spent on it.
226
+ *
227
+ * NAMING THE NUMBERS IS THE WHOLE VALUE, same argument as `INSTEAD` above: "the
228
+ * plan is short" changes nothing, "each surviving slide now runs 7.5s instead of
229
+ * 5.0s and has to carry 102 characters where the prompt budgeted 66" is a fact
230
+ * the author can act on — by adding the missing beat, or by accepting the deck
231
+ * they have.
232
+ *
233
+ * A WARNING, NEVER AN ERROR, and that is a contract rather than an oversight. A
234
+ * source that honestly carries eight points is a real thing, and only the author
235
+ * can tell that from a planner that stopped early. `build` prints every content
236
+ * loss and fails on none of them: a beat the emitter refused (`onBeatError`), a
237
+ * beat the budget cut (`reportCut`), a beat the planner never wrote (this one).
238
+ * A beat written and then thrown away is a bigger loss than one never written, so
239
+ * making the smallest of the three fatal while the larger two stay advisory would
240
+ * be incoherent — promoting this to `severity: "error"` is a decision about all
241
+ * three, not about this scan. test/verify.test.ts asserts the severity so that
242
+ * decision cannot be made by accident.
243
+ */
244
+ export declare function scanBeatCount(storyboard: Storyboard, prefs: Prefs): Finding[];
147
245
  /**
148
246
  * How far a name may precede the thing it names before a viewer notices.
149
247
  *
@@ -174,9 +272,19 @@ export declare const LEAD_SECONDS = 1;
174
272
  * CONSERVATIVE BY CONSTRUCTION, in three ways, because a warning that cries wolf
175
273
  * is a warning people learn to scroll past:
176
274
  * - a part is assumed to appear at the EARLIEST hold that could be its own,
177
- * `holds[min(j, last)]`. Where an archetype spends its first hold on a
178
- * landing rather than a part, the real appearance is later than this and the
179
- * finding is missed rather than invented.
275
+ * `holds[j]`. Where an archetype spends its first hold on a landing rather
276
+ * than a part, the real appearance is later than this and the finding is
277
+ * missed rather than invented.
278
+ *
279
+ * THIS PARAGRAPH USED TO SAY `holds[min(j, last)]`, AND IT WAS WRONG IN THE
280
+ * ONE DIRECTION A DOCSTRING MUST NOT BE WRONG IN. Clamping to the last hold
281
+ * does not under-report, it OVER-reports: an archetype that draws five bars
282
+ * on two holds charges bars three through five to the final hold, so a word
283
+ * spoken over a bar that is already on screen is read as a word spoken 1.05
284
+ * to 2.25s early, against a 1.0s threshold. 86 of `bar-compare`'s 90 flagged
285
+ * parts were that arithmetic and not a defect — 43% of every flagged part in
286
+ * the committed corpus. The promise of under-reporting is exactly why nobody
287
+ * checked. The scene is now skipped instead of clamped, see below.
180
288
  * - only labels the narration actually names are considered, by the same
181
289
  * five-character prefix match `scanHeadlines` uses — tuned on exactly this
182
290
  * problem, where the headline said "encoding" and the stage was `Encoder`.
@@ -28,7 +28,9 @@
28
28
  *
29
29
  * WHAT IT DOES NOT SEE, stated because a gate that overclaims is the thing this
30
30
  * file was written to stop. It reads declared sizes, so text shrunk by a
31
- * `scale` below 1 at a hold reads as its unscaled size; and it says nothing about
31
+ * `scale` below 1 at a hold reads as its unscaled size `apparent.ts` is the
32
+ * gate that closes exactly that, by measuring the painted glyph instead, and it
33
+ * catches a 3D projection the same way; and it says nothing about
32
34
  * whether the text FITS — `container_overflow` and `text_occluded` in
33
35
  * `hyperframes check` are the gates for that, and `check.ts` already grades them
34
36
  * up to errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,7 +63,7 @@
63
63
  "commander": "^14.0.1",
64
64
  "fflate": "^0.8.3",
65
65
  "gsap": "^3.14.2",
66
- "hyperframes": "0.7.71",
66
+ "hyperframes": "0.8.27",
67
67
  "katex": "^0.16.11",
68
68
  "puppeteer-core": "^25.3.0",
69
69
  "remark-gfm": "^4.0.1",