@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.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/define.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +753 -0
- package/dist/schema/errors.d.ts +20 -0
- package/dist/schema/event.d.ts +58 -0
- package/dist/schema/index.d.ts +9 -0
- package/dist/schema/json-schema.d.ts +9 -0
- package/dist/schema/manifest.d.ts +46 -0
- package/dist/schema/render-plan.d.ts +454 -0
- package/dist/schema/results.d.ts +335 -0
- package/dist/schema/scenario.d.ts +89 -0
- package/dist/schema/stable-json.d.ts +4 -0
- package/dist/schema/zod-format.d.ts +8 -0
- package/dist/types.d.ts +194 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
// ../schema/src/errors.ts
|
|
2
|
+
var EXIT_CODES = {
|
|
3
|
+
ok: 0,
|
|
4
|
+
assertion: 1,
|
|
5
|
+
usage: 2,
|
|
6
|
+
toolchain: 3,
|
|
7
|
+
capture: 4,
|
|
8
|
+
render: 5,
|
|
9
|
+
verification: 6
|
|
10
|
+
};
|
|
11
|
+
var DemoError = class extends Error {
|
|
12
|
+
kind;
|
|
13
|
+
exitCode;
|
|
14
|
+
details;
|
|
15
|
+
constructor(kind, message, details = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "DemoError";
|
|
18
|
+
this.kind = kind;
|
|
19
|
+
this.exitCode = EXIT_CODES[kind];
|
|
20
|
+
this.details = details;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ../schema/src/event.ts
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
var NanosString = z.string().regex(/^\d+$/, "must be a digit-only nanosecond string");
|
|
27
|
+
var DemoEventTypeSchema = z.enum([
|
|
28
|
+
"session.start",
|
|
29
|
+
"session.finish",
|
|
30
|
+
"chapter",
|
|
31
|
+
"step.start",
|
|
32
|
+
"step.finish",
|
|
33
|
+
"assertion.pass",
|
|
34
|
+
"assertion.fail",
|
|
35
|
+
"privacy.mask",
|
|
36
|
+
/*
|
|
37
|
+
* A bounded stretch where the recorder was waiting on something it does not drive.
|
|
38
|
+
* One member, not two, because a handoff and a plain condition answer the same question a
|
|
39
|
+
* reader of events.ndjson has — *why is the video sitting still here?* — and `payload.kind`
|
|
40
|
+
* separates them exactly as `step.finish` already carries `holdMs`.
|
|
41
|
+
*
|
|
42
|
+
* Appended once, when the wait ends, so `tEndNs` is a real reading rather than a
|
|
43
|
+
* placeholder: the `assertion.pass` shape, not the `step.start`/`step.finish` pair.
|
|
44
|
+
*/
|
|
45
|
+
"human.wait"
|
|
46
|
+
]);
|
|
47
|
+
var TargetSchema = z.object({
|
|
48
|
+
role: z.string().optional(),
|
|
49
|
+
accessibleName: z.string().optional(),
|
|
50
|
+
selector: z.string().optional(),
|
|
51
|
+
rect: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() }).optional()
|
|
52
|
+
});
|
|
53
|
+
var DemoEventSchema = z.object({
|
|
54
|
+
schema: z.literal("agent-demo.event/v1"),
|
|
55
|
+
seq: z.number().int().nonnegative(),
|
|
56
|
+
tStartNs: NanosString,
|
|
57
|
+
tEndNs: NanosString.optional(),
|
|
58
|
+
type: DemoEventTypeSchema,
|
|
59
|
+
stableId: z.string().min(1).optional(),
|
|
60
|
+
title: z.string().optional(),
|
|
61
|
+
subtitle: z.string().optional(),
|
|
62
|
+
target: TargetSchema.optional(),
|
|
63
|
+
payload: z.record(z.string(), z.unknown()).optional()
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ../schema/src/json-schema.ts
|
|
67
|
+
import { z as z3 } from "zod";
|
|
68
|
+
|
|
69
|
+
// ../schema/src/scenario.ts
|
|
70
|
+
import { z as z2 } from "zod";
|
|
71
|
+
var ScenarioIntroSchema = z2.object({
|
|
72
|
+
lines: z2.array(z2.string().min(1)).min(1).max(2, "intro.lines takes at most two lines \u2014 a third would have to shrink the type to fit"),
|
|
73
|
+
narration: z2.string().min(1).optional(),
|
|
74
|
+
durationMs: z2.number().int().positive().max(15e3, "intro.durationMs must be at most 15000ms").optional()
|
|
75
|
+
});
|
|
76
|
+
var ScenarioCameraSchema = z2.object({
|
|
77
|
+
maxZoom: z2.number().min(1, "camera.maxZoom must be at least 1 (no zoom)").max(1.6, "camera.maxZoom must be at most 1.6 \u2014 beyond that the upscaled capture turns UI text to mush").optional(),
|
|
78
|
+
margin: z2.number().min(0, "camera.margin must be at least 0").max(2, "camera.margin must be at most 2 (as a fraction of the target, per side)").optional(),
|
|
79
|
+
easeMs: z2.number().int().min(133, "camera.easeMs must be at least 133ms").max(2e3, "camera.easeMs must be at most 2000ms").optional(),
|
|
80
|
+
minDwellMs: z2.number().int().min(0, "camera.minDwellMs must be at least 0").max(5e3, "camera.minDwellMs must be at most 5000ms").optional()
|
|
81
|
+
});
|
|
82
|
+
var ScenarioMetaSchema = z2.object({
|
|
83
|
+
schema: z2.literal("agent-demo.scenario/v1"),
|
|
84
|
+
id: z2.string().regex(/^[a-z0-9][a-z0-9-]*$/, "must be a lowercase kebab-case id"),
|
|
85
|
+
title: z2.string().min(1),
|
|
86
|
+
language: z2.string().min(2).default("en"),
|
|
87
|
+
viewport: z2.object({
|
|
88
|
+
width: z2.literal(1920),
|
|
89
|
+
height: z2.literal(1080),
|
|
90
|
+
deviceScaleFactor: z2.literal(1)
|
|
91
|
+
}),
|
|
92
|
+
locale: z2.literal("en-US"),
|
|
93
|
+
timezoneId: z2.literal("UTC"),
|
|
94
|
+
colorScheme: z2.literal("light"),
|
|
95
|
+
reducedMotion: z2.literal("reduce"),
|
|
96
|
+
/** Exact console error strings the scenario tolerates. Anything else fails the run. */
|
|
97
|
+
allowedConsoleErrors: z2.array(z2.string()).default([]),
|
|
98
|
+
/*
|
|
99
|
+
* Whether this scenario hands the real browser window to a person, and in which phase.
|
|
100
|
+
*
|
|
101
|
+
* Declared here rather than discovered when `demo.handoff()` is first called, because an
|
|
102
|
+
* unavailable capability must be refused *before* Chromium starts — and what a `run()`
|
|
103
|
+
* body is going to do is not knowable then. Four things read it: whether Chromium launches
|
|
104
|
+
* headed, whether a Playwright trace is recorded at all, the refusals in `runScenario`, and
|
|
105
|
+
* which phase may legally call `demo.handoff()`.
|
|
106
|
+
*
|
|
107
|
+
* Headedness derives from this and from nothing else. There is deliberately no `--headed`
|
|
108
|
+
* flag: one would let the shipped examples be recorded headed, and S2 measured that headed
|
|
109
|
+
* and headless Chromium put glyphs on different pixels — so `make reproduce` would compare
|
|
110
|
+
* two videos that were never meant to match.
|
|
111
|
+
*
|
|
112
|
+
* The same precedent as everywhere else here: a `.default()`, so every scenario written
|
|
113
|
+
* before this parses unchanged.
|
|
114
|
+
*/
|
|
115
|
+
handoff: z2.enum(["none", "preflight", "session"]).default("none"),
|
|
116
|
+
/*
|
|
117
|
+
* How long a handoff waits for the person before giving up. Not a patience limit —
|
|
118
|
+
* past a few minutes, a block is indistinguishable from a hang, and the person is better
|
|
119
|
+
* served by a failure that names the wait than by a recorder that sits there.
|
|
120
|
+
*
|
|
121
|
+
* In the schema rather than only in the runtime so `validate` catches an out-of-range
|
|
122
|
+
* value with no browser and no waiting.
|
|
123
|
+
*/
|
|
124
|
+
handoffTimeoutMs: z2.number().int().min(5e3, "handoffTimeoutMs must be at least 5000ms").max(
|
|
125
|
+
3e5,
|
|
126
|
+
"handoffTimeoutMs must be at most 300000ms (5 minutes) \u2014 past that a wait is indistinguishable from a hang"
|
|
127
|
+
).default(12e4),
|
|
128
|
+
/*
|
|
129
|
+
* The opening card. Optional on the same precedent, like `handoff` above: every scenario
|
|
130
|
+
* written before this parses unchanged, and one that declares no intro renders exactly
|
|
131
|
+
* the video it always did.
|
|
132
|
+
*
|
|
133
|
+
* Note this is the field that makes an intro *possible* at all — Zod strips unknown keys
|
|
134
|
+
* (`scenario/src/define.ts`), so before it existed an `intro:` block in a demo file was
|
|
135
|
+
* discarded in silence rather than refused.
|
|
136
|
+
*/
|
|
137
|
+
intro: ScenarioIntroSchema.optional(),
|
|
138
|
+
/*
|
|
139
|
+
* Framing overrides for this scenario's pages. Optional like everything above it, and
|
|
140
|
+
* inert when absent: the defaults are the constants the camera has always used.
|
|
141
|
+
*
|
|
142
|
+
* Note this is read only when the camera is on at all (`--camera zoom`). A scenario
|
|
143
|
+
* recorded flat carries these harmlessly.
|
|
144
|
+
*/
|
|
145
|
+
camera: ScenarioCameraSchema.optional()
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ../schema/src/manifest.ts
|
|
149
|
+
import { z as z4 } from "zod";
|
|
150
|
+
var Sha256 = z4.string().regex(/^[0-9a-f]{64}$/);
|
|
151
|
+
var BundleManifestSchema = z4.object({
|
|
152
|
+
schema: z4.literal("agent-demo.bundle/v1"),
|
|
153
|
+
status: z4.enum(["passed", "failed"]),
|
|
154
|
+
scenario: z4.object({ id: z4.string().min(1), sourceSha256: Sha256 }),
|
|
155
|
+
toolchain: z4.object({
|
|
156
|
+
node: z4.string(),
|
|
157
|
+
playwright: z4.string(),
|
|
158
|
+
chromiumRevision: z4.string(),
|
|
159
|
+
ffmpeg: z4.string(),
|
|
160
|
+
/** May be the literal "unknown" — never a fabricated version. */
|
|
161
|
+
libass: z4.string(),
|
|
162
|
+
fontSha256: Sha256,
|
|
163
|
+
containerImage: z4.string().optional()
|
|
164
|
+
}),
|
|
165
|
+
environment: z4.object({
|
|
166
|
+
os: z4.string(),
|
|
167
|
+
/** Part of the reproducibility claim: byte-identity holds per architecture. */
|
|
168
|
+
architecture: z4.string(),
|
|
169
|
+
locale: z4.literal("en-US"),
|
|
170
|
+
timezone: z4.literal("UTC"),
|
|
171
|
+
viewport: z4.tuple([z4.literal(1920), z4.literal(1080)]),
|
|
172
|
+
deviceScaleFactor: z4.literal(1)
|
|
173
|
+
}),
|
|
174
|
+
assertions: z4.array(z4.object({ id: z4.string().min(1), status: z4.enum(["passed", "failed"]) })),
|
|
175
|
+
artifacts: z4.array(
|
|
176
|
+
z4.object({
|
|
177
|
+
path: z4.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "must be a relative POSIX path"),
|
|
178
|
+
sha256: Sha256,
|
|
179
|
+
bytes: z4.number().int().nonnegative(),
|
|
180
|
+
role: z4.enum(["source", "derived"])
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ../schema/src/render-plan.ts
|
|
186
|
+
import { z as z5 } from "zod";
|
|
187
|
+
var CueSchema = z5.object({
|
|
188
|
+
id: z5.string().min(1),
|
|
189
|
+
startMs: z5.number().int().nonnegative(),
|
|
190
|
+
endMs: z5.number().int().positive(),
|
|
191
|
+
lines: z5.array(z5.string()).min(1).max(2)
|
|
192
|
+
});
|
|
193
|
+
var HEX_COLOUR = z5.string().regex(/^#[0-9A-Fa-f]{6}$/, "must be a #RRGGBB colour");
|
|
194
|
+
var OutroSchema = z5.object({
|
|
195
|
+
durationMs: z5.number().int().positive().max(15e3),
|
|
196
|
+
lines: z5.array(z5.string().min(1)).min(1).max(2),
|
|
197
|
+
backgroundColor: HEX_COLOUR,
|
|
198
|
+
textColor: HEX_COLOUR,
|
|
199
|
+
assPath: z5.literal("captions/outro.ass")
|
|
200
|
+
});
|
|
201
|
+
var IntroSchema = z5.object({
|
|
202
|
+
durationMs: z5.number().int().positive().max(15e3),
|
|
203
|
+
lines: z5.array(z5.string().min(1)).min(1).max(2),
|
|
204
|
+
backgroundColor: HEX_COLOUR,
|
|
205
|
+
textColor: HEX_COLOUR,
|
|
206
|
+
assPath: z5.literal("captions/intro.ass")
|
|
207
|
+
});
|
|
208
|
+
var ChapterMarkSchema = z5.object({
|
|
209
|
+
startMs: z5.number().int().nonnegative(),
|
|
210
|
+
endMs: z5.number().int().positive(),
|
|
211
|
+
title: z5.string().min(1)
|
|
212
|
+
});
|
|
213
|
+
var ChaptersSchema = z5.object({
|
|
214
|
+
metadataPath: z5.literal("render/chapters.ffmetadata"),
|
|
215
|
+
marks: z5.array(ChapterMarkSchema).min(1)
|
|
216
|
+
}).refine(
|
|
217
|
+
({ marks }) => marks[0]?.startMs === 0 && marks.every(
|
|
218
|
+
(mark, index) => mark.endMs > mark.startMs && (index === 0 || marks[index - 1]?.endMs === mark.startMs)
|
|
219
|
+
),
|
|
220
|
+
{
|
|
221
|
+
// Contiguity is enforced rather than tolerated: a gap or an overlap produces a
|
|
222
|
+
// chapter list that players render inconsistently, and there is no legitimate way
|
|
223
|
+
// for the derivation to generate one.
|
|
224
|
+
message: "marks must start at 0 and tile the content contiguously",
|
|
225
|
+
path: ["marks"]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
var CursorPointSchema = z5.object({
|
|
229
|
+
id: z5.string().min(1),
|
|
230
|
+
x: z5.number().int().min(0).max(1920),
|
|
231
|
+
y: z5.number().int().min(0).max(1080),
|
|
232
|
+
arriveMs: z5.number().int().nonnegative(),
|
|
233
|
+
departMs: z5.number().int().nonnegative(),
|
|
234
|
+
action: z5.enum(["click", "type", "point"]),
|
|
235
|
+
rippleMs: z5.number().int().nonnegative().optional(),
|
|
236
|
+
/**
|
|
237
|
+
* Counter-scale percent, so the cursor stays one size on screen while the camera
|
|
238
|
+
* magnifies its window — up to 1.6x — and the emitter compensates with `\fscx`/`\fscy`.
|
|
239
|
+
* Absent means 100, so it carries no default: a cursor bundle from before the camera
|
|
240
|
+
* existed still parses and regenerates byte-for-byte.
|
|
241
|
+
*/
|
|
242
|
+
scale: z5.number().int().min(1).max(100).optional()
|
|
243
|
+
});
|
|
244
|
+
var CursorSchema = z5.object({
|
|
245
|
+
assPath: z5.literal("captions/cursor.ass"),
|
|
246
|
+
points: z5.array(CursorPointSchema).min(1)
|
|
247
|
+
}).refine(
|
|
248
|
+
({ points }) => points.every(
|
|
249
|
+
(point, index) => point.arriveMs <= point.departMs && (point.rippleMs === void 0 || point.rippleMs >= point.arriveMs) && (index === 0 || points[index - 1].departMs <= point.arriveMs && points[index - 1].arriveMs < point.arriveMs)
|
|
250
|
+
),
|
|
251
|
+
{
|
|
252
|
+
// The derivation emits points in event order, one per targeted step, so an overlap
|
|
253
|
+
// or an out-of-order point can only mean the derivation is broken — fail the plan
|
|
254
|
+
// rather than render a cursor that travels backwards in time.
|
|
255
|
+
message: "points must be ordered, arrive before departing, and not overlap",
|
|
256
|
+
path: ["points"]
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
var CameraShotSchema = z5.object({
|
|
260
|
+
id: z5.string().min(1),
|
|
261
|
+
enterMs: z5.number().int().nonnegative(),
|
|
262
|
+
holdFromMs: z5.number().int().nonnegative(),
|
|
263
|
+
x: z5.number().int().min(0),
|
|
264
|
+
y: z5.number().int().min(0),
|
|
265
|
+
w: z5.number().int().positive(),
|
|
266
|
+
h: z5.number().int().positive()
|
|
267
|
+
});
|
|
268
|
+
var CameraSchema = z5.object({
|
|
269
|
+
commandPath: z5.literal("render/camera.cmd"),
|
|
270
|
+
shots: z5.array(CameraShotSchema).min(1)
|
|
271
|
+
}).refine(
|
|
272
|
+
({ shots }) => shots.every(
|
|
273
|
+
(shot) => shot.x % 2 === 0 && shot.y % 2 === 0 && // Implied by the %32 and 9w/16 rules below; kept so a future relaxation of
|
|
274
|
+
// either cannot silently reintroduce M10's odd-geometry masking.
|
|
275
|
+
shot.w % 2 === 0 && shot.h % 2 === 0 && shot.w % 32 === 0 && shot.h === shot.w * 9 / 16 && shot.x + shot.w <= 1920 && shot.y + shot.h <= 1080
|
|
276
|
+
),
|
|
277
|
+
{
|
|
278
|
+
// Odd geometry is silently masked down on yuv420p (M10), and a window that is not
|
|
279
|
+
// exact 16:9 or leaves the 1920x1080 frame cannot feed the `scale=1920:1080` the
|
|
280
|
+
// camera command is built around. Reject at parse time rather than discover it in
|
|
281
|
+
// a rendered frame.
|
|
282
|
+
message: "every shot window must be even in x, y, w and h (M10), w a multiple of 32, h exactly 9w/16, and inside the 1920x1080 frame",
|
|
283
|
+
path: ["shots"]
|
|
284
|
+
}
|
|
285
|
+
).refine(
|
|
286
|
+
({ shots }) => shots[0]?.enterMs === 0 && shots[0]?.holdFromMs === 0 && shots[0]?.x === 0 && shots[0]?.y === 0 && shots[0]?.w === 1920 && shots[0]?.h === 1080 && shots.every(
|
|
287
|
+
(shot, index) => shot.holdFromMs >= shot.enterMs && (index === 0 || shots[index - 1].holdFromMs <= shot.enterMs)
|
|
288
|
+
),
|
|
289
|
+
{
|
|
290
|
+
// The shots are the timeline: a gap between one shot's hold and the next shot's
|
|
291
|
+
// ease would leave the previous window on screen with nothing frozen saying so, and
|
|
292
|
+
// pinning shot 0 to the full frame keeps "no zoom yet" a shot rather than an
|
|
293
|
+
// implicit default the renderer would have to know about.
|
|
294
|
+
message: "shots must tile the timeline: shot 0 is the full frame held from 0, each shot enters at or after the previous shot is held, and no ease ends before it starts",
|
|
295
|
+
path: ["shots"]
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
var SpeechClipSchema = z5.object({
|
|
299
|
+
id: z5.string().min(1),
|
|
300
|
+
/*
|
|
301
|
+
* Bundle-relative and unescapable by construction: no slash, no leading dot, so `..` and
|
|
302
|
+
* an absolute path are both unrepresentable. The renderer's `assertBundleRelative` says
|
|
303
|
+
* the same thing at the point of use; this says it at the point of freezing.
|
|
304
|
+
*
|
|
305
|
+
* The grammar is deliberately a *subset* of `assertBundleRelative`'s, which is why `:` is
|
|
306
|
+
* excluded even though a file name may legally contain one. A colon is forbidden in any path
|
|
307
|
+
* that reaches an FFmpeg argument, and two guards that disagree are worse than either alone:
|
|
308
|
+
* the looser one is upstream, so a plan carrying `speech/clips/step:3.wav` would parse, be
|
|
309
|
+
* frozen, and only then be refused at render time — a whole recording thrown away for a
|
|
310
|
+
* character the recorder could have replaced. `narration.test.ts` asserts the two agree on a
|
|
311
|
+
* table of candidates, because a comment cannot.
|
|
312
|
+
*/
|
|
313
|
+
path: z5.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
|
|
314
|
+
atMs: z5.number().int().nonnegative(),
|
|
315
|
+
durationMs: z5.number().int().positive(),
|
|
316
|
+
source: z5.enum(["synth", "file"])
|
|
317
|
+
});
|
|
318
|
+
var SpeechEngineSchema = z5.object({
|
|
319
|
+
name: z5.string().min(1),
|
|
320
|
+
version: z5.string().min(1),
|
|
321
|
+
modelSha256: z5.string().regex(/^[0-9a-f]{64}$/),
|
|
322
|
+
voice: z5.string().min(1)
|
|
323
|
+
});
|
|
324
|
+
var SpeechSchema = z5.object({
|
|
325
|
+
trackPath: z5.literal("speech/narration.wav"),
|
|
326
|
+
/*
|
|
327
|
+
* 24 kHz mono, because that is what Kokoro emits and nothing on this path resamples.
|
|
328
|
+
* Written as literals rather than imported from `@plaintake/audio`, which owns the
|
|
329
|
+
* format: that package depends on this one for `DemoError`, so importing it back would
|
|
330
|
+
* be the cycle `no-circular` refuses. `packages/audio/src/wav.test.ts` asserts the two
|
|
331
|
+
* agree, since a comment cannot.
|
|
332
|
+
*/
|
|
333
|
+
sampleRate: z5.literal(24e3),
|
|
334
|
+
channels: z5.literal(1),
|
|
335
|
+
clips: z5.array(SpeechClipSchema).min(1),
|
|
336
|
+
engine: SpeechEngineSchema.optional()
|
|
337
|
+
}).refine(
|
|
338
|
+
({ clips }) => new Set(clips.map((clip) => clip.id)).size === clips.length && clips.every(
|
|
339
|
+
(clip, index) => index === 0 || clips[index - 1].atMs + clips[index - 1].durationMs <= clip.atMs
|
|
340
|
+
),
|
|
341
|
+
{
|
|
342
|
+
/*
|
|
343
|
+
* Ordered and non-overlapping, enforced rather than tolerated — which is also what makes
|
|
344
|
+
* the mixer a *layout* rather than a mix, with no summing, no clipping and no limiter to
|
|
345
|
+
* make deterministic. Two clips overlapping would be two voices talking over each other,
|
|
346
|
+
* which no derivation should ever produce: the recorder holds each step open for its own
|
|
347
|
+
* clip. Duplicate ids would collide on one file path.
|
|
348
|
+
*/
|
|
349
|
+
message: "clips must have unique ids and be laid out in order without overlapping",
|
|
350
|
+
path: ["clips"]
|
|
351
|
+
}
|
|
352
|
+
).refine(({ clips, engine }) => engine !== void 0 || clips.every((clip) => clip.source === "file"), {
|
|
353
|
+
// A synthesised clip with no record of what synthesised it is the one state this block
|
|
354
|
+
// exists to prevent. The converse is fine: an engine recorded on an all-file track is
|
|
355
|
+
// merely redundant, not misleading.
|
|
356
|
+
message: "engine must be recorded whenever any clip was synthesised",
|
|
357
|
+
path: ["engine"]
|
|
358
|
+
});
|
|
359
|
+
var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
|
|
360
|
+
var RenderPlanSchema = z5.object({
|
|
361
|
+
schema: z5.literal(RENDER_PLAN_SCHEMA),
|
|
362
|
+
source: z5.object({
|
|
363
|
+
path: z5.literal("raw/session.webm"),
|
|
364
|
+
sha256: z5.string().regex(/^[0-9a-f]{64}$/),
|
|
365
|
+
durationMs: z5.number().int().nonnegative()
|
|
366
|
+
}),
|
|
367
|
+
video: z5.object({
|
|
368
|
+
width: z5.literal(1920),
|
|
369
|
+
height: z5.literal(1080),
|
|
370
|
+
fps: z5.literal(30),
|
|
371
|
+
pixelFormat: z5.literal("yuv420p"),
|
|
372
|
+
/*
|
|
373
|
+
* Everything before the closing card: the opening card, if there is one, plus the
|
|
374
|
+
* recorded content. Not the content alone.
|
|
375
|
+
*
|
|
376
|
+
* That is the definition the rest of the plan already depends on, rather than a
|
|
377
|
+
* convenience. Every other timestamp here — cue, cursor point, camera shot, chapter
|
|
378
|
+
* mark — is an offset into the finished video, so the number they are bounded by has to
|
|
379
|
+
* be one too. It is also where the outro starts (`writeCaptions`), where the cursor and
|
|
380
|
+
* camera tracks are told the picture ends, and what the last chapter mark tiles up to;
|
|
381
|
+
* defining it as the content alone would have put an intro's length into all four
|
|
382
|
+
* call sites separately.
|
|
383
|
+
*
|
|
384
|
+
* With no intro it is exactly what it always was, which is what keeps a plan without
|
|
385
|
+
* one byte-identical to what earlier versions wrote.
|
|
386
|
+
*/
|
|
387
|
+
durationMs: z5.number().int().positive(),
|
|
388
|
+
tailPadMs: z5.number().int().nonnegative()
|
|
389
|
+
}),
|
|
390
|
+
captions: z5.object({
|
|
391
|
+
language: z5.string().min(2),
|
|
392
|
+
srtPath: z5.literal("captions/captions.srt"),
|
|
393
|
+
vttPath: z5.literal("captions/captions.vtt"),
|
|
394
|
+
assPath: z5.literal("captions/captions.ass"),
|
|
395
|
+
cues: z5.array(CueSchema)
|
|
396
|
+
}),
|
|
397
|
+
/**
|
|
398
|
+
* How the burned-in captions were drawn. This is the **source** of
|
|
399
|
+
* `captions/captions.ass` rather than a note written alongside it, so a bundle describes
|
|
400
|
+
* its own appearance and the two cannot disagree.
|
|
401
|
+
*
|
|
402
|
+
* Every field added here carries a default, and each default is the value that preceded
|
|
403
|
+
* it: an opaque outline, no box, `BorderStyle 1`. A plan frozen before this therefore
|
|
404
|
+
* parses to exactly the style it was rendered with. It was never a compatibility event in
|
|
405
|
+
* either direction — a render consumes the frozen ASS file rather than this block, so an
|
|
406
|
+
* older reader ignoring fields it does not know still executes the same frozen arguments
|
|
407
|
+
* over the same frozen ASS. That observation generalises to the schema as a whole.
|
|
408
|
+
*/
|
|
409
|
+
style: z5.object({
|
|
410
|
+
fontFile: z5.literal("assets/fonts/NotoSans-Regular.ttf"),
|
|
411
|
+
fontName: z5.literal("Noto Sans"),
|
|
412
|
+
fontSize: z5.number().int().positive(),
|
|
413
|
+
textColor: z5.string(),
|
|
414
|
+
outlineColor: z5.string(),
|
|
415
|
+
/** ASS `Outline`: a stroke width under `BorderStyle 1`, box padding under `BorderStyle 4`. */
|
|
416
|
+
outlineWidth: z5.number().int().nonnegative(),
|
|
417
|
+
marginBottom: z5.number().int().nonnegative(),
|
|
418
|
+
marginSide: z5.number().int().nonnegative().default(80),
|
|
419
|
+
borderStyle: z5.union([z5.literal(1), z5.literal(4)]).default(1),
|
|
420
|
+
outlineOpacity: z5.number().min(0).max(1).default(1),
|
|
421
|
+
boxColor: HEX_COLOUR.default("#000000"),
|
|
422
|
+
/*
|
|
423
|
+
* Opaque, which under the `BorderStyle 1` default draws nothing at all: `BackColour` is
|
|
424
|
+
* the *shadow* colour there, and `Shadow` is 0. `&H00000000` is both the historical value
|
|
425
|
+
* and ASS's own default, so a plan frozen before this field existed regenerates its ASS
|
|
426
|
+
* byte-for-byte rather than byte-for-byte-except-one-field-nobody-can-see.
|
|
427
|
+
*/
|
|
428
|
+
boxOpacity: z5.number().min(0).max(1).default(1)
|
|
429
|
+
}),
|
|
430
|
+
ffmpeg: z5.object({
|
|
431
|
+
base: z5.array(z5.string()),
|
|
432
|
+
soft: z5.array(z5.string()),
|
|
433
|
+
hard: z5.array(z5.string())
|
|
434
|
+
}),
|
|
435
|
+
intro: IntroSchema.optional(),
|
|
436
|
+
outro: OutroSchema.optional(),
|
|
437
|
+
chapters: ChaptersSchema.optional(),
|
|
438
|
+
cursor: CursorSchema.optional(),
|
|
439
|
+
camera: CameraSchema.optional(),
|
|
440
|
+
speech: SpeechSchema.optional()
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ../schema/src/results.ts
|
|
444
|
+
import { z as z6 } from "zod";
|
|
445
|
+
var RELATIVE_POSIX = z6.string().regex(/^[A-Za-z0-9.][A-Za-z0-9._/-]*$/, "must be a relative POSIX path").refine((value) => !value.split("/").includes(".."), "must not traverse upwards");
|
|
446
|
+
var Sha2562 = z6.string().regex(/^[0-9a-f]{64}$/);
|
|
447
|
+
var DISPLAY_PATH = z6.string().min(1);
|
|
448
|
+
var envelope = (kind) => ({
|
|
449
|
+
schema: z6.literal("agent-demo.result/v1"),
|
|
450
|
+
kind: z6.literal(kind),
|
|
451
|
+
ok: z6.boolean(),
|
|
452
|
+
/** Human-readable failures. Empty iff ok. */
|
|
453
|
+
problems: z6.array(z6.string())
|
|
454
|
+
});
|
|
455
|
+
var ArtifactRefSchema = z6.object({
|
|
456
|
+
path: RELATIVE_POSIX,
|
|
457
|
+
sha256: Sha2562,
|
|
458
|
+
bytes: z6.number().int().nonnegative()
|
|
459
|
+
});
|
|
460
|
+
var StreamSummarySchema = z6.object({
|
|
461
|
+
container: z6.enum(["mp4", "webm", "other"]),
|
|
462
|
+
durationMs: z6.number().int().nonnegative(),
|
|
463
|
+
video: z6.object({
|
|
464
|
+
codec: z6.string(),
|
|
465
|
+
width: z6.number().int(),
|
|
466
|
+
height: z6.number().int(),
|
|
467
|
+
pixelFormat: z6.string(),
|
|
468
|
+
fps: z6.number().int(),
|
|
469
|
+
frames: z6.number().int(),
|
|
470
|
+
/**
|
|
471
|
+
* The video stream's own duration, as distinct from the container's above. The
|
|
472
|
+
* constant-rate check measures frames against this, because a padded narration track makes
|
|
473
|
+
* the container's duration the audio's. Zero for a WebM, which carries no per-track
|
|
474
|
+
* duration at all.
|
|
475
|
+
*/
|
|
476
|
+
durationMs: z6.number().int().nonnegative()
|
|
477
|
+
}),
|
|
478
|
+
subtitles: z6.array(z6.object({ codec: z6.string(), language: z6.string(), title: z6.string() })),
|
|
479
|
+
/** Properties, not a count — a narration track muxed in stereo or at the wrong rate
|
|
480
|
+
* still counts as one stream. */
|
|
481
|
+
audio: z6.array(
|
|
482
|
+
z6.object({
|
|
483
|
+
codec: z6.string(),
|
|
484
|
+
sampleRate: z6.number().int().nonnegative(),
|
|
485
|
+
channels: z6.number().int().nonnegative()
|
|
486
|
+
})
|
|
487
|
+
),
|
|
488
|
+
/** Titled chapters make FFmpeg's MP4 muxer add a `bin_data` track beside the `chpl` atom. */
|
|
489
|
+
dataStreams: z6.number().int().nonnegative(),
|
|
490
|
+
chapters: z6.array(
|
|
491
|
+
z6.object({
|
|
492
|
+
startMs: z6.number().int().nonnegative(),
|
|
493
|
+
endMs: z6.number().int().nonnegative(),
|
|
494
|
+
title: z6.string()
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
});
|
|
498
|
+
var ToolchainSchema = z6.object({
|
|
499
|
+
node: z6.string(),
|
|
500
|
+
playwright: z6.string(),
|
|
501
|
+
chromiumRevision: z6.string(),
|
|
502
|
+
ffmpeg: z6.string(),
|
|
503
|
+
/** May be the literal "unknown" — never fabricated. */
|
|
504
|
+
libass: z6.string(),
|
|
505
|
+
fontSha256: Sha2562
|
|
506
|
+
});
|
|
507
|
+
var AssertionSchema = z6.object({ id: z6.string().min(1), status: z6.enum(["passed", "failed"]) });
|
|
508
|
+
var StatusSchema = z6.enum(["passed", "failed"]);
|
|
509
|
+
var VariantsSchema = z6.enum(["soft", "hard"]);
|
|
510
|
+
var ValidationResultSchema = z6.object({
|
|
511
|
+
...envelope("validate"),
|
|
512
|
+
/** Absent when the scenario could not be loaded at all. */
|
|
513
|
+
scenario: z6.object({
|
|
514
|
+
id: z6.string(),
|
|
515
|
+
title: z6.string(),
|
|
516
|
+
language: z6.string(),
|
|
517
|
+
/**
|
|
518
|
+
* Whether this scenario needs a person, and in which phase. Reported so an author
|
|
519
|
+
* — or an agent reading `--json` — learns that a scenario cannot run unattended
|
|
520
|
+
* without first trying to run it. Defaulted, so a result written before this parses.
|
|
521
|
+
*/
|
|
522
|
+
handoff: z6.enum(["none", "preflight", "session"]).default("none"),
|
|
523
|
+
sha256: Sha2562
|
|
524
|
+
}).optional()
|
|
525
|
+
});
|
|
526
|
+
var RunCommandResultSchema = z6.object({
|
|
527
|
+
...envelope("run"),
|
|
528
|
+
bundleDir: DISPLAY_PATH,
|
|
529
|
+
status: StatusSchema,
|
|
530
|
+
cueCount: z6.number().int().nonnegative(),
|
|
531
|
+
/** Zero on the Free Tier, and zero for a scenario with no `chapter` calls. */
|
|
532
|
+
chapterCount: z6.number().int().nonnegative(),
|
|
533
|
+
/**
|
|
534
|
+
* How many lines the video speaks. Zero for a silent run, which is the default.
|
|
535
|
+
*
|
|
536
|
+
* The only place this fact surfaces. A narrated bundle is otherwise indistinguishable from a
|
|
537
|
+
* silent one without probing the MP4 — and the failure worth catching is `--speech on` on a
|
|
538
|
+
* scenario whose steps carry no `subtitle`, which produces a perfectly valid silent video and
|
|
539
|
+
* would look like the feature being broken rather than like nothing having been written to say.
|
|
540
|
+
*/
|
|
541
|
+
narratedCount: z6.number().int().nonnegative(),
|
|
542
|
+
diagnostics: z6.array(z6.object({ code: z6.string(), cueId: z6.string(), detail: z6.string() })),
|
|
543
|
+
assertions: z6.array(AssertionSchema),
|
|
544
|
+
outputs: z6.array(ArtifactRefSchema)
|
|
545
|
+
});
|
|
546
|
+
var RenderCommandResultSchema = z6.object({
|
|
547
|
+
...envelope("render"),
|
|
548
|
+
bundleDir: DISPLAY_PATH,
|
|
549
|
+
variants: VariantsSchema,
|
|
550
|
+
streams: z6.object({
|
|
551
|
+
base: StreamSummarySchema.optional(),
|
|
552
|
+
soft: StreamSummarySchema.optional(),
|
|
553
|
+
hard: StreamSummarySchema.optional()
|
|
554
|
+
}),
|
|
555
|
+
outputs: z6.array(ArtifactRefSchema)
|
|
556
|
+
});
|
|
557
|
+
var VerificationReportSchema = z6.object({
|
|
558
|
+
...envelope("verify"),
|
|
559
|
+
bundleDir: DISPLAY_PATH,
|
|
560
|
+
artifactCount: z6.number().int().nonnegative()
|
|
561
|
+
});
|
|
562
|
+
var InspectResultSchema = z6.object({
|
|
563
|
+
...envelope("inspect"),
|
|
564
|
+
bundleDir: DISPLAY_PATH,
|
|
565
|
+
status: StatusSchema,
|
|
566
|
+
scenario: z6.object({ id: z6.string(), sourceSha256: Sha2562 }),
|
|
567
|
+
toolchain: ToolchainSchema,
|
|
568
|
+
video: z6.object({
|
|
569
|
+
/** The whole output: the cards, if any, plus the content between them. */
|
|
570
|
+
durationMs: z6.number().int(),
|
|
571
|
+
frames: z6.number().int(),
|
|
572
|
+
width: z6.number().int(),
|
|
573
|
+
height: z6.number().int(),
|
|
574
|
+
fps: z6.number().int(),
|
|
575
|
+
/**
|
|
576
|
+
* Split out because `durationMs` alone is misleading once a card exists: 12,034ms of
|
|
577
|
+
* output can be 9,034ms of recording. The parts are reported separately rather than
|
|
578
|
+
* leaving the reader to subtract.
|
|
579
|
+
*
|
|
580
|
+
* **These do not sum to `durationMs` exactly.** `durationMs` is measured from the
|
|
581
|
+
* rendered container while these come from the plan, and 361 frames at 30fps is 12,033ms
|
|
582
|
+
* against a planned 12,034 — under one frame of rounding. No probe of the finished MP4
|
|
583
|
+
* can separate content frames from card frames, so the split has to be declared.
|
|
584
|
+
*
|
|
585
|
+
* `contentDurationMs` is the recording alone, with both cards taken off — which is what
|
|
586
|
+
* the word means, and is worth stating because the plan's own `video.durationMs` is a
|
|
587
|
+
* different number that includes the opening card.
|
|
588
|
+
*/
|
|
589
|
+
contentDurationMs: z6.number().int().nonnegative(),
|
|
590
|
+
introDurationMs: z6.number().int().nonnegative().default(0),
|
|
591
|
+
outroDurationMs: z6.number().int().nonnegative()
|
|
592
|
+
}),
|
|
593
|
+
captions: z6.object({
|
|
594
|
+
language: z6.string(),
|
|
595
|
+
cueCount: z6.number().int().nonnegative(),
|
|
596
|
+
characterCount: z6.number().int().nonnegative(),
|
|
597
|
+
medianCueMs: z6.number().int().nonnegative(),
|
|
598
|
+
shortestCueMs: z6.number().int().nonnegative(),
|
|
599
|
+
longestCueMs: z6.number().int().nonnegative()
|
|
600
|
+
}),
|
|
601
|
+
/** Measured from the rendered MP4 where possible, declared from the plan otherwise. */
|
|
602
|
+
chapters: z6.array(z6.object({ startMs: z6.number().int().nonnegative(), title: z6.string() })),
|
|
603
|
+
/**
|
|
604
|
+
* The narration this bundle carries, or absent for a silent one.
|
|
605
|
+
*
|
|
606
|
+
* Absent rather than a zero-clip object, matching the plan field it is read from — and matching
|
|
607
|
+
* what the distinction means: a silent bundle has no narration, not an empty one.
|
|
608
|
+
*
|
|
609
|
+
* `voice` is present only when something was synthesised. An author-supplied voiceover has no
|
|
610
|
+
* model behind it, and inventing a voice id for it would make the one field that answers "why
|
|
611
|
+
* does this sound different" answer wrongly.
|
|
612
|
+
*/
|
|
613
|
+
narration: z6.object({
|
|
614
|
+
clipCount: z6.number().int().positive(),
|
|
615
|
+
/** Total speech, excluding the silence between clips. Not the video's length. */
|
|
616
|
+
spokenMs: z6.number().int().nonnegative(),
|
|
617
|
+
/** How many clips the author supplied rather than the model producing. */
|
|
618
|
+
suppliedCount: z6.number().int().nonnegative(),
|
|
619
|
+
voice: z6.string().optional()
|
|
620
|
+
}).optional(),
|
|
621
|
+
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
622
|
+
outputs: z6.array(ArtifactRefSchema),
|
|
623
|
+
assertions: z6.array(AssertionSchema),
|
|
624
|
+
artifactCount: z6.number().int().nonnegative(),
|
|
625
|
+
/** Everything the manifest accounts for. What "delete this bundle" would reclaim. */
|
|
626
|
+
bundleBytes: z6.number().int().nonnegative()
|
|
627
|
+
});
|
|
628
|
+
var DoctorResultSchema = z6.object({
|
|
629
|
+
...envelope("doctor"),
|
|
630
|
+
/**
|
|
631
|
+
* The PlainTake build this is. Here rather than in `ToolchainSchema` on purpose:
|
|
632
|
+
* that record is written into a bundle and hashed, so a version in it would change every
|
|
633
|
+
* manifest on every release and break the byte-for-byte golden bundle. `doctor` is the
|
|
634
|
+
* one command a bug report is asked for, which makes it the right place.
|
|
635
|
+
*/
|
|
636
|
+
version: z6.string(),
|
|
637
|
+
ffmpeg: z6.string(),
|
|
638
|
+
ffprobe: z6.string(),
|
|
639
|
+
libass: z6.string(),
|
|
640
|
+
hasLibass: z6.boolean(),
|
|
641
|
+
hasX264: z6.boolean(),
|
|
642
|
+
/**
|
|
643
|
+
* Whether this FFmpeg can encode AAC, which is what a narrated bundle is muxed with.
|
|
644
|
+
*
|
|
645
|
+
* Reported but **not** folded into `problems`, so its absence never fails `doctor`. AAC is
|
|
646
|
+
* needed by a bundle that carries narration and by nothing else, so a build without it is
|
|
647
|
+
* perfectly usable for the silent default — and `toolchainProblems` already refuses the render
|
|
648
|
+
* that actually needs it, with a fix string. Failing `doctor` here would be telling someone
|
|
649
|
+
* their installation is broken for a capability they may never turn on.
|
|
650
|
+
*/
|
|
651
|
+
hasAac: z6.boolean(),
|
|
652
|
+
filters: z6.array(z6.string()),
|
|
653
|
+
/**
|
|
654
|
+
* Whether spoken narration could be produced right now, and what is missing if not.
|
|
655
|
+
*
|
|
656
|
+
* Separate from `problems` for the same reason as `hasAac`: the voice model is a one-time
|
|
657
|
+
* opt-in download, so its absence is a *state*, not a fault. `ready` is the same question
|
|
658
|
+
* `openNarrator` asks before a run, answered by the same function (`speechAssetProblems`), so
|
|
659
|
+
* `doctor` cannot say speech is fine and then a run refuse it.
|
|
660
|
+
*/
|
|
661
|
+
speech: z6.object({
|
|
662
|
+
ready: z6.boolean(),
|
|
663
|
+
/** Installed voices, in catalogue order. Empty means the model cannot speak yet. */
|
|
664
|
+
voices: z6.array(z6.string()),
|
|
665
|
+
/** What is missing, in the same words a refused run would use. Empty when ready. */
|
|
666
|
+
notes: z6.array(z6.string())
|
|
667
|
+
})
|
|
668
|
+
});
|
|
669
|
+
var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
|
|
670
|
+
var ActivateReasonSchema = z6.enum([
|
|
671
|
+
"empty-key",
|
|
672
|
+
"not-found",
|
|
673
|
+
"revoked",
|
|
674
|
+
"offline",
|
|
675
|
+
"unexpected-response",
|
|
676
|
+
"not-configured"
|
|
677
|
+
]);
|
|
678
|
+
var ActivateResultSchema = z6.object({
|
|
679
|
+
...envelope("activate"),
|
|
680
|
+
/** Present only when ok is false, so a machine reader can distinguish the classes. */
|
|
681
|
+
reason: ActivateReasonSchema.optional(),
|
|
682
|
+
/** Present only on success — a failed activation has no record. The key itself is
|
|
683
|
+
* deliberately absent: the caller already has it, and stdout may be logged. */
|
|
684
|
+
licence: z6.object({
|
|
685
|
+
subject: z6.string().min(1),
|
|
686
|
+
activatedAt: z6.string().min(1),
|
|
687
|
+
uses: z6.number().int().nonnegative()
|
|
688
|
+
}).optional()
|
|
689
|
+
}).refine(okMatchesProblems, "ok must be true iff problems is empty").refine(
|
|
690
|
+
// Making the "present only when" comments above literally true, so a reader can
|
|
691
|
+
// switch on `ok` and know which optional fields exist without checking each one.
|
|
692
|
+
(value) => value.reason !== void 0 === !value.ok && (value.licence === void 0 || value.ok),
|
|
693
|
+
"reason appears iff ok is false, and licence only on success"
|
|
694
|
+
);
|
|
695
|
+
var LicenceResultSchema = z6.discriminatedUnion("tier", [
|
|
696
|
+
z6.object({
|
|
697
|
+
...envelope("licence"),
|
|
698
|
+
tier: z6.literal("free"),
|
|
699
|
+
/**
|
|
700
|
+
* Why a present-but-unusable record degraded to free, in readLicense's own words.
|
|
701
|
+
* Must never quote a filesystem path: assertNoAbsolutePaths exempts only `problems`.
|
|
702
|
+
*/
|
|
703
|
+
problem: z6.string().optional()
|
|
704
|
+
}),
|
|
705
|
+
z6.object({
|
|
706
|
+
...envelope("licence"),
|
|
707
|
+
tier: z6.literal("pro"),
|
|
708
|
+
subject: z6.string().min(1),
|
|
709
|
+
activatedAt: z6.string().min(1),
|
|
710
|
+
uses: z6.number().int().nonnegative()
|
|
711
|
+
})
|
|
712
|
+
]).refine(okMatchesProblems, "ok must be true iff problems is empty");
|
|
713
|
+
|
|
714
|
+
// ../schema/src/zod-format.ts
|
|
715
|
+
import { ZodError } from "zod";
|
|
716
|
+
function isZodError(error) {
|
|
717
|
+
return error instanceof ZodError;
|
|
718
|
+
}
|
|
719
|
+
function formatZodError(error) {
|
|
720
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/define.ts
|
|
724
|
+
function defineDemo(input) {
|
|
725
|
+
if (typeof input?.run !== "function") {
|
|
726
|
+
throw new DemoError("usage", "defineDemo requires a run function");
|
|
727
|
+
}
|
|
728
|
+
if (input.preflight !== void 0 && typeof input.preflight !== "function") {
|
|
729
|
+
throw new DemoError("usage", "defineDemo: preflight must be a function when present");
|
|
730
|
+
}
|
|
731
|
+
if (input.warmup !== void 0 && typeof input.warmup !== "function") {
|
|
732
|
+
throw new DemoError("usage", "defineDemo: warmup must be a function when present");
|
|
733
|
+
}
|
|
734
|
+
let meta;
|
|
735
|
+
try {
|
|
736
|
+
meta = ScenarioMetaSchema.parse(input);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if (isZodError(error)) throw new DemoError("usage", formatZodError(error));
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
...meta,
|
|
743
|
+
run: input.run,
|
|
744
|
+
// Re-attached conditionally, so a scenario without one has no `preflight` key at all
|
|
745
|
+
// rather than a key whose value is `undefined` — `record()` tests for the key.
|
|
746
|
+
// `warmup` follows the same rule for the same reason.
|
|
747
|
+
...input.preflight === void 0 ? {} : { preflight: input.preflight },
|
|
748
|
+
...input.warmup === void 0 ? {} : { warmup: input.warmup }
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
export {
|
|
752
|
+
defineDemo
|
|
753
|
+
};
|