@jokerized/decksmith 0.7.0 → 0.8.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.
@@ -1,2 +1,42 @@
1
+ /**
2
+ * An equation becoming the next one.
3
+ *
4
+ * The keyed morph — the 3Blue1Brown move — and the one thing in the vocabulary
5
+ * that shows a DERIVATION rather than a statement: the terms the author keys
6
+ * travel from where they sit in the first line to where they sit in the second,
7
+ * as rigid bodies, and everything else dissolves. Which terms are keyed is the
8
+ * animation: `a^2+b^2=c^2` to `c^2-b^2=a^2` is a glyph riot unkeyed and a
9
+ * three-body exchange keyed, on the same two strings.
10
+ *
11
+ * The keying reuses `equation-walk`'s: a term is located in the TeX by normal
12
+ * form and wrapped in `\htmlClass{term t-<tone> ds-k-<tone>}{...}`, so KaTeX
13
+ * emits a real element the runtime can find. It must be found in BOTH lines
14
+ * to be a key at all — a term on one side only is dropped, with its legend
15
+ * row, exactly as the walk drops a term it cannot place.
16
+ *
17
+ * Geometry is measured in the browser, after fonts (Seam B), and the morph
18
+ * reaches the timeline as ONE typed `fromTo` on the host driving a GSAP plugin.
19
+ * See `src/emit/morph-runtime.ts` for why that, and not a callback.
20
+ */
21
+ import type { BeatOf, Source, Term } from "../../types.js";
1
22
  import type { Emitter } from "../kit.js";
23
+ /**
24
+ * Both lines and their keys, decided in Node — the morph's half of what
25
+ * `prepareWalk` is to the walk, so `plan` can report what `build` will do.
26
+ * Throws where the emitter would: a line KaTeX refuses even repaired, or no key.
27
+ */
28
+ export declare function prepareMorph(beat: BeatOf<"equation-morph">, source: Source): {
29
+ a: {
30
+ id: string;
31
+ tex: string;
32
+ };
33
+ b: {
34
+ id: string;
35
+ tex: string;
36
+ };
37
+ wa: string;
38
+ wb: string;
39
+ both: Term[];
40
+ notes: string[];
41
+ };
2
42
  export declare const equationMorph: Emitter<"equation-morph">;
@@ -10,7 +10,14 @@ import type { Term } from "../../types.js";
10
10
  import type { Emitter, Theme } from "../kit.js";
11
11
  export declare const OPTS = "{ displayMode: true, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
12
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. */
13
+ /**
14
+ * Where `term` sits in `tex`, comparing normal forms. Null when it is absent.
15
+ *
16
+ * The span starts at the first matched character's token — with the `\left`
17
+ * that sizes it, when the term opens on a sized delimiter — and ends where the
18
+ * last matched character's token ends. Never further: whatever follows the term
19
+ * in the equation, a `\right` included, is not the term's.
20
+ */
14
21
  export declare function locate(tex: string, term: string): {
15
22
  start: number;
16
23
  end: number;
@@ -36,7 +43,47 @@ cls?: (t: Term) => string): {
36
43
  tex: string;
37
44
  used: Term[];
38
45
  missing: Term[];
46
+ broken: Term[];
39
47
  };
48
+ /** What `prepareWalk` decided about one equation and its terms. */
49
+ export interface PreparedWalk {
50
+ /** The TeX to render, repaired and term-wrapped. Absent when `plain`. */
51
+ tex?: string;
52
+ /** The repaired TeX without wrappers, for measuring. The source TeX when `plain`. */
53
+ raw: string;
54
+ /** Set when KaTeX refuses the equation even after repair. */
55
+ plain?: {
56
+ error: string;
57
+ };
58
+ used: Term[];
59
+ missing: Term[];
60
+ notes: string[];
61
+ }
62
+ /**
63
+ * Everything the walk needs to draw one equation, decided in Node — so `plan`
64
+ * can report it and `build` cannot ship a `katex.render` call that throws.
65
+ *
66
+ * The equation is parsed and, if KaTeX refuses it, repaired (`repairTex`). Each
67
+ * term is repaired the same way, so a term spelled with the paper's own macro
68
+ * still finds it once both are drawn as names. Then the terms are wrapped, each
69
+ * wrapping parsed before it is kept.
70
+ *
71
+ * An equation no repair makes parseable is DEGRADED, not dropped and not
72
+ * shipped: `plain` is set and the slide shows the TeX source as text, with the
73
+ * terms still marked so the walk still walks. `notes` says, in words, every
74
+ * repair, degradation and dropped term — `plan` prints them and `build` reports
75
+ * them as the beat's warnings.
76
+ */
77
+ export declare function prepareWalk(eq: {
78
+ id: string;
79
+ tex: string;
80
+ }, terms: readonly Term[], beatId: string): PreparedWalk;
81
+ /**
82
+ * A legend chip's contents: the term's TeX for KaTeX when it parses inline
83
+ * (repaired if it had to be), and its source as plain text when it does not —
84
+ * a chip is where a viewer reads WHICH symbol, so it is never left empty.
85
+ */
86
+ export declare function chipSetup(sid: string, term: Term): string;
40
87
  /**
41
88
  * Display equations want to live between 68px and 108px.
42
89
  *
@@ -58,6 +105,18 @@ cls?: (t: Term) => string): {
58
105
  export declare function equationSize(tex: string): number;
59
106
  /** Estimated rendered width of a display, in ems. */
