@plaintake/scenario 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,335 @@
1
+ import { z } from 'zod';
2
+ declare const ArtifactRefSchema: z.ZodObject<{
3
+ path: z.ZodString;
4
+ sha256: z.ZodString;
5
+ bytes: z.ZodNumber;
6
+ }, z.core.$strip>;
7
+ export declare const ValidationResultSchema: z.ZodObject<{
8
+ scenario: z.ZodOptional<z.ZodObject<{
9
+ id: z.ZodString;
10
+ title: z.ZodString;
11
+ language: z.ZodString;
12
+ handoff: z.ZodDefault<z.ZodEnum<{
13
+ none: "none";
14
+ preflight: "preflight";
15
+ session: "session";
16
+ }>>;
17
+ sha256: z.ZodString;
18
+ }, z.core.$strip>>;
19
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
20
+ kind: z.ZodLiteral<"validate">;
21
+ ok: z.ZodBoolean;
22
+ problems: z.ZodArray<z.ZodString>;
23
+ }, z.core.$strip>;
24
+ export declare const RunCommandResultSchema: z.ZodObject<{
25
+ bundleDir: z.ZodString;
26
+ status: z.ZodEnum<{
27
+ passed: "passed";
28
+ failed: "failed";
29
+ }>;
30
+ cueCount: z.ZodNumber;
31
+ chapterCount: z.ZodNumber;
32
+ narratedCount: z.ZodNumber;
33
+ diagnostics: z.ZodArray<z.ZodObject<{
34
+ code: z.ZodString;
35
+ cueId: z.ZodString;
36
+ detail: z.ZodString;
37
+ }, z.core.$strip>>;
38
+ assertions: z.ZodArray<z.ZodObject<{
39
+ id: z.ZodString;
40
+ status: z.ZodEnum<{
41
+ passed: "passed";
42
+ failed: "failed";
43
+ }>;
44
+ }, z.core.$strip>>;
45
+ outputs: z.ZodArray<z.ZodObject<{
46
+ path: z.ZodString;
47
+ sha256: z.ZodString;
48
+ bytes: z.ZodNumber;
49
+ }, z.core.$strip>>;
50
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
51
+ kind: z.ZodLiteral<"run">;
52
+ ok: z.ZodBoolean;
53
+ problems: z.ZodArray<z.ZodString>;
54
+ }, z.core.$strip>;
55
+ export declare const RenderCommandResultSchema: z.ZodObject<{
56
+ bundleDir: z.ZodString;
57
+ variants: z.ZodEnum<{
58
+ soft: "soft";
59
+ hard: "hard";
60
+ }>;
61
+ streams: z.ZodObject<{
62
+ base: z.ZodOptional<z.ZodObject<{
63
+ container: z.ZodEnum<{
64
+ mp4: "mp4";
65
+ webm: "webm";
66
+ other: "other";
67
+ }>;
68
+ durationMs: z.ZodNumber;
69
+ video: z.ZodObject<{
70
+ codec: z.ZodString;
71
+ width: z.ZodNumber;
72
+ height: z.ZodNumber;
73
+ pixelFormat: z.ZodString;
74
+ fps: z.ZodNumber;
75
+ frames: z.ZodNumber;
76
+ durationMs: z.ZodNumber;
77
+ }, z.core.$strip>;
78
+ subtitles: z.ZodArray<z.ZodObject<{
79
+ codec: z.ZodString;
80
+ language: z.ZodString;
81
+ title: z.ZodString;
82
+ }, z.core.$strip>>;
83
+ audio: z.ZodArray<z.ZodObject<{
84
+ codec: z.ZodString;
85
+ sampleRate: z.ZodNumber;
86
+ channels: z.ZodNumber;
87
+ }, z.core.$strip>>;
88
+ dataStreams: z.ZodNumber;
89
+ chapters: z.ZodArray<z.ZodObject<{
90
+ startMs: z.ZodNumber;
91
+ endMs: z.ZodNumber;
92
+ title: z.ZodString;
93
+ }, z.core.$strip>>;
94
+ }, z.core.$strip>>;
95
+ soft: z.ZodOptional<z.ZodObject<{
96
+ container: z.ZodEnum<{
97
+ mp4: "mp4";
98
+ webm: "webm";
99
+ other: "other";
100
+ }>;
101
+ durationMs: z.ZodNumber;
102
+ video: z.ZodObject<{
103
+ codec: z.ZodString;
104
+ width: z.ZodNumber;
105
+ height: z.ZodNumber;
106
+ pixelFormat: z.ZodString;
107
+ fps: z.ZodNumber;
108
+ frames: z.ZodNumber;
109
+ durationMs: z.ZodNumber;
110
+ }, z.core.$strip>;
111
+ subtitles: z.ZodArray<z.ZodObject<{
112
+ codec: z.ZodString;
113
+ language: z.ZodString;
114
+ title: z.ZodString;
115
+ }, z.core.$strip>>;
116
+ audio: z.ZodArray<z.ZodObject<{
117
+ codec: z.ZodString;
118
+ sampleRate: z.ZodNumber;
119
+ channels: z.ZodNumber;
120
+ }, z.core.$strip>>;
121
+ dataStreams: z.ZodNumber;
122
+ chapters: z.ZodArray<z.ZodObject<{
123
+ startMs: z.ZodNumber;
124
+ endMs: z.ZodNumber;
125
+ title: z.ZodString;
126
+ }, z.core.$strip>>;
127
+ }, z.core.$strip>>;
128
+ hard: z.ZodOptional<z.ZodObject<{
129
+ container: z.ZodEnum<{
130
+ mp4: "mp4";
131
+ webm: "webm";
132
+ other: "other";
133
+ }>;
134
+ durationMs: z.ZodNumber;
135
+ video: z.ZodObject<{
136
+ codec: z.ZodString;
137
+ width: z.ZodNumber;
138
+ height: z.ZodNumber;
139
+ pixelFormat: z.ZodString;
140
+ fps: z.ZodNumber;
141
+ frames: z.ZodNumber;
142
+ durationMs: z.ZodNumber;
143
+ }, z.core.$strip>;
144
+ subtitles: z.ZodArray<z.ZodObject<{
145
+ codec: z.ZodString;
146
+ language: z.ZodString;
147
+ title: z.ZodString;
148
+ }, z.core.$strip>>;
149
+ audio: z.ZodArray<z.ZodObject<{
150
+ codec: z.ZodString;
151
+ sampleRate: z.ZodNumber;
152
+ channels: z.ZodNumber;
153
+ }, z.core.$strip>>;
154
+ dataStreams: z.ZodNumber;
155
+ chapters: z.ZodArray<z.ZodObject<{
156
+ startMs: z.ZodNumber;
157
+ endMs: z.ZodNumber;
158
+ title: z.ZodString;
159
+ }, z.core.$strip>>;
160
+ }, z.core.$strip>>;
161
+ }, z.core.$strip>;
162
+ outputs: z.ZodArray<z.ZodObject<{
163
+ path: z.ZodString;
164
+ sha256: z.ZodString;
165
+ bytes: z.ZodNumber;
166
+ }, z.core.$strip>>;
167
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
168
+ kind: z.ZodLiteral<"render">;
169
+ ok: z.ZodBoolean;
170
+ problems: z.ZodArray<z.ZodString>;
171
+ }, z.core.$strip>;
172
+ export declare const VerificationReportSchema: z.ZodObject<{
173
+ bundleDir: z.ZodString;
174
+ artifactCount: z.ZodNumber;
175
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
176
+ kind: z.ZodLiteral<"verify">;
177
+ ok: z.ZodBoolean;
178
+ problems: z.ZodArray<z.ZodString>;
179
+ }, z.core.$strip>;
180
+ export declare const InspectResultSchema: z.ZodObject<{
181
+ bundleDir: z.ZodString;
182
+ status: z.ZodEnum<{
183
+ passed: "passed";
184
+ failed: "failed";
185
+ }>;
186
+ scenario: z.ZodObject<{
187
+ id: z.ZodString;
188
+ sourceSha256: z.ZodString;
189
+ }, z.core.$strip>;
190
+ toolchain: z.ZodObject<{
191
+ node: z.ZodString;
192
+ playwright: z.ZodString;
193
+ chromiumRevision: z.ZodString;
194
+ ffmpeg: z.ZodString;
195
+ libass: z.ZodString;
196
+ fontSha256: z.ZodString;
197
+ }, z.core.$strip>;
198
+ video: z.ZodObject<{
199
+ durationMs: z.ZodNumber;
200
+ frames: z.ZodNumber;
201
+ width: z.ZodNumber;
202
+ height: z.ZodNumber;
203
+ fps: z.ZodNumber;
204
+ contentDurationMs: z.ZodNumber;
205
+ introDurationMs: z.ZodDefault<z.ZodNumber>;
206
+ outroDurationMs: z.ZodNumber;
207
+ }, z.core.$strip>;
208
+ captions: z.ZodObject<{
209
+ language: z.ZodString;
210
+ cueCount: z.ZodNumber;
211
+ characterCount: z.ZodNumber;
212
+ medianCueMs: z.ZodNumber;
213
+ shortestCueMs: z.ZodNumber;
214
+ longestCueMs: z.ZodNumber;
215
+ }, z.core.$strip>;
216
+ chapters: z.ZodArray<z.ZodObject<{
217
+ startMs: z.ZodNumber;
218
+ title: z.ZodString;
219
+ }, z.core.$strip>>;
220
+ narration: z.ZodOptional<z.ZodObject<{
221
+ clipCount: z.ZodNumber;
222
+ spokenMs: z.ZodNumber;
223
+ suppliedCount: z.ZodNumber;
224
+ voice: z.ZodOptional<z.ZodString>;
225
+ }, z.core.$strip>>;
226
+ outputs: z.ZodArray<z.ZodObject<{
227
+ path: z.ZodString;
228
+ sha256: z.ZodString;
229
+ bytes: z.ZodNumber;
230
+ }, z.core.$strip>>;
231
+ assertions: z.ZodArray<z.ZodObject<{
232
+ id: z.ZodString;
233
+ status: z.ZodEnum<{
234
+ passed: "passed";
235
+ failed: "failed";
236
+ }>;
237
+ }, z.core.$strip>>;
238
+ artifactCount: z.ZodNumber;
239
+ bundleBytes: z.ZodNumber;
240
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
241
+ kind: z.ZodLiteral<"inspect">;
242
+ ok: z.ZodBoolean;
243
+ problems: z.ZodArray<z.ZodString>;
244
+ }, z.core.$strip>;
245
+ export declare const DoctorResultSchema: z.ZodObject<{
246
+ version: z.ZodString;
247
+ ffmpeg: z.ZodString;
248
+ ffprobe: z.ZodString;
249
+ libass: z.ZodString;
250
+ hasLibass: z.ZodBoolean;
251
+ hasX264: z.ZodBoolean;
252
+ hasAac: z.ZodBoolean;
253
+ filters: z.ZodArray<z.ZodString>;
254
+ speech: z.ZodObject<{
255
+ ready: z.ZodBoolean;
256
+ voices: z.ZodArray<z.ZodString>;
257
+ notes: z.ZodArray<z.ZodString>;
258
+ }, z.core.$strip>;
259
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
260
+ kind: z.ZodLiteral<"doctor">;
261
+ ok: z.ZodBoolean;
262
+ problems: z.ZodArray<z.ZodString>;
263
+ }, z.core.$strip>;
264
+ export declare const ActivateResultSchema: z.ZodObject<{
265
+ reason: z.ZodOptional<z.ZodEnum<{
266
+ "empty-key": "empty-key";
267
+ "not-found": "not-found";
268
+ revoked: "revoked";
269
+ offline: "offline";
270
+ "unexpected-response": "unexpected-response";
271
+ "not-configured": "not-configured";
272
+ }>>;
273
+ licence: z.ZodOptional<z.ZodObject<{
274
+ subject: z.ZodString;
275
+ activatedAt: z.ZodString;
276
+ uses: z.ZodNumber;
277
+ }, z.core.$strip>>;
278
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
279
+ kind: z.ZodLiteral<"activate">;
280
+ ok: z.ZodBoolean;
281
+ problems: z.ZodArray<z.ZodString>;
282
+ }, z.core.$strip>;
283
+ /**
284
+ * Mirrors `LicenseStatus` in @plaintake/license: a discriminated union on `tier`, so a
285
+ * caller cannot read `subject` without having established the tier.
286
+ */
287
+ export declare const LicenceResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
288
+ tier: z.ZodLiteral<"free">;
289
+ problem: z.ZodOptional<z.ZodString>;
290
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
291
+ kind: z.ZodLiteral<"licence">;
292
+ ok: z.ZodBoolean;
293
+ problems: z.ZodArray<z.ZodString>;
294
+ }, z.core.$strip>, z.ZodObject<{
295
+ tier: z.ZodLiteral<"pro">;
296
+ subject: z.ZodString;
297
+ activatedAt: z.ZodString;
298
+ uses: z.ZodNumber;
299
+ schema: z.ZodLiteral<"agent-demo.result/v1">;
300
+ kind: z.ZodLiteral<"licence">;
301
+ ok: z.ZodBoolean;
302
+ problems: z.ZodArray<z.ZodString>;
303
+ }, z.core.$strip>], "tier">;
304
+ export type ArtifactRef = z.infer<typeof ArtifactRefSchema>;
305
+ export type ValidationResult = z.infer<typeof ValidationResultSchema>;
306
+ export type RunCommandResult = z.infer<typeof RunCommandResultSchema>;
307
+ export type RenderCommandResult = z.infer<typeof RenderCommandResultSchema>;
308
+ export type VerificationReport = z.infer<typeof VerificationReportSchema>;
309
+ export type InspectResult = z.infer<typeof InspectResultSchema>;
310
+ export type DoctorResult = z.infer<typeof DoctorResultSchema>;
311
+ export type ActivateResult = z.infer<typeof ActivateResultSchema>;
312
+ export type LicenceResult = z.infer<typeof LicenceResultSchema>;
313
+ export type DemoResult = ValidationResult | RunCommandResult | RenderCommandResult | VerificationReport | InspectResult | DoctorResult | ActivateResult | LicenceResult;
314
+ /**
315
+ * Renders a filesystem location for display in a result, relative to `root` when it
316
+ * lies inside it.
317
+ *
318
+ * A path outside the root is returned unchanged rather than rejected, because the CLI
319
+ * is deliberately not sandboxed and `--output /tmp/scratch` is a legitimate
320
+ * thing to ask for. The MCP server has the opposite requirement — spec §6, "return
321
+ * relative artifact paths" — and gets it from its sandbox, which resolves every input
322
+ * beneath the workspace root before this is ever called, so the relative branch always
323
+ * wins there. `assertNoAbsolutePaths` enforces that at the MCP boundary.
324
+ *
325
+ * Note this applies to *locations* like `bundleDir`. Paths *within* a bundle stay
326
+ * strictly relative, enforced by the schemas above.
327
+ */
328
+ export declare function toResultPath(root: string, path: string): string;
329
+ /**
330
+ * Guards the MCP contract: no field anywhere in a result may be an absolute path.
331
+ * Cheap, and it fails loudly rather than quietly leaking the server's filesystem
332
+ * layout to a client that asked for workspace-relative paths.
333
+ */
334
+ export declare function assertNoAbsolutePaths(result: DemoResult): void;
335
+ export {};
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The opening card, declared by the scenario rather than by configuration.
4
+ *
5
+ * The counterpart to the closing credit, and deliberately not the same kind of thing. The
6
+ * outro is *branding* — one card for everything a machine records, owned by the licence and
7
+ * fixed in code on the Free Tier so the credit cannot be quietly removed. An intro is
8
+ * *content*: the hook for this demo and no other, which is why it is authored here beside
9
+ * the steps it introduces and is available on every tier. Only its colours follow branding.
10
+ *
11
+ * `narration` is spoken by the same voice that reads a step's `subtitle`, and is optional:
12
+ * a silent title card is a reasonable thing to want, and a scenario recorded without
13
+ * `--speech` has no voice to read it with either way. `durationMs` is a floor rather than a
14
+ * cut — the recorder lengthens the card when the line needs longer, exactly as `holdMs`
15
+ * gives way to a step's own narration.
16
+ *
17
+ * Two lines, matching the outro: same card, same layout, and a third line would have to
18
+ * shrink the type to fit.
19
+ */
20
+ export declare const ScenarioIntroSchema: z.ZodObject<{
21
+ lines: z.ZodArray<z.ZodString>;
22
+ narration: z.ZodOptional<z.ZodString>;
23
+ durationMs: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strip>;
25
+ export type ScenarioIntro = z.infer<typeof ScenarioIntroSchema>;
26
+ /**
27
+ * Per-scenario camera framing, for the demos whose pages the defaults do not suit.
28
+ *
29
+ * Every field is optional and every default is the constant the camera already used, so a
30
+ * scenario that says nothing is framed exactly as it was before this existed. That is the
31
+ * whole design: the ceiling is not lowered globally, because doing so would silently
32
+ * re-frame every recording anyone has ever made, and the values here were measured against
33
+ * real pages rather than guessed.
34
+ *
35
+ * `maxZoom` is the one that matters. At the 1.6x default the tightest window is 1216x684 —
36
+ * 37% of the frame discarded — which is fine for a compact form and is why one recorded demo
37
+ * lost the whole left third of a results page. A busy layout wants 1.3x or so; text-heavy
38
+ * pages that need the magnification can keep 1.6.
39
+ *
40
+ * `margin` is the breathing room kept around the target when choosing that zoom, as a
41
+ * fraction of the target on each side. Raising it is the gentler way to keep context in
42
+ * frame: it lowers the zoom the target asks for, rather than capping the zoom it gets.
43
+ *
44
+ * `minDwellMs` is the one thing here that changes the recording rather than the framing —
45
+ * see `runtime.ts`, where it is applied.
46
+ */
47
+ export declare const ScenarioCameraSchema: z.ZodObject<{
48
+ maxZoom: z.ZodOptional<z.ZodNumber>;
49
+ margin: z.ZodOptional<z.ZodNumber>;
50
+ easeMs: z.ZodOptional<z.ZodNumber>;
51
+ minDwellMs: z.ZodOptional<z.ZodNumber>;
52
+ }, z.core.$strip>;
53
+ export type ScenarioCamera = z.infer<typeof ScenarioCameraSchema>;
54
+ export declare const ScenarioMetaSchema: z.ZodObject<{
55
+ schema: z.ZodLiteral<"agent-demo.scenario/v1">;
56
+ id: z.ZodString;
57
+ title: z.ZodString;
58
+ language: z.ZodDefault<z.ZodString>;
59
+ viewport: z.ZodObject<{
60
+ width: z.ZodLiteral<1920>;
61
+ height: z.ZodLiteral<1080>;
62
+ deviceScaleFactor: z.ZodLiteral<1>;
63
+ }, z.core.$strip>;
64
+ locale: z.ZodLiteral<"en-US">;
65
+ timezoneId: z.ZodLiteral<"UTC">;
66
+ colorScheme: z.ZodLiteral<"light">;
67
+ reducedMotion: z.ZodLiteral<"reduce">;
68
+ allowedConsoleErrors: z.ZodDefault<z.ZodArray<z.ZodString>>;
69
+ handoff: z.ZodDefault<z.ZodEnum<{
70
+ none: "none";
71
+ preflight: "preflight";
72
+ session: "session";
73
+ }>>;
74
+ handoffTimeoutMs: z.ZodDefault<z.ZodNumber>;
75
+ intro: z.ZodOptional<z.ZodObject<{
76
+ lines: z.ZodArray<z.ZodString>;
77
+ narration: z.ZodOptional<z.ZodString>;
78
+ durationMs: z.ZodOptional<z.ZodNumber>;
79
+ }, z.core.$strip>>;
80
+ camera: z.ZodOptional<z.ZodObject<{
81
+ maxZoom: z.ZodOptional<z.ZodNumber>;
82
+ margin: z.ZodOptional<z.ZodNumber>;
83
+ easeMs: z.ZodOptional<z.ZodNumber>;
84
+ minDwellMs: z.ZodOptional<z.ZodNumber>;
85
+ }, z.core.$strip>>;
86
+ }, z.core.$strip>;
87
+ /** Which phase, if any, hands the browser over. Read by the recorder and the surfaces. */
88
+ export type HandoffMode = z.infer<typeof ScenarioMetaSchema>['handoff'];
89
+ export type ScenarioMeta = z.infer<typeof ScenarioMetaSchema>;
@@ -0,0 +1,4 @@
1
+ /** Deterministic JSON: sorted keys, 2-space indent, LF, exactly one trailing newline. */
2
+ export declare function stableStringify(value: unknown): string;
3
+ /** Single-line deterministic JSON for NDJSON records. */
4
+ export declare function stableJsonLine(value: unknown): string;
@@ -0,0 +1,8 @@
1
+ import { ZodError } from 'zod';
2
+ /** So a caller outside this package's Zod boundary never needs its own `zod` dependency. */
3
+ export declare function isZodError(error: unknown): error is ZodError;
4
+ /**
5
+ * One line per issue, `path: message`, instead of `ZodError.message`'s raw JSON array —
6
+ * the same array `validate` and `demo_validate` were both printing verbatim as a "problem".
7
+ */
8
+ export declare function formatZodError(error: ZodError): string;
@@ -0,0 +1,194 @@
1
+ import type { ScenarioMeta } from './schema/index.js';
2
+ import type { Locator, Page } from 'playwright-core';
3
+ /**
4
+ * What kind of interaction `run()` performs, declared rather than observed. Like
5
+ * `target`, this is metadata the recorder records and never performs — it tells the
6
+ * cursor track what to draw at the target (`click` a ripple, `type`/`point` a settle).
7
+ * Absent means `point`: the cursor still moves to the target, nothing pulses.
8
+ */
9
+ export type DemoAction = 'click' | 'type' | 'point';
10
+ export type DemoStep = {
11
+ id: string;
12
+ title: string;
13
+ subtitle?: string;
14
+ /** Locator whose bounding box is recorded on step.start. Never used to perform the action. */
15
+ target?: Locator;
16
+ /** Declared interaction kind, recorded on step.start and consumed by the cursor track. */
17
+ action?: DemoAction;
18
+ /** Extra presentation hold after the action completes, in whole milliseconds. */
19
+ holdMs?: number;
20
+ run: () => Promise<unknown>;
21
+ };
22
+ export type DemoAssertion = {
23
+ id: string;
24
+ title: string;
25
+ run: () => Promise<unknown>;
26
+ };
27
+ /**
28
+ * A CSS selector string, not a Locator, so a mask can be registered
29
+ * before the element it hides exists.
30
+ */
31
+ export type DemoMask = {
32
+ id: string;
33
+ selector: string;
34
+ reason?: string;
35
+ };
36
+ /**
37
+ * A bounded wait for something PlainTake is not driving — a person approving a push
38
+ * notification on their phone, clicking a link in an email, or a background job finishing.
39
+ * Prompts nobody and needs no visible window, so unlike a handoff it works headless and in
40
+ * CI.
41
+ *
42
+ * It exists rather than leaving authors to `await page.waitForSelector(...)` for the three
43
+ * reasons `demo.pause` exists rather than `page.waitForTimeout`: it races the run's abort
44
+ * signal, so Ctrl-C during a five-minute wait does not hang until the wait expires; it puts
45
+ * the wait on the timeline, so a still stretch of video has a recorded explanation; and it
46
+ * gives timeouts one error kind instead of whatever Playwright happened to throw.
47
+ */
48
+ export type DemoWaitFor = {
49
+ id: string;
50
+ /** Recorded as the event title, and shown to a person in later phases. Never a secret. */
51
+ title: string;
52
+ /**
53
+ * Resolves when the wait is over. Invoked once; a rejection fails the run.
54
+ *
55
+ * Pass `{ timeout: 0 }` to whatever Playwright call is inside, so `timeoutMs` below is the
56
+ * only deadline. Otherwise Playwright's own 30s default fires first and reports the
57
+ * author's locator as the culprit rather than the wait.
58
+ */
59
+ until: () => Promise<unknown>;
60
+ /** Defaults to 120s, capped at 300s. Out-of-range values are a usage error. */
61
+ timeoutMs?: number;
62
+ };
63
+ /**
64
+ * Hands the real browser window to a person, and waits for them.
65
+ *
66
+ * The counterpart to `waitFor`: that one blocks on a condition and prompts nobody, this one
67
+ * prompts somebody and blocks on them. The person acts *in the browser* — types the one-time
68
+ * code, solves the CAPTCHA, completes the SSO round trip — so no secret ever passes through
69
+ * PlainTake. See `HumanReply`: the only thing that comes back is which of three things
70
+ * happened.
71
+ *
72
+ * Requires `handoff` in the scenario's metadata, and the phase must match. That is checked
73
+ * before Chromium starts, so a scenario that could never work says so immediately.
74
+ */
75
+ export type PreflightHandoff = {
76
+ id: string;
77
+ /** Shown to the person and recorded as the event title. Never a secret. */
78
+ title: string;
79
+ /** Extra lines at the prompt: what to do, and how to know it worked. */
80
+ detail?: string;
81
+ /**
82
+ * Optional. When present, the handoff can end on its own the moment the condition holds,
83
+ * without the person confirming — the same `{ timeout: 0 }` rule as `DemoWaitFor.until`
84
+ * applies. Without it, the only way out is the person, the deadline, or a cancellation.
85
+ */
86
+ until?: () => Promise<unknown>;
87
+ /** Defaults to the scenario's `handoffTimeoutMs`, itself 120s and capped at 300s. */
88
+ timeoutMs?: number;
89
+ };
90
+ /**
91
+ * A handoff that happens on camera. Everything a pre-flight one has, plus the single
92
+ * thing that only means anything once the recording is running.
93
+ */
94
+ export type DemoHandoff = PreflightHandoff & {
95
+ /**
96
+ * CSS selector, registered before the window is handed over and left registered afterwards,
97
+ * exactly as `demo.mask` does — so a field the *person* fills during the handoff is
98
+ * unreadable from the moment it exists, on this document and on every one after it.
99
+ *
100
+ * It hides what it names and nothing else. Anything already on screen when the handoff
101
+ * begins was already in the frames before it, so mask that with `demo.mask` earlier
102
+ * instead; and a confirmation toast, a URL bar or a desktop notification is beyond reach
103
+ * of any selector. A person cannot proofread what they cannot see.
104
+ *
105
+ * Session-only, and a usage error in pre-flight: nothing is being recorded there, so a mask
106
+ * would hide nothing while silently applying to the whole video that follows.
107
+ */
108
+ mask?: string;
109
+ };
110
+ /**
111
+ * What a scenario may do before recording starts.
112
+ *
113
+ * Deliberately narrower than `DemoContext`. `chapter`, `step`, `assert` and `mask` all exist
114
+ * to put something on a video timeline, and during pre-flight there is no video timeline
115
+ * yet. Offering them here and silently dropping their output is exactly the "quietly does
116
+ * nothing" failure the rest of the DSL is built to avoid, so they are absent instead.
117
+ *
118
+ * Both members are here rather than only on `DemoContext` because pre-flight is the *better*
119
+ * place for a handoff: a sign-in done here reaches neither the video nor the trace.
120
+ *
121
+ * `handoff` takes the narrower `PreflightHandoff` for the same reason the four missing
122
+ * methods are missing: `mask` exists to keep something out of the pixels, and there are no
123
+ * pixels yet. Passing one here is a compile error on the spot rather than a mask that
124
+ * quietly does nothing — and the runtime refuses it too, for the object that was not a
125
+ * literal.
126
+ */
127
+ export interface PreflightContext {
128
+ waitFor(condition: DemoWaitFor): Promise<void>;
129
+ handoff(request: PreflightHandoff): Promise<void>;
130
+ }
131
+ export interface DemoContext extends PreflightContext {
132
+ /** Widened by exactly one optional property, `mask`. See `DemoHandoff`. */
133
+ handoff(request: DemoHandoff): Promise<void>;
134
+ chapter(title: string): Promise<void>;
135
+ step(step: DemoStep): Promise<void>;
136
+ assert(assertion: DemoAssertion): Promise<void>;
137
+ mask(mask: DemoMask): Promise<void>;
138
+ pause(ms: number): Promise<void>;
139
+ }
140
+ export type DemoRunArgs = {
141
+ page: Page;
142
+ demo: DemoContext;
143
+ baseURL: string;
144
+ };
145
+ /** Same property names as `DemoRunArgs`, so moving code between phases changes nothing else. */
146
+ export type PreflightArgs = {
147
+ page: Page;
148
+ demo: PreflightContext;
149
+ baseURL: string;
150
+ };
151
+ /**
152
+ * Runs before `tracing.start()` as well as before `screencast.start()`, so nothing it
153
+ * does reaches either the trace or the video, and hands the page back on `about:blank`.
154
+ */
155
+ type PreflightHook = {
156
+ preflight?(args: PreflightArgs): Promise<void>;
157
+ };
158
+ /**
159
+ * Runs after pre-flight's `about:blank` hand-back and before `runtime.beginSession()`,
160
+ * `tracing.start()` and `screencast.start()`.
161
+ *
162
+ * It exists because the hand-back is load-bearing and must stay: whatever pre-flight ended
163
+ * on is not frame 0 of the video, and the timeline origin is measured under the same starting
164
+ * condition a no-preflight run starts under. But that same hand-back is why a scenario
165
+ * cannot start its video on a loaded page by navigating in pre-flight — the reset throws
166
+ * the navigation away, and the alternative, a `page.goto` as the first step's action, films
167
+ * the load: a client-rendered app whose first paint is blank puts seconds of white between
168
+ * the opening card and the first screen. This phase is the way out. What it navigates to and
169
+ * waits for is the author's to declare, because only the scenario knows which page the video
170
+ * opens on and what "ready" means on it; that it runs off-camera is the recorder's to
171
+ * guarantee, and is the same guarantee pre-flight already rests on.
172
+ *
173
+ * Takes `PreflightArgs` rather than a context of its own: it is still pre-session, so
174
+ * `chapter`, `step`, `assert`, `mask` and `pause` would have no timeline to write to and are
175
+ * absent for the same reason they are absent from pre-flight. A `waitFor` or a `handoff`
176
+ * called here records under the pre-flight phase, which is the honest phase for anything
177
+ * that happens before the session begins.
178
+ */
179
+ type WarmupHook = {
180
+ warmup?(args: PreflightArgs): Promise<void>;
181
+ };
182
+ export type DemoDefinition = ScenarioMeta & {
183
+ run(args: DemoRunArgs): Promise<void>;
184
+ } & PreflightHook & WarmupHook;
185
+ /**
186
+ * The metadata fields `ScenarioMetaSchema` supplies a `.default()` for, and which an author
187
+ * may therefore leave out. Named rather than spelled inline twice, so adding a defaulted
188
+ * field is one edit here instead of two that can drift apart.
189
+ */
190
+ type Defaulted = 'language' | 'allowedConsoleErrors' | 'handoff' | 'handoffTimeoutMs';
191
+ export type DemoDefinitionInput = Omit<ScenarioMeta, Defaulted> & Partial<Pick<ScenarioMeta, Defaulted>> & {
192
+ run(args: DemoRunArgs): Promise<void>;
193
+ } & PreflightHook & WarmupHook;
194
+ export {};
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@plaintake/scenario",
3
+ "version": "1.2.1",
4
+ "description": "Authoring SDK for PlainTake demo scenarios: defineDemo and the scenario DSL types. Install for editor autocomplete; the PlainTake binary ships a runtime fallback.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "dependencies": {
22
+ "playwright-core": "1.62.1",
23
+ "zod": "4.4.3"
24
+ },
25
+ "devDependencies": {
26
+ "@plaintake/schema": "0.0.0"
27
+ },
28
+ "scripts": {}
29
+ }