@jokerized/decksmith 0.3.1 → 0.4.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.
- package/README.md +208 -15
- package/dist/cli.js +3125 -742
- package/dist/deck-player-element.js +1 -0
- package/dist/deck-player.js +1 -0
- package/dist/deck-runtime.js +27 -2
- package/dist/embed.html +243 -0
- package/dist/index.js +3211 -886
- package/dist/mcp.js +3189 -844
- package/dist/types/deck/player-element.d.ts +1 -0
- package/dist/types/deck/player.d.ts +37 -0
- package/dist/types/deck/protocol.d.ts +80 -0
- package/dist/types/deck/runtime.d.ts +163 -0
- package/dist/types/emit/archetypes/claim-figure.d.ts +0 -7
- package/dist/types/emit/camera.d.ts +14 -6
- package/dist/types/emit/composition.d.ts +13 -0
- package/dist/types/emit/kit.d.ts +66 -8
- package/dist/types/emit/svg.d.ts +27 -0
- package/dist/types/images/providers.d.ts +14 -5
- package/dist/types/index.d.ts +32 -0
- package/dist/types/mcp/tools.d.ts +20 -0
- package/dist/types/net/fetch.d.ts +81 -0
- package/dist/types/pack/media.d.ts +12 -0
- package/dist/types/plan/prompt.d.ts +2 -1
- package/dist/types/server/pipeline.d.ts +51 -8
- package/dist/types/server/upload.d.ts +29 -5
- package/dist/types/source/assets.d.ts +77 -4
- package/dist/types/source/harvest.d.ts +250 -0
- package/dist/types/source/readability.d.ts +120 -0
- package/dist/types/source/transcode.d.ts +83 -0
- package/dist/types/tmpdir.d.ts +33 -0
- package/dist/types/types.d.ts +49 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** One stop, as a host sees it. */
|
|
2
|
+
export interface PlayerStop {
|
|
3
|
+
i: number;
|
|
4
|
+
label: string;
|
|
5
|
+
notes?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class DecksmithPlayer extends HTMLElement {
|
|
8
|
+
#private;
|
|
9
|
+
static observedAttributes: string[];
|
|
10
|
+
/** Every stop the deck reported, empty until `ds-ready`. */
|
|
11
|
+
get stops(): PlayerStop[];
|
|
12
|
+
/** Where the deck is now. */
|
|
13
|
+
get at(): number;
|
|
14
|
+
/** Whether the deck answered the handshake. */
|
|
15
|
+
get connected(): boolean;
|
|
16
|
+
connectedCallback(): void;
|
|
17
|
+
disconnectedCallback(): void;
|
|
18
|
+
attributeChangedCallback(name: string, was: string | null, now: string | null): void;
|
|
19
|
+
next(): void;
|
|
20
|
+
prev(): void;
|
|
21
|
+
go(at: number): void;
|
|
22
|
+
play(on?: boolean): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register the element, once.
|
|
26
|
+
*
|
|
27
|
+
* GUARDED AND PARAMETERISED because `customElements.define` throws on a
|
|
28
|
+
* duplicate name, and a host page that imports this twice — two bundlers, or a
|
|
29
|
+
* hot reload — should get a no-op rather than an exception that takes the page
|
|
30
|
+
* down. The tag is an argument so a consumer whose page already owns
|
|
31
|
+
* `decksmith-player` can mount it under another name.
|
|
32
|
+
*
|
|
33
|
+
* NOT called at import time. A module with a side effect cannot be imported for
|
|
34
|
+
* its types, and `player-element.ts` exists precisely so the side-effecting
|
|
35
|
+
* entry point is a separate file a consumer opts into.
|
|
36
|
+
*/
|
|
37
|
+
export declare function define(tag?: string): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host page and a deck say to each other.
|
|
3
|
+
*
|
|
4
|
+
* ONE FILE, IMPORTED BY BOTH ENDS, so `tsc` checks the sender against the
|
|
5
|
+
* receiver. The alternative — a string literal in the runtime and a matching one
|
|
6
|
+
* in the element — is two copies of a contract that drift the first time a field
|
|
7
|
+
* is added, and the drift is invisible because `postMessage` takes `any` and a
|
|
8
|
+
* message nobody understands is simply ignored.
|
|
9
|
+
*
|
|
10
|
+
* WHY A BRIDGE AT ALL, rather than hosting the deck in the page. A deck is a
|
|
11
|
+
* whole document with its own fonts, GSAP, KaTeX, a vendored player and a ready
|
|
12
|
+
* gate, and `frameOf` in runtime.ts can only reach a SAME-ORIGIN frame — it
|
|
13
|
+
* returns null otherwise and the runtime merely warns, so a deck served from
|
|
14
|
+
* another origin would navigate perfectly and paint nothing, silently. Keeping
|
|
15
|
+
* the frame keeps that same-origin pair inside one directory, where it is always
|
|
16
|
+
* true, and makes the host link `postMessage`, which does not care about origin.
|
|
17
|
+
*
|
|
18
|
+
* VERSION COUPLING IS THE HAZARD. Every deck published before this existed has
|
|
19
|
+
* no listener, and a deck is a static artifact that outlives the tool that made
|
|
20
|
+
* it. So silence is a SUPPORTED state, not an error: the element times out, says
|
|
21
|
+
* so with a distinct code, and leaves the deck usable — it is still a deck in a
|
|
22
|
+
* frame, and its own keyboard still works.
|
|
23
|
+
*/
|
|
24
|
+
/** The channel name, on every message in both directions. */
|
|
25
|
+
export declare const CHANNEL = "decksmith-deck";
|
|
26
|
+
/** Sent by the host once the frame has loaded, to open the conversation. */
|
|
27
|
+
export interface Hello {
|
|
28
|
+
channel: typeof CHANNEL;
|
|
29
|
+
type: "hello";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The deck's answer, and the only message that establishes the host's origin.
|
|
33
|
+
*
|
|
34
|
+
* NOTHING IS POSTED TO "*" AFTER THIS. A stop carries the slide's speaker
|
|
35
|
+
* notes, and any page on the internet can put a deck in a frame — posting
|
|
36
|
+
* wildcard would hand an arbitrary framer the presenter's notes. The origin is
|
|
37
|
+
* taken from the `hello` event and every later message is addressed to it.
|
|
38
|
+
*/
|
|
39
|
+
export interface Ready {
|
|
40
|
+
channel: typeof CHANNEL;
|
|
41
|
+
type: "ready";
|
|
42
|
+
/** Every stop the deck can land on, in order. */
|
|
43
|
+
stops: {
|
|
44
|
+
i: number;
|
|
45
|
+
label: string;
|
|
46
|
+
notes?: string;
|
|
47
|
+
}[];
|
|
48
|
+
/** Where it is now. */
|
|
49
|
+
at: number;
|
|
50
|
+
}
|
|
51
|
+
/** Where the deck landed, after any move. */
|
|
52
|
+
export interface Stopped {
|
|
53
|
+
channel: typeof CHANNEL;
|
|
54
|
+
type: "stop";
|
|
55
|
+
at: number;
|
|
56
|
+
total: number;
|
|
57
|
+
label: string;
|
|
58
|
+
notes?: string;
|
|
59
|
+
playing: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** What a host may ask for. Deliberately small: everything else is a deck concern. */
|
|
62
|
+
export type Command = {
|
|
63
|
+
channel: typeof CHANNEL;
|
|
64
|
+
type: "next";
|
|
65
|
+
} | {
|
|
66
|
+
channel: typeof CHANNEL;
|
|
67
|
+
type: "prev";
|
|
68
|
+
} | {
|
|
69
|
+
channel: typeof CHANNEL;
|
|
70
|
+
type: "go";
|
|
71
|
+
at: number;
|
|
72
|
+
} | {
|
|
73
|
+
channel: typeof CHANNEL;
|
|
74
|
+
type: "play";
|
|
75
|
+
on: boolean;
|
|
76
|
+
};
|
|
77
|
+
export type FromDeck = Ready | Stopped;
|
|
78
|
+
export type ToDeck = Hello | Command;
|
|
79
|
+
/** A message from us, rather than from anything else sharing the window. */
|
|
80
|
+
export declare function isOurs(data: unknown): data is FromDeck | ToDeck;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Narration } from "./subtitles.js";
|
|
1
2
|
/** One entry of the slideshow island's `slides` array. */
|
|
2
3
|
export interface SlideSpec {
|
|
3
4
|
sceneId: string;
|
|
@@ -49,6 +50,32 @@ export interface TransitionPlan {
|
|
|
49
50
|
export declare function planTransition(fromT: number, toT: number, opts?: {
|
|
50
51
|
reducedMotion?: boolean;
|
|
51
52
|
}): TransitionPlan;
|
|
53
|
+
/**
|
|
54
|
+
* How long autoplay waits on the stop it is on, or `null` for "do not set a
|
|
55
|
+
* timer". Pure, for the same reason `planTransition` and `refused` are: the
|
|
56
|
+
* decision has two callers and lives inside `start`, which no test in this
|
|
57
|
+
* project can reach.
|
|
58
|
+
*
|
|
59
|
+
* `heard` is the whole point of the split. A narrated stop is timed by its own
|
|
60
|
+
* audio — speech drives the deck, which is this project's entire timing model —
|
|
61
|
+
* so it waits for `ended` and wants no timer. But `voice.at` answers
|
|
62
|
+
* synchronously about whether a segment EXISTS, while `play()` rejects a beat
|
|
63
|
+
* later, so at arrival a stop whose file is missing is indistinguishable from a
|
|
64
|
+
* working one. Ask again when `play()` settles and the answer can flip either
|
|
65
|
+
* way: to `false`, and the stop needs the clock after all or autoplay waits
|
|
66
|
+
* forever for an `ended` that a source which never loaded cannot fire; or back
|
|
67
|
+
* to `true`, when a gesture retried the segment and it is speaking now, and the
|
|
68
|
+
* timer armed while it was silent would cut the sentence it just started.
|
|
69
|
+
*
|
|
70
|
+
* `gapMs` is the gap the author left before the next stop, the same number the
|
|
71
|
+
* linear render used, clamped so neither a back-to-back pair nor a long hold
|
|
72
|
+
* turns into a bad wait.
|
|
73
|
+
*/
|
|
74
|
+
export declare function dwellMs(opts: {
|
|
75
|
+
playing: boolean;
|
|
76
|
+
heard: boolean;
|
|
77
|
+
gapMs: number;
|
|
78
|
+
}): number | null;
|
|
52
79
|
/** `#3` is slide 3; `#3.2` is slide 3, second fragment. Both 1-based. */
|
|
53
80
|
export declare function formatHash(pos: Pos): string;
|
|
54
81
|
export declare function parseHash(hash: string): Pos | null;
|
|
@@ -57,3 +84,139 @@ export declare function parseHash(hash: string): Pos | null;
|
|
|
57
84
|
* longer emit falls back to its slide rather than to nothing.
|
|
58
85
|
*/
|
|
59
86
|
export declare function findStop(stops: readonly Stop[], pos: Pos): number;
|
|
87
|
+
interface Player extends HTMLElement {
|
|
88
|
+
ready?: boolean;
|
|
89
|
+
seek: (t: number) => void;
|
|
90
|
+
pause?: () => void;
|
|
91
|
+
}
|
|
92
|
+
/** Only what we call on a GSAP timeline. */
|
|
93
|
+
interface Seekable {
|
|
94
|
+
seek: (t: number) => void;
|
|
95
|
+
}
|
|
96
|
+
interface Frame {
|
|
97
|
+
doc: Document;
|
|
98
|
+
timelines: Record<string, Seekable>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The composition, reached through the player's iframe.
|
|
102
|
+
*
|
|
103
|
+
* Same-origin only, which means a deck must be served over http — opening
|
|
104
|
+
* `deck.html` from the filesystem gives the iframe an opaque origin and we
|
|
105
|
+
* cannot drive it. `present()` says so out loud rather than rendering blank.
|
|
106
|
+
*/
|
|
107
|
+
export declare function frameOf(player: Player): Frame | null;
|
|
108
|
+
export interface Voice {
|
|
109
|
+
/**
|
|
110
|
+
* Arrive at a stop: cut whatever was speaking, start this stop from zero.
|
|
111
|
+
* Returns whether this stop has anything to say — autoplay needs to know,
|
|
112
|
+
* because a silent stop has no `ended` event to wait for and would otherwise
|
|
113
|
+
* be where playback quietly stops forever.
|
|
114
|
+
*/
|
|
115
|
+
at: (stop: Stop) => boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Stop talking without arriving anywhere. The deck has ONE audio track and
|
|
118
|
+
* `claim-figure` already spends it on narration — see its `muted` note — so
|
|
119
|
+
* anything else that wants to make a sound has to take it first.
|
|
120
|
+
*/
|
|
121
|
+
hush: () => void;
|
|
122
|
+
toggleMute: () => void;
|
|
123
|
+
toggleSubtitles: () => void;
|
|
124
|
+
/** Called when the segment for the CURRENT stop finishes of its own accord. */
|
|
125
|
+
onEnded: (fn: () => void) => void;
|
|
126
|
+
/**
|
|
127
|
+
* Called when the CURRENT stop's `play()` settles, with whether there is going
|
|
128
|
+
* to be a sound. Autoplay needs it because `at` answers one beat too early:
|
|
129
|
+
* `at` says a segment EXISTS, and `play()` says whether it can be heard.
|
|
130
|
+
*
|
|
131
|
+
* BOTH answers matter. False means no `ended` will ever fire, so a deck
|
|
132
|
+
* playing itself must fall back to its own clock or sit on that slide forever
|
|
133
|
+
* with the button lit. True is how it gives that clock back — a segment
|
|
134
|
+
* retried after a dropped connection is speaking now, and a timer armed while
|
|
135
|
+
* it was silent would cut the sentence it just started.
|
|
136
|
+
*/
|
|
137
|
+
onSettled: (fn: (heard: boolean) => void) => void;
|
|
138
|
+
/**
|
|
139
|
+
* A real gesture: retry the segment for the stop we are on. Covers the
|
|
140
|
+
* autoplay policy, which is what it was written for, and a file that failed to
|
|
141
|
+
* load — see `refused` for why the second one is never retried unasked.
|
|
142
|
+
*/
|
|
143
|
+
unlock: () => void;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Was that `play()` rejection the autoplay policy, or an audio file we will
|
|
147
|
+
* never be able to play? Pure, so the classification is testable without a
|
|
148
|
+
* browser; the element that produces the rejection is not.
|
|
149
|
+
*
|
|
150
|
+
* The two need telling apart because they are not the same failure and must not
|
|
151
|
+
* be reported as one. The policy is recoverable by construction: the first
|
|
152
|
+
* gesture retries the segment and it speaks, so "press any key for sound" is
|
|
153
|
+
* simply true. A file that did not load MIGHT be — a two-second wifi drop and a
|
|
154
|
+
* one-off 503 look exactly like this from here — but it is just as likely a deck
|
|
155
|
+
* whose audio directory never got copied, and telling THAT viewer to press a key
|
|
156
|
+
* names the wrong culprit and goes on being wrong every time they try.
|
|
157
|
+
*
|
|
158
|
+
* So this split decides what the strip says, and whether anything retries
|
|
159
|
+
* unasked. It does NOT decide whether a person may retry: a gesture re-arms
|
|
160
|
+
* either failure (see `unlock`), because someone asking is not the same as us
|
|
161
|
+
* guessing. And no gate here can see any of it — nothing in the suite opens
|
|
162
|
+
* deck.html or plays a sound.
|
|
163
|
+
*
|
|
164
|
+
* `NotSupportedError` is the unplayable one — observed rather than taken from
|
|
165
|
+
* the spec: a source that cannot be fetched at all rejects `play()` with
|
|
166
|
+
* `NotSupportedError` and leaves `audio.error.code` at 4
|
|
167
|
+
* (`MEDIA_ERR_SRC_NOT_SUPPORTED`), and so does one that is fetched but is not
|
|
168
|
+
* media.
|
|
169
|
+
*
|
|
170
|
+
* Be honest about what that is worth. It is ONE run, on this machine, in a
|
|
171
|
+
* headless browser whose engine and version nobody wrote down, and that run
|
|
172
|
+
* could not show the other side of the split at all: the build played an
|
|
173
|
+
* ungestured sound even under
|
|
174
|
+
* `--autoplay-policy=document-user-activation-required`, so no `NotAllowedError`
|
|
175
|
+
* ever arrived to compare against. Nothing here has watched the two failures
|
|
176
|
+
* come out of the same browser. Treat "a missing segment reads as missing on the
|
|
177
|
+
* first attempt" as what that one run did, not as a cross-engine guarantee.
|
|
178
|
+
*
|
|
179
|
+
* Everything else FAILS SAFE to the policy, `NotAllowedError` included, and that
|
|
180
|
+
* is exactly what makes such thin evidence affordable: rejection names are
|
|
181
|
+
* per-engine, and an unknown one degrading to what ships today is the smaller
|
|
182
|
+
* mistake — "press any key" over a file we could have played still recovers on
|
|
183
|
+
* the first gesture, whereas the other way round leaves a recoverable deck
|
|
184
|
+
* permanently silent.
|
|
185
|
+
*/
|
|
186
|
+
export declare function refused(err: unknown): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Speech and subtitles for one presented deck.
|
|
189
|
+
*
|
|
190
|
+
* Two rules earn their own object. First, leaving a stop must silence it
|
|
191
|
+
* immediately by every route — arrow, click, Home/End, hashchange — so all of
|
|
192
|
+
* them funnel through `at`, which stops before it starts; two sentences talking
|
|
193
|
+
* over each other is worse than no narration at all.
|
|
194
|
+
*
|
|
195
|
+
* Second, cues are timed against `audio.currentTime` and nothing else. A timer
|
|
196
|
+
* started alongside `play()` agrees with the audio right up until the first
|
|
197
|
+
* stall, and then never again — and a stall is exactly when a viewer is looking
|
|
198
|
+
* at the subtitle to find out what they missed.
|
|
199
|
+
*
|
|
200
|
+
* Exported, with `Voice`, only so a test can drive its failure paths: everything
|
|
201
|
+
* that matters below happens after `play()` rejects, and nothing else in this
|
|
202
|
+
* module can hand it that promise. `start` is the only caller.
|
|
203
|
+
*/
|
|
204
|
+
export declare function mountVoice(doc: Document, narration: Narration, ui: {
|
|
205
|
+
subs: HTMLElement;
|
|
206
|
+
flags: HTMLElement;
|
|
207
|
+
}): Voice;
|
|
208
|
+
/** One player-page video, keyed in the island by the scene that draws its still. */
|
|
209
|
+
export interface ClipSpec {
|
|
210
|
+
/** Already in embeddable form — `embedUrl` in src/pack/media.ts did that at build time. */
|
|
211
|
+
url: string;
|
|
212
|
+
/** The figure's caption. It titles the frame, which is all a screen reader gets. */
|
|
213
|
+
title: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read the island, defensively, for the same reason `parseNarration` is
|
|
217
|
+
* defensive: a deck built before this existed has no island, and one built by a
|
|
218
|
+
* newer emitter may carry fields this reader has never heard of. Neither may do
|
|
219
|
+
* anything worse than leave the poster alone.
|
|
220
|
+
*/
|
|
221
|
+
export declare function parseClips(json: string | null | undefined): Record<string, ClipSpec>;
|
|
222
|
+
export {};
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A claim from the source, shown next to the figure that backs it.
|
|
3
|
-
*
|
|
4
|
-
* The layout is chosen from the figure's own aspect ratio rather than being a
|
|
5
|
-
* parameter: EXPERIMENT-002 full-bled a 1.98-aspect figure and pushed its
|
|
6
|
-
* caption 200px off-canvas. Only a genuine strip earns the full width.
|
|
7
|
-
*/
|
|
8
1
|
import type { Emitter } from "../kit.js";
|
|
9
2
|
export declare const claimFigure: Emitter<"claim-figure">;
|
|
@@ -13,12 +13,20 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Three things here are load-bearing and each of them cost a render to learn:
|
|
15
15
|
*
|
|
16
|
-
* 1. THE MOVE IS TWO `fromTo`s AND NOTHING ELSE.
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* 1. THE MOVE IS TWO `fromTo`s AND NOTHING ELSE. The first camera in experiment
|
|
17
|
+
* 008 drove `viewBox` from an `onUpdate`, played perfectly in a browser, and
|
|
18
|
+
* rendered 900 frames of a frozen `0 0 1920 1080` with every gate green.
|
|
19
|
+
*
|
|
20
|
+
* THAT RECORD STANDS; THE MECHANISM IT WAS ATTRIBUTED TO DOES NOT. `seek()`
|
|
21
|
+
* passing `suppressEvents` is why `decksmith frames` sees nothing, but the
|
|
22
|
+
* render does not seek — capture runs off Chrome's `beginFrame`, and both
|
|
23
|
+
* constructions were re-rendered on 2026-09-04 and both ramped smoothly. What
|
|
24
|
+
* a callback actually costs is reproducibility: 11 differing frames of 3,120
|
|
25
|
+
* on the demo as built, 260 with one `onUpdate` tween added. Whatever froze
|
|
26
|
+
* experiment 008's camera, it was not measured, and this file should not keep
|
|
27
|
+
* asserting it. The rule is unchanged and so is the reason for it — state must
|
|
28
|
+
* be applied by the thing being seeked. See invariant 11 in AGENTS.md and
|
|
29
|
+
* `.planning/2026-09-04-invariant-11-under-beginframe.md`.
|
|
22
30
|
*
|
|
23
31
|
* 2. THE EASES ARE CLOSED FORM AND THEY ARE NOT DECORATION. `zoomEase` travels
|
|
24
32
|
* scale in log space — a linear scale tween reads as a lurch, and every
|
|
@@ -67,6 +67,19 @@ export interface DeckOptions {
|
|
|
67
67
|
* failure shape.
|
|
68
68
|
*/
|
|
69
69
|
onBeatError?: (beatId: string, err: Error) => void;
|
|
70
|
+
/**
|
|
71
|
+
* What to do when a beat IS drawn but not as it was authored.
|
|
72
|
+
*
|
|
73
|
+
* The other half of `onBeatError`, and the reason it is a second hook rather
|
|
74
|
+
* than a second call to that one: this beat is IN the deck. An emitter that
|
|
75
|
+
* drops an ornament to fit its beat — see `Scene.warnings` — says so here, and
|
|
76
|
+
* absent this hook it says it to nobody, which is the silence the whole
|
|
77
|
+
* mechanism exists to avoid.
|
|
78
|
+
*
|
|
79
|
+
* Called once per built scene, from `layout`. `planCut` emits every beat as
|
|
80
|
+
* well and stays quiet: its scenes are thrown away.
|
|
81
|
+
*/
|
|
82
|
+
onBeatWarning?: (beatId: string, warning: string) => void;
|
|
70
83
|
/**
|
|
71
84
|
* The subsetted bundle's `@font-face` CSS, INLINED rather than linked.
|
|
72
85
|
*
|
package/dist/types/emit/kit.d.ts
CHANGED
|
@@ -137,6 +137,39 @@ export interface EmitContext {
|
|
|
137
137
|
* `hyperframes lint` rejects it (`unscoped_gsap_selector`).
|
|
138
138
|
*/
|
|
139
139
|
sid: string;
|
|
140
|
+
/**
|
|
141
|
+
* WHERE THIS SCENE BEGINS ON THE DECK'S OWN CLOCK, in seconds, ALREADY ROUNDED
|
|
142
|
+
* to invariant 10's three places — the identical number the scene wrapper
|
|
143
|
+
* publishes as `data-start`. Write it; do not round it again, because two
|
|
144
|
+
* roundings of one number is how a byte moves.
|
|
145
|
+
*
|
|
146
|
+
* Everything else an archetype emits is scene-relative, and that is the right
|
|
147
|
+
* default: an emitter that knew where it sat in the deck would be an emitter a
|
|
148
|
+
* cut could invalidate. ONE thing is not expressible that way. A `<video>` is
|
|
149
|
+
* timed by the RUNTIME, not by this scene's timeline, and the runtime reads
|
|
150
|
+
* `data-start` as an ABSOLUTE composition second — so a clip that declares a
|
|
151
|
+
* scene-relative start is seeked into a window that has already passed.
|
|
152
|
+
*
|
|
153
|
+
* MEASURED, because the alternative looked right on paper. hyperframes'
|
|
154
|
+
* compiler injects `data-start="0" data-hf-auto-start=""` into a media tag
|
|
155
|
+
* that declares no timing, and its runtime resolves that marker against the
|
|
156
|
+
* enclosing `[data-composition-id]`, which is exactly this scene — so the
|
|
157
|
+
* marker ought to have been enough. Rendered at 0.8.27, on a two-beat deck
|
|
158
|
+
* whose clip is red for 2s, green for 2s then blue for 2s and whose second
|
|
159
|
+
* scene starts at 7s: with the marker the plate is BLUE at composition 9.5s,
|
|
160
|
+
* 10.5s and 12.5s — the clip's last frame, frozen, because the render placed
|
|
161
|
+
* it at second 0 and it had ended before the scene began. With `data-start="7"`
|
|
162
|
+
* the same frames are green, green, blue: clip seconds 2.5, 3.5 and 5.5. Every
|
|
163
|
+
* gate is green over both.
|
|
164
|
+
*
|
|
165
|
+
* OPTIONAL, because the shell is not the only caller: an archetype test calls
|
|
166
|
+
* `emitScene` with a context it builds by hand, and twelve of the thirteen
|
|
167
|
+
* emitters have no use for this. The one that does refuses by name when it is
|
|
168
|
+
* missing rather than inventing a second, i.e. wrong, clock. `planCut` passes
|
|
169
|
+
* a provisional 0 for the same reason it passes provisional scene ids — its
|
|
170
|
+
* pass exists to measure holds, and every scene it emits is thrown away.
|
|
171
|
+
*/
|
|
172
|
+
start: number;
|
|
140
173
|
}
|
|
141
174
|
/**
|
|
142
175
|
* A value in a GSAP vars payload that is JavaScript rather than data.
|
|
@@ -278,20 +311,45 @@ export interface Scene {
|
|
|
278
311
|
* off, against 7 frames of antialiasing once deferred. Both failures are
|
|
279
312
|
* invisible to every gate. `cameraMeasure` carries the numbers and the controls.
|
|
280
313
|
*
|
|
281
|
-
* INVARIANT 11 IS THE TRAP HERE. This is not a callback on a tween.
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
314
|
+
* INVARIANT 11 IS THE TRAP HERE. This is not a callback on a tween. Measuring
|
|
315
|
+
* from an `onUpdate` — the obvious way to "measure late" — is what the
|
|
316
|
+
* invariant forbids, and the reason is reproducibility rather than the frozen
|
|
317
|
+
* video an older version of this comment claimed: measured 2026-09-04, one
|
|
318
|
+
* `onUpdate` tween took the demo from 11 non-reproducible frames of 3,120 to
|
|
319
|
+
* 260. See AGENTS.md invariant 11. Measurement here happens before the
|
|
320
|
+
* timeline exists, and what it produces is ordinary tween values.
|
|
286
321
|
*/
|
|
287
322
|
measure?: string[];
|
|
288
323
|
/**
|
|
289
324
|
* Vendored runtimes this scene's tweens need registered before its script
|
|
290
|
-
* runs — `"dsMorph"` for the equation morph
|
|
291
|
-
* when some scene names it, so a deck that names
|
|
292
|
-
* it was.
|
|
325
|
+
* runs — `"dsMorph"` for the equation morph, `"morphSVG"` for a reshape. The
|
|
326
|
+
* shell loads a runtime only when some scene names it, so a deck that names
|
|
327
|
+
* none is byte-for-byte what it was.
|
|
328
|
+
*
|
|
329
|
+
* `PLUGINS` in `composition.ts` is the ONE place a name resolves, and it is
|
|
330
|
+
* the list to read before returning this field: the type is
|
|
331
|
+
* `readonly string[]`, so a name that table does not know is a typo no
|
|
332
|
+
* compiler can see, and the shell refuses it there rather than emitting a
|
|
333
|
+
* scene whose plugin tween animates nothing.
|
|
293
334
|
*/
|
|
294
335
|
plugins?: readonly string[];
|
|
336
|
+
/**
|
|
337
|
+
* WHAT THE ARCHETYPE HAD TO GIVE UP TO DRAW THIS BEAT, one sentence each.
|
|
338
|
+
*
|
|
339
|
+
* The middle answer between drawing the beat as asked and refusing it. An
|
|
340
|
+
* emitter that cannot fit everything it was given has three moves: draw it
|
|
341
|
+
* anyway and let it collide or truncate, which is the failure this project
|
|
342
|
+
* keeps finding by eye; throw, which reaches `onBeatError` and costs the whole
|
|
343
|
+
* SLIDE; or draw less and say so, which is this. Reach for it when the thing
|
|
344
|
+
* dropped is an ORNAMENT — line-chart's reshape against a baseline, say —
|
|
345
|
+
* and for the throw when what is left would misrepresent the source.
|
|
346
|
+
*
|
|
347
|
+
* Surfaced by `layout` through `DeckOptions.onBeatWarning`, once per built
|
|
348
|
+
* scene. `planCut`'s measuring pass emits every beat too and deliberately does
|
|
349
|
+
* not report these: it throws its scenes away, and the same sentence twice per
|
|
350
|
+
* beat trains people to stop reading it.
|
|
351
|
+
*/
|
|
352
|
+
warnings?: readonly string[];
|
|
295
353
|
/**
|
|
296
354
|
* Hold points in seconds from the scene's start — where a presenter should
|
|
297
355
|
* pause. The shell converts these to absolute island fragment times.
|
package/dist/types/emit/svg.d.ts
CHANGED
|
@@ -69,6 +69,33 @@ export declare function nv(v: number): number;
|
|
|
69
69
|
*/
|
|
70
70
|
export declare const DRAW_FROM: Vars;
|
|
71
71
|
export declare const DRAW_TO: Vars;
|
|
72
|
+
/**
|
|
73
|
+
* ONE SHAPE BECOMES ANOTHER — a baseline curve lifting off its own position and
|
|
74
|
+
* settling into the result, leaving whatever was drawn under it behind.
|
|
75
|
+
*
|
|
76
|
+
* The from-vars are SELF-REFERENTIAL: `{ morphSVG: { shape: target } }` means
|
|
77
|
+
* "start from the `d` you were authored with". That is the construction
|
|
78
|
+
* `experiments/013-vocabulary/gaps/spike/index.html:147` measured, and it is the
|
|
79
|
+
* only one that keeps invariant 2 checkable by `tsc` — MorphSVG's natural
|
|
80
|
+
* spelling is a bare `to`, which is a `from()` wearing different clothes.
|
|
81
|
+
* `_parseShape` reads `data-original` when the shape names the target itself
|
|
82
|
+
* (MorphSVGPlugin.js), so the start is the authored path however many times the
|
|
83
|
+
* tween is re-initialised under a seek.
|
|
84
|
+
*
|
|
85
|
+
* `to` NAMES A SELECTOR, so the end shape lives in the document as a hidden
|
|
86
|
+
* sibling rather than as a path string in the timeline. Two copies of the same
|
|
87
|
+
* geometry — one in the `d` attribute, one in the tween — is the duplication
|
|
88
|
+
* `drawFrom` was retired for.
|
|
89
|
+
*
|
|
90
|
+
* Safe under capture for the reason DrawSVG is: a plugin's `render()` is part of
|
|
91
|
+
* being seeked where a callback is not. Measured for THIS plugin in a deck, not
|
|
92
|
+
* inherited from that one — `.planning/2026-09-08-reshape-seam.md`.
|
|
93
|
+
*
|
|
94
|
+
* ONE CALL PER (ELEMENT, MORPH). `first` is false for any reshape that is not
|
|
95
|
+
* the element's first on `morphSVG`; a second immediate render on one property
|
|
96
|
+
* would establish its start state at build time and undo the first.
|
|
97
|
+
*/
|
|
98
|
+
export declare function reshape(target: string, to: string, at: number, seconds: number, first: boolean): Tween;
|
|
72
99
|
/**
|
|
73
100
|
* Something travels a polyline — a pulse along an arrow, a marker ring along a
|
|
74
101
|
* curve, a highlight down a divider: one `x`/`y` `fromTo` per leg, each leg's
|
|
@@ -82,11 +82,20 @@ export declare function codexImages(opts?: CodexImagesOptions): ImageProvider;
|
|
|
82
82
|
* something better draws it. Same request, same bytes.
|
|
83
83
|
*/
|
|
84
84
|
export declare function drawSvg(req: ImageRequest): string;
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The size an SVG declares, for `sizeOf` in ./illustrate.ts and for anything
|
|
87
|
+
* else that already imports this module.
|
|
88
|
+
*
|
|
89
|
+
* It USED to live here, as a regex for `viewBox="0 0 %d %d"` over the first 200
|
|
90
|
+
* bytes — four integers, no units, no preamble — which is exactly the SVG
|
|
91
|
+
* `drawSvg` above writes and nothing else. Figure ingest needs to measure an SVG
|
|
92
|
+
* a stranger wrote, so the reader was generalised and moved to src/source/assets.ts
|
|
93
|
+
* beside the other formats. It moved THERE rather than staying here because
|
|
94
|
+
* that module imports nothing but node and the schemas: the reverse would put
|
|
95
|
+
* the Codex runner and this file's OpenAI adapter on the import path of every
|
|
96
|
+
* source parse, for two integers.
|
|
97
|
+
*/
|
|
98
|
+
export { svgSize } from "../source/assets.js";
|
|
90
99
|
/** The last rung. Pure, so it cannot fail, so `illustrate` always finishes. */
|
|
91
100
|
export declare function toolSvg(): ImageProvider;
|
|
92
101
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,12 +8,25 @@ import { type Format, type Source, type Storyboard } from "./types.js";
|
|
|
8
8
|
*/
|
|
9
9
|
export { parseMarkdown } from "./source/markdown.js";
|
|
10
10
|
export type { ParseOptions } from "./source/markdown.js";
|
|
11
|
+
/**
|
|
12
|
+
* A web page in, a markdown document and a directory of files out — the step
|
|
13
|
+
* BEFORE `parseMarkdown`, not a replacement for it. Separate because it needs a
|
|
14
|
+
* browser and the network, which `parseMarkdown` deliberately does not.
|
|
15
|
+
*
|
|
16
|
+
* `attachClips` is exported with it because a harvest is not finished without
|
|
17
|
+
* it: the markdown dialect has no way to say `kind: "clip"`, so a page's videos
|
|
18
|
+
* come back beside the document and this is what puts them into the source. It
|
|
19
|
+
* runs BEFORE `fetchFigures`, which passes a clip through untouched.
|
|
20
|
+
*/
|
|
21
|
+
export { attachClips, harvest, toMarkdown } from "./source/harvest.js";
|
|
22
|
+
export type { Block, Harvested, HarvestedClip, HarvestOptions } from "./source/harvest.js";
|
|
11
23
|
/**
|
|
12
24
|
* Figures referenced by URL, downloaded beside the source. Separate from
|
|
13
25
|
* `parseMarkdown` because it touches the network and a server may want to fetch
|
|
14
26
|
* through its own client instead.
|
|
15
27
|
*/
|
|
16
28
|
export { fetchFigures } from "./source/assets.js";
|
|
29
|
+
export { fetchGuarded, isBlockedAddress } from "./net/fetch.js";
|
|
17
30
|
/**
|
|
18
31
|
* Subset a CJK webfont over the glyphs a deck actually renders. Exported
|
|
19
32
|
* because invariant 9 — a font stack naming a family the bundle does not
|
|
@@ -153,6 +166,19 @@ export type { AssetRequest, Fetcher, Media, MediaPlan } from "./pack/media.js";
|
|
|
153
166
|
*/
|
|
154
167
|
export { CONFIG_FILE, loadPrefs } from "./prefs.js";
|
|
155
168
|
export type { Prefs, PrefsPatch } from "./prefs.js";
|
|
169
|
+
/**
|
|
170
|
+
* The `TMPDIR` guard, exported because `src/server/main.ts` has no other way to
|
|
171
|
+
* reach it: `build:server` transpiles without bundling, so a deep import there
|
|
172
|
+
* would survive into `dist/server/` and resolve to nothing. It fails the house
|
|
173
|
+
* rule for this file — nobody outside asked for it — and it is here anyway,
|
|
174
|
+
* because the alternative is a second copy of the guard living under `server/`.
|
|
175
|
+
*
|
|
176
|
+
* A CONSUMER SHOULD NEVER NEED TO CALL IT. It is a no-op in a published install
|
|
177
|
+
* (see src/tmpdir.ts for why), and importing this module does not run it: an
|
|
178
|
+
* absent `TMPDIR` is inherited by every child process, which is not something a
|
|
179
|
+
* library import may decide for its host.
|
|
180
|
+
*/
|
|
181
|
+
export { guardTmpdir } from "./tmpdir.js";
|
|
156
182
|
/**
|
|
157
183
|
* Every schema and type. Unusually broad for this file, and justified: `types.ts`
|
|
158
184
|
* is nothing but the wire format — what `source.json`, `storyboard.json`,
|
|
@@ -184,6 +210,12 @@ export interface BuildDeckOptions {
|
|
|
184
210
|
* Absent, the emitter's error propagates — see `DeckOptions.onBeatError`.
|
|
185
211
|
*/
|
|
186
212
|
onBeatError?: (beatId: string, err: Error) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Called when a beat is drawn but not as it was authored — an emitter dropped
|
|
215
|
+
* an ornament to fit the beat's length. The beat is IN the deck; nothing is
|
|
216
|
+
* missing but the ornament. See `DeckOptions.onBeatWarning`.
|
|
217
|
+
*/
|
|
218
|
+
onBeatWarning?: (beatId: string, warning: string) => void;
|
|
187
219
|
/** Any name in `THEME_NAMES`. Overrides `storyboard.theme`. */
|
|
188
220
|
theme?: string;
|
|
189
221
|
/** Multiplies every duration and hold. 1 leaves the bytes untouched. */
|
|
@@ -90,6 +90,7 @@ export declare const estimateSchema: z.ZodObject<{
|
|
|
90
90
|
export declare const createSchema: z.ZodObject<{
|
|
91
91
|
document_path: z.ZodOptional<z.ZodString>;
|
|
92
92
|
document_text: z.ZodOptional<z.ZodString>;
|
|
93
|
+
document_url: z.ZodOptional<z.ZodString>;
|
|
93
94
|
settings: z.ZodOptional<z.ZodObject<{
|
|
94
95
|
format: z.ZodOptional<z.ZodEnum<{
|
|
95
96
|
[x: string]: string;
|
|
@@ -221,6 +222,25 @@ export declare function deckTools(opts: McpOptions): {
|
|
|
221
222
|
state: import("../server/queue.js").StepState;
|
|
222
223
|
}[];
|
|
223
224
|
log: string[];
|
|
225
|
+
} | {
|
|
226
|
+
harvest_warnings: string[];
|
|
227
|
+
next: string;
|
|
228
|
+
deck_path?: string | undefined;
|
|
229
|
+
slides?: number | undefined;
|
|
230
|
+
duration_seconds?: number | undefined;
|
|
231
|
+
warnings?: string[] | undefined;
|
|
232
|
+
storyboard_path: string;
|
|
233
|
+
error?: import("../server/errors.js").JobError | undefined;
|
|
234
|
+
job_id: string;
|
|
235
|
+
state: import("../server/queue.js").JobState;
|
|
236
|
+
stage: import("../server/queue.js").Stage | undefined;
|
|
237
|
+
queue_position: number | undefined;
|
|
238
|
+
elapsed_seconds: number;
|
|
239
|
+
steps: {
|
|
240
|
+
name: import("../server/queue.js").Stage;
|
|
241
|
+
state: import("../server/queue.js").StepState;
|
|
242
|
+
}[];
|
|
243
|
+
log: string[];
|
|
224
244
|
}>;
|
|
225
245
|
/** Poll, blocking up to `wait_seconds` for the job to move on. */
|
|
226
246
|
status(input: z.infer<typeof statusSchema>): Promise<{
|