60
107
  export declare function texUnits(tex: string): number;
108
+ /**
109
+ * Grow each legend chip to hold the glyphs KaTeX set in it.
110
+ *
111
+ * A chip is a painted box with 2px of vertical padding, and the layout gate
112
+ * measures text against the nearest painted box. `x^{\text{gt}}_{<i}` or a
113
+ * `\frac` sets glyph boxes above and below the line box, so the chip failed
114
+ * `text_box_overflow` on its own superscript — measured on HypePaper deck
115
+ * 0ef77cae: the `gt` sat 10px above the chip's top edge. Padding is added only
116
+ * where a glyph box actually crosses an edge, so a chip that already held its
117
+ * term is untouched.
118
+ */
119
+ export declare function chipFit(sid: string, terms: Term[]): string[];
61
120
  /** One row per term: a chip KaTeX fills in `setup`, and the label. Shared with the morph. */
62
121
  export declare function legendRows(sid: string, terms: Term[], theme: Theme): string;
63
122
  export declare function legendCss(theme: Theme): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The attribute `equation-walk`'s fit leaves on a display it could not fit,
3
+ * holding the formula. `fidelity` reads it and fails the deck naming that
4
+ * formula — the one statement the layout gate's `span.mord` cannot make.
5
+ */
6
+ export declare const UNFIT_ATTR = "data-ds-unfit";
7
+ /** KaTeX's message for `tex`, or null when it parses. Same options the deck uses. */
8
+ export declare function texError(tex: string, display: boolean): string | null;
9
+ export interface Repaired {
10
+ /** The TeX to render: the input when it parsed, otherwise the repaired form. */
11
+ tex: string;
12
+ /** One sentence per repair made, in the order made. Empty when none was needed. */
13
+ repairs: string[];
14
+ /** KaTeX's message when even the repaired form does not parse; null otherwise. */
15
+ error: string | null;
16
+ }
17
+ /**
18
+ * Parse `tex`; if KaTeX refuses it, apply the repairs that cannot change its
19
+ * meaning, one at a time, until it parses or none is left to try.
20
+ *
21
+ * THE REPAIRS, and why each is safe:
22
+ *
23
+ * - An UNBALANCED `\left`/`\right` is closed with the null delimiter `\right.`
24
+ * (or opened with `\left.`). The delimiter the author wrote is still drawn,
25
+ * at the size it was asked for; only the missing invisible partner is added.
26
+ * - An UNBALANCED brace group is closed at the end, or opened at the start.
27
+ * - `\label`, `\nonumber`, `\notag` are removed (see `NO_INK`).
28
+ * - An UNDEFINED macro — a paper's own `\newcommand`, which the source never
29
+ * carries — is drawn as its NAME in upright type: `\raydir` becomes
30
+ * `\operatorname{raydir}`, an operator rather than `\mathrm` so that two in a
31
+ * row keep a space between them (`\camerarot \cameraint` set as `\mathrm`
32
+ * read "camerarotcameraint"). A degradation, not a restoration, and the sentence
33
+ * it returns says so; but it keeps the symbol on the slide under the name the
34
+ * paper gave it, where the alternatives are to drop the symbol or the slide.
35
+ *
36
+ * Deterministic: the same input always produces the same output and sentences,
37
+ * which is what lets `plan` report a repair that `build` will then make.
38
+ */
39
+ export declare function repairTex(input: string, display: boolean): Repaired;
@@ -123,8 +123,11 @@ export { verify } from "./verify/index.js";
123
123
  * `scanBeatCount` also fold into `verify`; that one needs the preferences the
