@liustack/pptfast 0.1.0 → 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.
@@ -0,0 +1,564 @@
1
+ import {
2
+ AssetsSchema,
3
+ BrandSchema,
4
+ CAPACITY,
5
+ COMPONENT_TYPES,
6
+ LAYOUT_REGISTRY,
7
+ MetaSchema,
8
+ NarrativeProfileInputSchema,
9
+ PptfastError,
10
+ PptxIRSchema,
11
+ STRATEGY_DEFINITIONS,
12
+ SlideSchema,
13
+ ThemeSchema,
14
+ getInstalledThemeIds,
15
+ resolveEffectiveLayoutId,
16
+ resolveNarrative
17
+ } from "./chunk-7N4HGSMW.js";
18
+
19
+ // src/version.ts
20
+ var VERSION = "0.4.0";
21
+
22
+ // src/ir/legacy-v3.ts
23
+ import { z } from "zod";
24
+ var PptxIRV3Schema = z.object({
25
+ version: z.literal("3").default("3"),
26
+ filename: z.string().default("presentation"),
27
+ // Pre-rename field name and axis vocabulary (mode/delivery/audience) —
28
+ // frozen as of the 0.3.0 release, spec §9.3. Same open-schema/closed-
29
+ // semantic split `NarrativeProfileInputSchema` documents: the actual
30
+ // mode/delivery/audience enum closure happened at `resolveScenario`
31
+ // runtime, not here, even before this rename.
32
+ scenario: z.union([z.string(), NarrativeProfileInputSchema]).optional(),
33
+ theme: ThemeSchema.default({ id: "consulting" }),
34
+ meta: MetaSchema.default({}),
35
+ assets: AssetsSchema.default({ images: {} }),
36
+ brand: BrandSchema.optional(),
37
+ seed: z.number().int().optional(),
38
+ slides: z.array(SlideSchema)
39
+ }).strict();
40
+
41
+ // src/ir/migrate.ts
42
+ var STRATEGY_VALUE_MIGRATION = { narrative: "storytelling" };
43
+ var PACING_VALUE_MIGRATION = { text: "dense", presentation: "spacious" };
44
+ function migrateNarrativeInput(scenario) {
45
+ if (scenario === void 0 || typeof scenario === "string") return scenario;
46
+ const narrative = {};
47
+ for (const [key, value] of Object.entries(scenario)) {
48
+ if (key === "mode") {
49
+ narrative.strategy = typeof value === "string" ? STRATEGY_VALUE_MIGRATION[value] ?? value : value;
50
+ } else if (key === "delivery") {
51
+ narrative.pacing = typeof value === "string" ? PACING_VALUE_MIGRATION[value] ?? value : value;
52
+ } else if (key === "audience") {
53
+ narrative.audience = value;
54
+ } else {
55
+ narrative[key] = value;
56
+ }
57
+ }
58
+ return narrative;
59
+ }
60
+ function migrateIrV3ToV4(v3) {
61
+ const narrative = migrateNarrativeInput(v3.scenario);
62
+ return {
63
+ version: "4",
64
+ filename: v3.filename,
65
+ ...narrative !== void 0 ? { narrative } : {},
66
+ theme: v3.theme,
67
+ meta: v3.meta,
68
+ assets: v3.assets,
69
+ ...v3.brand !== void 0 ? { brand: v3.brand } : {},
70
+ ...v3.seed !== void 0 ? { seed: v3.seed } : {},
71
+ slides: v3.slides
72
+ };
73
+ }
74
+
75
+ // src/plan/index.ts
76
+ import { z as z2 } from "zod";
77
+ var PAGE_TYPES = ["cover", "chapter", "content", "ending"];
78
+ var BEAT_VALUES = ["anchor", "dense", "breathing"];
79
+ var PageSpecSchema = z2.object({
80
+ id: z2.string(),
81
+ type: z2.enum(PAGE_TYPES),
82
+ heading: z2.string(),
83
+ /** One of the three beat values, or omitted entirely — an omitted
84
+ * beat is never a hard-gate violation on its own (see
85
+ * {@link checkBeatRotation}'s policy functions below). It gets
86
+ * auto-alternated at assemble time (W5 task 3, not this task). Renamed
87
+ * from `rhythm` (vocabulary-v4 rename, spec §4.3/§6/§8.1) — same
88
+ * three values, same semantics, page-level term only, distinct from
89
+ * the deck-level `pacing` axis. */
90
+ beat: z2.enum(BEAT_VALUES).optional(),
91
+ /** Optional authoring hint pointing fill/select at a preferred
92
+ * component type or layout id — see {@link checkFocusVocabulary}. */
93
+ focus: z2.string().optional(),
94
+ /** Free-text content anchor, "for the fill step's own reading only" (spec §5) — read by a later
95
+ * fill step, never validated or interpreted here. */
96
+ summary: z2.string().optional()
97
+ }).strict();
98
+ var DeckSpecSchema = z2.object({
99
+ version: z2.literal("1").default("1"),
100
+ // Same open-schema/closed-semantic split as PptxIRSchema's `narrative`
101
+ // field — see `NarrativeProfileInputSchema`'s doc comment in `ir/index.ts`
102
+ // for the full rationale (reused verbatim here, not redefined, so the
103
+ // two can't drift apart). Field renamed from `scenario` to `narrative`
104
+ // this task (spec §8.1's `DeckPlan`→`DeckSpec` rename, task 2) — its
105
+ // *value* was already in the new strategy/pacing vocabulary as of task 1
106
+ // (vocabulary-v4 rename) — `resolveNarrative` below is what actually
107
+ // enforces that.
108
+ narrative: z2.union([z2.string(), NarrativeProfileInputSchema]).optional(),
109
+ theme: z2.string().optional(),
110
+ filename: z2.string().optional(),
111
+ seed: z2.number().int().optional(),
112
+ meta: MetaSchema.default({}),
113
+ /** Deck logo placement — reused verbatim from the IR's own `brand` field
114
+ * (`BrandSchema`, `../ir`) so the deck spec and IR can't drift apart on
115
+ * shape, same pattern as `meta` just above. Unlike `meta`, no
116
+ * `.default({})`: IR's own `brand` field is a bare `.optional()` with no
117
+ * default either (`undefined` means "no brand", not "an empty brand
118
+ * object") — consumed by `BrandChrome` (`src/svg/BrandChrome.tsx`) for
119
+ * the deck's logo image and corner position. */
120
+ brand: BrandSchema.optional(),
121
+ pages: z2.array(PageSpecSchema)
122
+ }).strict();
123
+ function specJsonSchema() {
124
+ return z2.toJSONSchema(DeckSpecSchema);
125
+ }
126
+ function formatSpecIssues(errors) {
127
+ return errors.map((e) => e.pageId ? `page "${e.pageId}" \u2014 ${e.path}: ${e.message}` : `${e.path}: ${e.message}`).join("\n");
128
+ }
129
+ function formatInvalidSpecError(errors) {
130
+ return `invalid spec (${errors.length} issue${errors.length === 1 ? "" : "s"}):
131
+ ${formatSpecIssues(errors)}`;
132
+ }
133
+ function resolveSpecThemeId(spec) {
134
+ return spec.theme ?? "consulting";
135
+ }
136
+ function checkPagesNonEmpty(spec) {
137
+ if (spec.pages.length > 0) return [];
138
+ return [{ path: "pages", message: "spec has no pages \u2014 a spec needs at least a cover page and an ending page" }];
139
+ }
140
+ function checkBoundaryTypes(spec) {
141
+ const { pages } = spec;
142
+ const errors = [];
143
+ const first = pages[0];
144
+ const last = pages[pages.length - 1];
145
+ if (first.type !== "cover") {
146
+ errors.push({
147
+ path: "pages.0.type",
148
+ pageId: first.id,
149
+ message: `first page must be type "cover" (got "${first.type}") \u2014 a spec must open with a cover page`
150
+ });
151
+ }
152
+ if (last.type !== "ending") {
153
+ errors.push({
154
+ path: `pages.${pages.length - 1}.type`,
155
+ pageId: last.id,
156
+ message: `last page must be type "ending" (got "${last.type}") \u2014 a spec must close with an ending page`
157
+ });
158
+ }
159
+ for (let i = 1; i < pages.length - 1; i++) {
160
+ const page = pages[i];
161
+ if (page.type === "cover" || page.type === "ending") {
162
+ errors.push({
163
+ path: `pages.${i}.type`,
164
+ pageId: page.id,
165
+ message: `page "${page.id}" is type "${page.type}", only allowed as the first (cover) or last (ending) page \u2014 use "content" or "chapter" for interior pages`
166
+ });
167
+ }
168
+ }
169
+ return errors;
170
+ }
171
+ function isUnsafePageId(id) {
172
+ return id.includes("/") || id.includes("\\") || id === "..";
173
+ }
174
+ function checkPageIds(spec) {
175
+ const errors = [];
176
+ const seen = /* @__PURE__ */ new Map();
177
+ spec.pages.forEach((page, i) => {
178
+ if (page.id.trim() === "") {
179
+ errors.push({ path: `pages.${i}.id`, message: `page ${i + 1} has an empty id \u2014 every page needs a non-empty, unique id` });
180
+ return;
181
+ }
182
+ if (isUnsafePageId(page.id)) {
183
+ errors.push({
184
+ path: `pages.${i}.id`,
185
+ pageId: page.id,
186
+ message: `page id "${page.id}" is not a safe file name \u2014 ids used as page/asset file names must not contain path separators or ".."`
187
+ });
188
+ return;
189
+ }
190
+ const indices = seen.get(page.id);
191
+ if (indices) indices.push(i);
192
+ else seen.set(page.id, [i]);
193
+ });
194
+ for (const [id, indices] of seen) {
195
+ if (indices.length < 2) continue;
196
+ errors.push({
197
+ path: "pages",
198
+ pageId: id,
199
+ message: `duplicate page id "${id}" used by ${indices.length} pages (positions ${indices.map((i) => i + 1).join(", ")}) \u2014 page ids must be unique within a spec`
200
+ });
201
+ }
202
+ return errors;
203
+ }
204
+ var HEADING_MAX_CHARS = CAPACITY.headingMaxChars;
205
+ function specHeadingLength(heading) {
206
+ return heading.length;
207
+ }
208
+ function checkHeadings(spec) {
209
+ const errors = [];
210
+ spec.pages.forEach((page, i) => {
211
+ if (page.heading.trim() === "") {
212
+ errors.push({ path: `pages.${i}.heading`, pageId: page.id, message: `page "${page.id}" is missing a required heading` });
213
+ return;
214
+ }
215
+ const length = specHeadingLength(page.heading);
216
+ if (length > HEADING_MAX_CHARS) {
217
+ errors.push({
218
+ path: `pages.${i}.heading`,
219
+ pageId: page.id,
220
+ message: `page "${page.id}" heading is ${length} characters, exceeds the ${HEADING_MAX_CHARS}-character limit \u2014 tighten it into a short, assertive phrase`
221
+ });
222
+ }
223
+ });
224
+ return errors;
225
+ }
226
+ function checkTheme(spec) {
227
+ const themeId = resolveSpecThemeId(spec);
228
+ const installed = getInstalledThemeIds();
229
+ if (installed.includes(themeId)) return [];
230
+ return [{ path: "theme", message: `unknown theme "${themeId}" \u2014 available: ${installed.join(", ")} (see \`pptfast themes\`)` }];
231
+ }
232
+ var LAYOUT_IDS = Object.keys(LAYOUT_REGISTRY);
233
+ function checkFocusVocabulary(spec, strategy) {
234
+ const tendencies = STRATEGY_DEFINITIONS[strategy].tendencies;
235
+ const errors = [];
236
+ spec.pages.forEach((page, i) => {
237
+ if (page.focus === void 0) return;
238
+ if (tendencies.includes(page.focus) || COMPONENT_TYPES.includes(page.focus) || LAYOUT_IDS.includes(page.focus)) {
239
+ return;
240
+ }
241
+ errors.push({
242
+ path: `pages.${i}.focus`,
243
+ pageId: page.id,
244
+ message: `unknown focus "${page.focus}" for strategy "${strategy}" \u2014 expected one of this strategy's tendencies (${tendencies.join(", ")}), a component type (${COMPONENT_TYPES.join(", ")}), or a layout id (${LAYOUT_IDS.join(", ")})`
245
+ });
246
+ });
247
+ return errors;
248
+ }
249
+ function declaredBeatContentPages(spec) {
250
+ const result = [];
251
+ spec.pages.forEach((page, index) => {
252
+ if (page.type === "content" && page.beat !== void 0) {
253
+ result.push({ index, id: page.id, beat: page.beat });
254
+ }
255
+ });
256
+ return result;
257
+ }
258
+ function checkAlternatePolicy(spec, strategy) {
259
+ const seq = declaredBeatContentPages(spec);
260
+ const errors = [];
261
+ let i = 0;
262
+ while (i < seq.length) {
263
+ let j = i + 1;
264
+ while (j < seq.length && seq[j].beat === seq[i].beat) j++;
265
+ const runLength = j - i;
266
+ if (runLength >= 3) {
267
+ const members = seq.slice(i, j);
268
+ errors.push({
269
+ path: "pages",
270
+ pageId: members[0].id,
271
+ message: `${runLength} consecutive content pages declare beat "${seq[i].beat}" (${members.map((m) => m.id).join(", ")}) \u2014 strategy "${strategy}" requires beat to alternate, vary at least one of them`
272
+ });
273
+ }
274
+ i = j;
275
+ }
276
+ return errors;
277
+ }
278
+ function checkAnchorOpenPolicy(spec, strategy) {
279
+ const firstContentIndex = spec.pages.findIndex((page) => page.type === "content");
280
+ if (firstContentIndex === -1) return [];
281
+ const firstContent = spec.pages[firstContentIndex];
282
+ if (firstContent.beat === void 0 || firstContent.beat === "anchor") return [];
283
+ return [
284
+ {
285
+ path: `pages.${firstContentIndex}.beat`,
286
+ pageId: firstContent.id,
287
+ message: `first content page declares beat "${firstContent.beat}" \u2014 strategy "${strategy}" requires the deck to open its first content page on "anchor" beat when a beat is declared`
288
+ }
289
+ ];
290
+ }
291
+ function checkAnchorSparsePolicy(spec, strategy) {
292
+ const declared = declaredBeatContentPages(spec);
293
+ if (declared.length === 0) return [];
294
+ const anchorPages = declared.filter((page) => page.beat === "anchor");
295
+ if (anchorPages.length / declared.length <= 0.5) return [];
296
+ const pct = Math.round(anchorPages.length / declared.length * 100);
297
+ return [
298
+ {
299
+ path: "pages",
300
+ // First offending anchor page, same "representative pageId" shape
301
+ // checkAlternatePolicy's own issue carries (members[0]!.id there) —
302
+ // this gate's violation is deck-wide (a ratio, not one page), but a
303
+ // representative id still gives a CLI/agent caller something to jump
304
+ // to rather than only a bare "pages" path.
305
+ pageId: anchorPages[0].id,
306
+ message: `${anchorPages.length} of ${declared.length} content pages with a declared beat are "anchor" (${pct}%: ${anchorPages.map((page) => page.id).join(", ")}) \u2014 strategy "${strategy}" requires "anchor" to stay a minority of declared beats, vary some to "dense" or "breathing"`
307
+ }
308
+ ];
309
+ }
310
+ function checkBeatRotation(spec, strategy) {
311
+ const policy = STRATEGY_DEFINITIONS[strategy].beatPolicy;
312
+ switch (policy) {
313
+ case "uniform-dense":
314
+ case "repetition-ok":
315
+ return [];
316
+ case "alternate":
317
+ return checkAlternatePolicy(spec, strategy);
318
+ case "anchor-open":
319
+ return checkAnchorOpenPolicy(spec, strategy);
320
+ case "anchor-sparse":
321
+ return checkAnchorSparsePolicy(spec, strategy);
322
+ default: {
323
+ const exhaustive = policy;
324
+ throw new Error(`unhandled beat policy: ${String(exhaustive)}`);
325
+ }
326
+ }
327
+ }
328
+ var SPEC_PAGE_COUNT_RANGE = {
329
+ dense: { min: 8, max: 30 },
330
+ balanced: { min: 6, max: 24 },
331
+ spacious: { min: 4, max: 16 }
332
+ };
333
+ function checkPageCount(spec, pacing) {
334
+ const { min, max } = SPEC_PAGE_COUNT_RANGE[pacing];
335
+ const n = spec.pages.length;
336
+ if (n >= min && n <= max) return [];
337
+ return [
338
+ {
339
+ path: "pages",
340
+ message: `spec has ${n} pages \u2014 "${pacing}" pacing expects ${min}-${max} pages, change pacing or add/remove pages`
341
+ }
342
+ ];
343
+ }
344
+ function pageIdFromRawInput(input, index) {
345
+ if (typeof input !== "object" || input === null) return void 0;
346
+ const pages = input.pages;
347
+ if (!Array.isArray(pages)) return void 0;
348
+ const page = pages[index];
349
+ if (typeof page !== "object" || page === null) return void 0;
350
+ const id = page.id;
351
+ return typeof id === "string" ? id : void 0;
352
+ }
353
+ function validateSpec(input) {
354
+ const r = DeckSpecSchema.safeParse(input);
355
+ if (!r.success) {
356
+ const errors = r.error.issues.map((issue) => {
357
+ const path = issue.path.join(".");
358
+ const m = /^pages\.(\d+)/.exec(path);
359
+ return { path, message: issue.message, pageId: m ? pageIdFromRawInput(input, Number(m[1])) : void 0 };
360
+ });
361
+ return { ok: false, errors };
362
+ }
363
+ const spec = r.data;
364
+ const emptyErrors = checkPagesNonEmpty(spec);
365
+ if (emptyErrors.length > 0) return { ok: false, errors: emptyErrors };
366
+ const boundaryErrors = checkBoundaryTypes(spec);
367
+ if (boundaryErrors.length > 0) return { ok: false, errors: boundaryErrors };
368
+ const idErrors = checkPageIds(spec);
369
+ if (idErrors.length > 0) return { ok: false, errors: idErrors };
370
+ const headingErrors = checkHeadings(spec);
371
+ if (headingErrors.length > 0) return { ok: false, errors: headingErrors };
372
+ const themeErrors = checkTheme(spec);
373
+ if (themeErrors.length > 0) return { ok: false, errors: themeErrors };
374
+ let resolvedAxes;
375
+ try {
376
+ resolvedAxes = resolveNarrative(spec.narrative);
377
+ } catch (err) {
378
+ if (!(err instanceof PptfastError)) throw err;
379
+ return { ok: false, errors: [{ path: "narrative", message: err.message }] };
380
+ }
381
+ const beatErrors = checkBeatRotation(spec, resolvedAxes.strategy);
382
+ if (beatErrors.length > 0) return { ok: false, errors: beatErrors };
383
+ const focusErrors = checkFocusVocabulary(spec, resolvedAxes.strategy);
384
+ if (focusErrors.length > 0) return { ok: false, errors: focusErrors };
385
+ const pageCountErrors = checkPageCount(spec, resolvedAxes.pacing);
386
+ if (pageCountErrors.length > 0) return { ok: false, errors: pageCountErrors };
387
+ return { ok: true, spec, errors: [] };
388
+ }
389
+
390
+ // src/plan/assemble.ts
391
+ function stableHash(s) {
392
+ let h = 5381;
393
+ for (let i = 0; i < s.length; i++) h = (h << 5) + h + s.charCodeAt(i) | 0;
394
+ return Math.abs(h);
395
+ }
396
+ function generateSeed(filename, pageIds) {
397
+ return stableHash([filename ?? "", ...pageIds].join("\n"));
398
+ }
399
+ var LOCKED_KEYS = ["type", "heading"];
400
+ function assembleDeck(spec, pages) {
401
+ const validated = validateSpec(spec);
402
+ if (!validated.ok) {
403
+ throw new PptfastError(formatInvalidSpecError(validated.errors));
404
+ }
405
+ const deckSpec = validated.spec;
406
+ for (const page of deckSpec.pages) {
407
+ const raw = pages[page.id];
408
+ if (raw === void 0) continue;
409
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
410
+ throw new PptfastError(`page "${page.id}": page content must be an object`);
411
+ }
412
+ for (const key of LOCKED_KEYS) {
413
+ if (Object.hasOwn(raw, key)) {
414
+ throw new PptfastError(`page "${page.id}": "${key}" is locked by the spec \u2014 remove it from the page file`);
415
+ }
416
+ }
417
+ }
418
+ const specIds = new Set(deckSpec.pages.map((page) => page.id));
419
+ const orphanIds = Object.keys(pages).filter((id) => !specIds.has(id));
420
+ if (orphanIds.length > 0) {
421
+ throw new PptfastError(
422
+ `orphan page id${orphanIds.length === 1 ? "" : "s"} ${orphanIds.map((id) => `"${id}"`).join(", ")} \u2014 not in the spec, delete the page file or add the page to the spec`
423
+ );
424
+ }
425
+ const slides = deckSpec.pages.map((page) => buildSlide(page, pages[page.id]));
426
+ const generatedSeed = deckSpec.seed === void 0 ? generateSeed(deckSpec.filename, deckSpec.pages.map((page) => page.id)) : void 0;
427
+ const seed = deckSpec.seed ?? generatedSeed;
428
+ const rawIr = {
429
+ version: "4",
430
+ ...deckSpec.narrative !== void 0 ? { narrative: deckSpec.narrative } : {},
431
+ ...deckSpec.theme !== void 0 ? { theme: { id: deckSpec.theme } } : {},
432
+ ...deckSpec.filename !== void 0 ? { filename: deckSpec.filename } : {},
433
+ ...deckSpec.brand !== void 0 ? { brand: deckSpec.brand } : {},
434
+ meta: deckSpec.meta,
435
+ seed,
436
+ slides
437
+ };
438
+ const parsed = PptxIRSchema.safeParse(rawIr);
439
+ if (!parsed.success) {
440
+ const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
441
+ throw new PptfastError(`assembled deck did not produce valid IR:
442
+ ${detail}`);
443
+ }
444
+ const { ir, materializedCount } = materializeEffectiveLayouts(parsed.data);
445
+ return {
446
+ ir,
447
+ ...generatedSeed !== void 0 ? { generatedSeed } : {},
448
+ ...materializedCount > 0 ? { materializedLayoutCount: materializedCount } : {}
449
+ };
450
+ }
451
+ function materializeEffectiveLayouts(ir) {
452
+ let materializedCount = 0;
453
+ const slides = ir.slides.map((slide, index) => {
454
+ if (slide.layout !== void 0) return slide;
455
+ const effectiveLayoutId = resolveEffectiveLayoutId(ir, slide, index);
456
+ if (effectiveLayoutId === null) return slide;
457
+ materializedCount++;
458
+ return { ...slide, layout: effectiveLayoutId };
459
+ });
460
+ return materializedCount === 0 ? { ir, materializedCount } : { ir: { ...ir, slides }, materializedCount };
461
+ }
462
+ function buildSlide(page, raw) {
463
+ if (raw === void 0) {
464
+ return {
465
+ id: page.id,
466
+ type: page.type,
467
+ heading: page.heading,
468
+ placeholder: true,
469
+ ...page.summary !== void 0 ? { subheading: page.summary } : {}
470
+ };
471
+ }
472
+ return {
473
+ id: page.id,
474
+ type: page.type,
475
+ heading: page.heading,
476
+ ...raw.components !== void 0 ? { components: raw.components } : {},
477
+ ...raw.layout !== void 0 ? { layout: raw.layout } : {},
478
+ ...raw.arrangement !== void 0 ? { arrangement: raw.arrangement } : {},
479
+ ...raw.background !== void 0 ? { background: raw.background } : {},
480
+ ...raw.image_side !== void 0 ? { image_side: raw.image_side } : {},
481
+ ...raw.footnote !== void 0 ? { footnote: raw.footnote } : {},
482
+ ...raw.notes !== void 0 ? { notes: raw.notes } : {}
483
+ };
484
+ }
485
+ var UNTITLED_HEADING = "Untitled";
486
+ function disassembleDeck(ir) {
487
+ const pages = {};
488
+ const pageSpecs = ir.slides.map((slide, index) => {
489
+ const id = slide.id ?? `p-${index + 1}-${slide.type}`;
490
+ const heading = slide.heading !== void 0 && slide.heading.trim() !== "" ? slide.heading : UNTITLED_HEADING;
491
+ const pageSpec = {
492
+ id,
493
+ type: slide.type,
494
+ heading,
495
+ ...slide.placeholder === true && slide.subheading !== void 0 ? { summary: slide.subheading } : {}
496
+ };
497
+ if (slide.placeholder !== true) pages[id] = extractPageContent(slide);
498
+ return pageSpec;
499
+ });
500
+ const spec = {
501
+ version: "1",
502
+ // `ir.narrative` (v4 field, vocabulary-v4 rename) carries straight into
503
+ // the deck spec's own `narrative` field — its value is
504
+ // already in the new strategy/pacing vocabulary, no remapping needed.
505
+ ...ir.narrative !== void 0 ? { narrative: ir.narrative } : {},
506
+ theme: ir.theme.id,
507
+ filename: ir.filename,
508
+ ...ir.seed !== void 0 ? { seed: ir.seed } : {},
509
+ ...ir.brand !== void 0 ? { brand: ir.brand } : {},
510
+ meta: ir.meta,
511
+ pages: pageSpecs
512
+ };
513
+ return { spec, pages };
514
+ }
515
+ function extractPageContent(slide) {
516
+ const content = {};
517
+ if (slide.components.length > 0) content.components = slide.components;
518
+ if (slide.layout !== void 0) content.layout = slide.layout;
519
+ if (slide.arrangement !== void 0) content.arrangement = slide.arrangement;
520
+ if (slide.background !== void 0) content.background = slide.background;
521
+ if (slide.image_side !== void 0) content.image_side = slide.image_side;
522
+ if (slide.footnote !== void 0) content.footnote = slide.footnote;
523
+ if (slide.notes !== void 0) content.notes = slide.notes;
524
+ return content;
525
+ }
526
+
527
+ // src/plan/migrate.ts
528
+ function migrateDeckPlanToSpec(raw) {
529
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
530
+ const { scenario, pages, ...rest } = raw;
531
+ const result = { ...rest };
532
+ if (scenario !== void 0) result.narrative = scenario;
533
+ if (Array.isArray(pages)) {
534
+ result.pages = pages.map(migratePageRhythmToBeat);
535
+ } else if (pages !== void 0) {
536
+ result.pages = pages;
537
+ }
538
+ return result;
539
+ }
540
+ function migratePageRhythmToBeat(page) {
541
+ if (typeof page !== "object" || page === null || Array.isArray(page)) return page;
542
+ const { rhythm, ...rest } = page;
543
+ const next = { ...rest };
544
+ if (rhythm !== void 0) next.beat = rhythm;
545
+ return next;
546
+ }
547
+
548
+ export {
549
+ VERSION,
550
+ PptxIRV3Schema,
551
+ migrateIrV3ToV4,
552
+ PageSpecSchema,
553
+ DeckSpecSchema,
554
+ specJsonSchema,
555
+ formatSpecIssues,
556
+ formatInvalidSpecError,
557
+ resolveSpecThemeId,
558
+ SPEC_PAGE_COUNT_RANGE,
559
+ validateSpec,
560
+ assembleDeck,
561
+ disassembleDeck,
562
+ migrateDeckPlanToSpec
563
+ };
564
+ //# sourceMappingURL=chunk-4W2YZPJJ.js.map