124
124
  * deck was asked for, which a built directory does not carry — and
125
125
  * `scanUnusedFigures` folds in only where the source was passed alongside.
126
+ * `scanMath` does not fold into `verify`: by then the build has already said
127
+ * the same thing through `onBeatWarning`, and saying it twice trains people to
128
+ * stop reading it.
126
129
  */
127
- export { scanBeatCount, scanNarrationDrift, scanPaperArc, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
130
+ export { scanBeatCount, scanMath, scanNarrationDrift, scanPaperArc, scanHeadlines, scanNarrationLead, scanRepeatedObject, scanUnusedFigures, } from "./verify/index.js";
128
131
  export { check, parseCheckReport, sampleTimes } from "./verify/check.js";
129
132
  export type { CheckOptions } from "./verify/check.js";
130
133
  /**
@@ -1,15 +1,3 @@
1
- /**
2
- * What the model is told, and what it is told about.
3
- *
4
- * Planning quality is the product, and this file is where it is won or lost —
5
- * the schema only guarantees the storyboard is well-formed, never that it is a
6
- * good explanation of this particular document.
7
- *
8
- * `renderSource` deliberately puts every citable id next to its caption. The
9
- * model cannot cite an id it was never shown, so the inventory is the cheapest
10
- * defence against the failure mode that actually happens: a plausible-looking
11
- * `figureId` that exists in no source.
12
- */
13
1
  import type { Prefs } from "../prefs.js";
14
2
  import { type Source } from "../types.js";
15
3
  /**
@@ -66,6 +66,30 @@ export interface ResolvedChrome {
66
66
  export declare function resolveChrome(need?: string): Promise<ResolvedChrome>;
67
67
  /** The path alone, for the callers that only ever wanted that. */
68
68
  export declare function chromePath(need?: string): Promise<string>;
69
+ /**
70
+ * Whether Chrome died for want of a sandbox. Stock Ubuntu 24.04, GitHub's
71
+ * `ubuntu-latest` included, sets `kernel.apparmor_restrict_unprivileged_userns=1`.
72
+ * Chrome's sandbox needs exactly those namespaces, so it exits at launch with
73
+ * `FATAL:zygote_host_impl_linux.cc(…)] No usable sandbox!`, which puppeteer
74
+ * quotes in the error it throws.
75
+ */
76
+ export declare function sandboxMissing(err: unknown): boolean;
77
+ /** How a person gets the sandbox back. */
78
+ export declare const SANDBOX_FIX = "`sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0`, or give Chrome an AppArmor profile";
79
+ /**
80
+ * Launch Chrome for a page DeckSmith wrote itself: a built deck, a caption band.
81
+ *
82
+ * On a host with no usable sandbox (`sandboxMissing`) every gate that opens the
83
+ * deck reported `not_measured`, a warning, so `build` PASSED having measured
84
+ * nothing (.planning/2026-09-18-linux-determinism.md §4). The sandbox stays on
85
+ * wherever Chrome can have one. Only when Chrome refuses to start without it is
86
+ * the launch retried with `--no-sandbox`, and stderr says so. That is no wider
87
+ * than the render already is: hyperframes launches its own capture with
88
+ * `--no-sandbox` every time, on this same deck.
89
+ *
90
+ * Pages from the web never come here: `harvest` refuses instead.
91
+ */
92
+ export declare function launchOwnPage<T>(launch: (args: string[]) => Promise<T>, args: string[], what: string): Promise<T>;
69
93
  type Browser = Awaited<ReturnType<typeof import("puppeteer-core").launch>>;
70
94
  type Page = Awaited<ReturnType<Browser["newPage"]>>;
71
95
  /** An open deck, seekable and photographable. Close it. */
@@ -111,6 +111,22 @@ export interface Timing {
111
111
  * written without it, and those decks did reserve nothing.
112
112
  */
113
113
  captionReserve?: number;
114
+ /**
115
+ * The format the deck was built for: its id and its duration budget.
116
+ *
117
+ * The composition records only pixels, and `verify` used to find the budget
118
+ * by looking those up among the presets. A custom canvas matches none, so
119
+ * every custom build read "no duration budget applies" — true of the pixels
120
+ * and false of the deck, which inherits the budget of the profile it was
121
+ * resized from (`resizeFormat`). Optional, and absent falls back to the pixel
122
+ * lookup: every `timing.json` written before this lacks it.
123
+ */
124
+ format?: {
125
+ id: string;
126
+ minWeight: number;
127
+ maxSeconds?: number;
128
+ warnSeconds?: number;
129
+ };
114
130
  scenes: TimedScene[];
115
131
  segments: TimedSegment[];
116
132
  }
@@ -26,6 +26,13 @@ export declare function readCanvas(html: string): Canvas | undefined;
26
26
  * be its own is a gate people learn to ignore. Today the two 16:9 profiles have
27
27
  * the same (absent) budget, so the reading is exact as well as safe.
28
28
  */
29
+ /** What the budget gate reads off a format, whether a preset or the one `build` recorded. */
30
+ export type BudgetFormat = Pick<Format, "id" | "minWeight" | "maxSeconds" | "warnSeconds">;
31
+ /** `timing.json`'s record of the format, with the canvas it was recorded for. */
32
+ export interface RecordedFormat extends BudgetFormat {
33
+ width: number;
34
+ height: number;
35
+ }
29
36
  export declare function profilesFor(width: number, height: number, formats?: Readonly<Record<string, Format>>): Format[];
30
37
  /**
31
38
  * The budget finding for one built composition, if there is one.
@@ -46,4 +53,4 @@ export declare function profilesFor(width: number, height: number, formats?: Rea
46
53
  * a table that disagrees, rather than the rule being deleted and silently absent
47
54
  * the next time a real destination is tighter than the rest.
48
55
  */
49
- export declare function scanBudget(html: string, storyboard?: Storyboard, kept?: readonly Beat[], formats?: Readonly<Record<string, Format>>): Finding[];
56
+ export declare function scanBudget(html: string, storyboard?: Storyboard, kept?: readonly Beat[], formats?: Readonly<Record<string, Format>>, recorded?: RecordedFormat): Finding[];
@@ -291,6 +291,25 @@ export declare function gradeFidelity(rows: readonly Measured[], floor?: number)
291
291
  * thing it was told to deliver, not a note about taste.
292
292
  */
293
293
  export declare function gradeReserve(rows: readonly Measured[], tolerance?: number): Finding[];
294
+ /** An equation the deck's own fit gave up on: the scene, and the formula. */
295
+ export interface Unfit {
296
+ sid: string;
297
+ tex: string;
298
+ }
299
+ /**
300
+ * Run IN THE PAGE: every display `equation-walk`'s fit marked as not fitting.
301
+ *
302
+ * Read once, after the ready gate — the fit runs inside it, before any frame,
303
+ * so the marks are the same at every time the deck could be seeked to.
304
+ */
305
+ export declare function collectUnfit(attr: string): Unfit[];
306
+ /**
307
+ * An error per equation that does not fit its box at the type floor even broken
308
+ * across lines, NAMING THE FORMULA. The layout gate sees the same overflow and
309
+ * can only say `span.mord`, which tells nobody which of a deck's equations to
310
+ * shorten.
311
+ */
312
+ export declare function gradeUnfit(rows: readonly Unfit[]): Finding[];
294
313
  /**
295
314
  * Seek every declared stop and count the ink below its caption.
296
315
  *
@@ -171,6 +171,23 @@ export declare function scanRepeatedObject(storyboard: Storyboard): Finding[];
171
171
  * citing it in evidence is a different defect, and not this scan's to report.
172
172
  */
173
173
  export declare function scanUnusedFigures(storyboard: Storyboard, source: Source): Finding[];
174
+ /**
175
+ * Every equation a walk or a morph will draw, parsed now by the KaTeX the deck
176
+ * vendors, with what `build` will do about each one said in words.
177
+ *
178
+ * Run at `plan` because that is when the storyboard is in front of the author
179
+ * and the cheapest moment to change a beat. Before this, a bad formula was first
180
+ * parsed by a browser at `verify`, after a deck had been built and narrated
181
+ * around it: 26 of 92 HypePaper build failures in the week to 2026-10-07.
182
+ *
183
+ * It calls the SAME preparation the emitters call (`prepareWalk`,
184
+ * `prepareMorph`), so what it reports is what `build` will do, not a second
185
+ * opinion that can drift from it. A WARNING throughout: a repaired formula, a
186
+ * formula shown as plain source and a dropped term are all drawn without
187
+ * throwing — and a beat that will be left out is reported again, by name, when
188
+ * `build` leaves it out.
189
+ */
190
+ export declare function scanMath(storyboard: Storyboard, source: Source): Finding[];
174
191
  /**
175
192
  * Warn when the plan came back with fewer beats than were asked for.
176
193
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",