@liustack/pptfast 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  export { P as PptfastPlatform, i as installPlatform } from './registry-CYKxZ0-U.js';
3
3
 
4
- declare const VERSION = "0.1.0";
4
+ declare const VERSION = "0.3.0";
5
5
 
6
6
  /** Error class thrown by pptfast's public API surface (validation, rendering, export). */
7
7
  declare class PptfastError extends Error {
8
8
  }
9
9
 
10
- declare const THEME_IDS: readonly ["consulting", "enterprise", "academic", "insight", "campaign", "bloom", "classroom", "ink", "tech", "runway", "journal", "luxe", "heritage"];
10
+ declare const BUILTIN_THEME_IDS: readonly ["consulting", "enterprise", "academic", "insight", "campaign", "bloom", "classroom", "ink", "tech", "runway", "journal", "luxe", "heritage"];
11
11
  declare const BackgroundSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
12
  kind: z.ZodLiteral<"color">;
13
13
  value: z.ZodString;
@@ -32,6 +32,79 @@ declare const BackgroundSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
32
32
  contain: "contain";
33
33
  }>>;
34
34
  }, z.core.$strict>], "kind">;
35
+ /**
36
+ * Style-token override (theme.style): deep-partial palette/fonts/shape
37
+ * merged over the built-in theme (see themes/index.ts resolveStyle). Scope is
38
+ * deliberately palette-level (spec §11): no defaultBackgrounds or manifest
39
+ * overrides. gapScale range mirrors the documented sane range in
40
+ * themes/tokens.ts StyleShape.
41
+ */
42
+ declare const StyleOverrideSchema: z.ZodObject<{
43
+ colors: z.ZodOptional<z.ZodObject<{
44
+ bg: z.ZodOptional<z.ZodString>;
45
+ surface: z.ZodOptional<z.ZodString>;
46
+ panel: z.ZodOptional<z.ZodString>;
47
+ primary: z.ZodOptional<z.ZodString>;
48
+ accent: z.ZodOptional<z.ZodString>;
49
+ text: z.ZodOptional<z.ZodString>;
50
+ muted: z.ZodOptional<z.ZodString>;
51
+ border: z.ZodOptional<z.ZodString>;
52
+ chartPalette: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
+ accentPool: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ cardStroke: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strict>>;
56
+ fonts: z.ZodOptional<z.ZodObject<{
57
+ heading: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
+ body: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ mono: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ }, z.core.$strict>>;
61
+ shape: z.ZodOptional<z.ZodObject<{
62
+ radius: z.ZodOptional<z.ZodNumber>;
63
+ gapScale: z.ZodOptional<z.ZodNumber>;
64
+ }, z.core.$strict>>;
65
+ }, z.core.$strict>;
66
+ type StyleOverride = z.infer<typeof StyleOverrideSchema>;
67
+ /**
68
+ * Brand (logical slide-master) config: brand-chrome behavior owned by a theme.
69
+ * W1 scope = exactly the two flags migrated from the old manifest.chrome.
70
+ * Single source of truth — the TS type is inferred, never hand-written.
71
+ */
72
+ declare const BrandConfigSchema: z.ZodObject<{
73
+ suppressFooterOnCardContent: z.ZodOptional<z.ZodBoolean>;
74
+ suppressFooterRule: z.ZodOptional<z.ZodBoolean>;
75
+ }, z.core.$strict>;
76
+ type BrandConfig = z.infer<typeof BrandConfigSchema>;
77
+ declare const ThemeSchema: z.ZodObject<{
78
+ id: z.ZodDefault<z.ZodString>;
79
+ style: z.ZodOptional<z.ZodObject<{
80
+ colors: z.ZodOptional<z.ZodObject<{
81
+ bg: z.ZodOptional<z.ZodString>;
82
+ surface: z.ZodOptional<z.ZodString>;
83
+ panel: z.ZodOptional<z.ZodString>;
84
+ primary: z.ZodOptional<z.ZodString>;
85
+ accent: z.ZodOptional<z.ZodString>;
86
+ text: z.ZodOptional<z.ZodString>;
87
+ muted: z.ZodOptional<z.ZodString>;
88
+ border: z.ZodOptional<z.ZodString>;
89
+ chartPalette: z.ZodOptional<z.ZodArray<z.ZodString>>;
90
+ accentPool: z.ZodOptional<z.ZodArray<z.ZodString>>;
91
+ cardStroke: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strict>>;
93
+ fonts: z.ZodOptional<z.ZodObject<{
94
+ heading: z.ZodOptional<z.ZodArray<z.ZodString>>;
95
+ body: z.ZodOptional<z.ZodArray<z.ZodString>>;
96
+ mono: z.ZodOptional<z.ZodArray<z.ZodString>>;
97
+ }, z.core.$strict>>;
98
+ shape: z.ZodOptional<z.ZodObject<{
99
+ radius: z.ZodOptional<z.ZodNumber>;
100
+ gapScale: z.ZodOptional<z.ZodNumber>;
101
+ }, z.core.$strict>>;
102
+ }, z.core.$strict>>;
103
+ brand: z.ZodOptional<z.ZodObject<{
104
+ suppressFooterOnCardContent: z.ZodOptional<z.ZodBoolean>;
105
+ suppressFooterRule: z.ZodOptional<z.ZodBoolean>;
106
+ }, z.core.$strict>>;
107
+ }, z.core.$strict>;
35
108
  declare const MetaSchema: z.ZodObject<{
36
109
  organization: z.ZodOptional<z.ZodString>;
37
110
  authors: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -74,7 +147,7 @@ declare const AssetsSchema: z.ZodObject<{
74
147
  error: z.ZodOptional<z.ZodString>;
75
148
  }, z.core.$strict>>>;
76
149
  }, z.core.$strict>;
77
- declare const BlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
150
+ declare const ComponentSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
78
151
  type: z.ZodLiteral<"bullets">;
79
152
  items: z.ZodArray<z.ZodString>;
80
153
  style: z.ZodOptional<z.ZodEnum<{
@@ -127,9 +200,9 @@ declare const BlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
127
200
  chart_type: z.ZodEnum<{
128
201
  line: "line";
129
202
  dumbbell: "dumbbell";
203
+ funnel: "funnel";
130
204
  bar: "bar";
131
205
  pie: "pie";
132
- funnel: "funnel";
133
206
  }>;
134
207
  direction: z.ZodOptional<z.ZodEnum<{
135
208
  horizontal: "horizontal";
@@ -320,14 +393,27 @@ declare const BlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
320
393
  before_after: "before_after";
321
394
  }>>;
322
395
  }, z.core.$strict>], "type">;
396
+ /**
397
+ * All 24 component `type` discriminant values, derived from `ComponentSchema`
398
+ * itself (never hand-copied) so this list can't drift from the union above.
399
+ * Typed as plain `readonly string[]` rather than `Component["type"][]` —
400
+ * every consumer of this list (W5's plan `focus` vocabulary gate,
401
+ * `src/plan/index.ts`) tests membership of an arbitrary author-supplied
402
+ * string, and TS's `Array.includes` is invariant in its element type, so a
403
+ * narrower literal-union type would reject that call at the caller.
404
+ */
405
+ declare const COMPONENT_TYPES: readonly string[];
323
406
  declare const SlideSchema: z.ZodObject<{
324
- type: z.ZodEnum<{
407
+ type: z.ZodDefault<z.ZodEnum<{
325
408
  cover: "cover";
326
409
  chapter: "chapter";
327
410
  content: "content";
328
411
  ending: "ending";
329
- }>;
330
- variant: z.ZodOptional<z.ZodEnum<{
412
+ }>>;
413
+ id: z.ZodOptional<z.ZodString>;
414
+ placeholder: z.ZodOptional<z.ZodLiteral<true>>;
415
+ layout: z.ZodOptional<z.ZodString>;
416
+ arrangement: z.ZodOptional<z.ZodEnum<{
331
417
  code: "code";
332
418
  quote: "quote";
333
419
  single: "single";
@@ -336,15 +422,11 @@ declare const SlideSchema: z.ZodObject<{
336
422
  image_focus: "image_focus";
337
423
  big_number: "big_number";
338
424
  assertion_evidence: "assertion_evidence";
339
- image_split: "image_split";
340
- image_top: "image_top";
341
- image_bottom: "image_bottom";
342
- image_annotate: "image_annotate";
343
425
  aside: "aside";
344
426
  }>>;
345
427
  heading: z.ZodOptional<z.ZodString>;
346
428
  subheading: z.ZodOptional<z.ZodString>;
347
- blocks: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
429
+ components: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
348
430
  type: z.ZodLiteral<"bullets">;
349
431
  items: z.ZodArray<z.ZodString>;
350
432
  style: z.ZodOptional<z.ZodEnum<{
@@ -397,9 +479,9 @@ declare const SlideSchema: z.ZodObject<{
397
479
  chart_type: z.ZodEnum<{
398
480
  line: "line";
399
481
  dumbbell: "dumbbell";
482
+ funnel: "funnel";
400
483
  bar: "bar";
401
484
  pie: "pie";
402
- funnel: "funnel";
403
485
  }>;
404
486
  direction: z.ZodOptional<z.ZodEnum<{
405
487
  horizontal: "horizontal";
@@ -635,31 +717,40 @@ declare const SlideSchema: z.ZodObject<{
635
717
  footnote: z.ZodOptional<z.ZodString>;
636
718
  }, z.core.$strict>;
637
719
  declare const PptxIRSchema: z.ZodObject<{
638
- version: z.ZodLiteral<"2">;
639
- filename: z.ZodString;
640
- theme: z.ZodObject<{
641
- id: z.ZodEnum<{
642
- consulting: "consulting";
643
- enterprise: "enterprise";
644
- academic: "academic";
645
- insight: "insight";
646
- campaign: "campaign";
647
- bloom: "bloom";
648
- classroom: "classroom";
649
- ink: "ink";
650
- tech: "tech";
651
- runway: "runway";
652
- journal: "journal";
653
- luxe: "luxe";
654
- heritage: "heritage";
655
- }>;
656
- override: z.ZodOptional<z.ZodObject<{
657
- primary: z.ZodOptional<z.ZodString>;
658
- accent: z.ZodOptional<z.ZodString>;
659
- font_heading: z.ZodOptional<z.ZodArray<z.ZodString>>;
660
- font_body: z.ZodOptional<z.ZodArray<z.ZodString>>;
720
+ version: z.ZodDefault<z.ZodLiteral<"3">>;
721
+ filename: z.ZodDefault<z.ZodString>;
722
+ scenario: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
723
+ theme: z.ZodDefault<z.ZodObject<{
724
+ id: z.ZodDefault<z.ZodString>;
725
+ style: z.ZodOptional<z.ZodObject<{
726
+ colors: z.ZodOptional<z.ZodObject<{
727
+ bg: z.ZodOptional<z.ZodString>;
728
+ surface: z.ZodOptional<z.ZodString>;
729
+ panel: z.ZodOptional<z.ZodString>;
730
+ primary: z.ZodOptional<z.ZodString>;
731
+ accent: z.ZodOptional<z.ZodString>;
732
+ text: z.ZodOptional<z.ZodString>;
733
+ muted: z.ZodOptional<z.ZodString>;
734
+ border: z.ZodOptional<z.ZodString>;
735
+ chartPalette: z.ZodOptional<z.ZodArray<z.ZodString>>;
736
+ accentPool: z.ZodOptional<z.ZodArray<z.ZodString>>;
737
+ cardStroke: z.ZodOptional<z.ZodString>;
738
+ }, z.core.$strict>>;
739
+ fonts: z.ZodOptional<z.ZodObject<{
740
+ heading: z.ZodOptional<z.ZodArray<z.ZodString>>;
741
+ body: z.ZodOptional<z.ZodArray<z.ZodString>>;
742
+ mono: z.ZodOptional<z.ZodArray<z.ZodString>>;
743
+ }, z.core.$strict>>;
744
+ shape: z.ZodOptional<z.ZodObject<{
745
+ radius: z.ZodOptional<z.ZodNumber>;
746
+ gapScale: z.ZodOptional<z.ZodNumber>;
747
+ }, z.core.$strict>>;
661
748
  }, z.core.$strict>>;
662
- }, z.core.$strict>;
749
+ brand: z.ZodOptional<z.ZodObject<{
750
+ suppressFooterOnCardContent: z.ZodOptional<z.ZodBoolean>;
751
+ suppressFooterRule: z.ZodOptional<z.ZodBoolean>;
752
+ }, z.core.$strict>>;
753
+ }, z.core.$strict>>;
663
754
  meta: z.ZodDefault<z.ZodObject<{
664
755
  organization: z.ZodOptional<z.ZodString>;
665
756
  authors: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -711,14 +802,18 @@ declare const PptxIRSchema: z.ZodObject<{
711
802
  br: "br";
712
803
  }>>;
713
804
  }, z.core.$strict>>;
805
+ seed: z.ZodOptional<z.ZodNumber>;
714
806
  slides: z.ZodArray<z.ZodObject<{
715
- type: z.ZodEnum<{
807
+ type: z.ZodDefault<z.ZodEnum<{
716
808
  cover: "cover";
717
809
  chapter: "chapter";
718
810
  content: "content";
719
811
  ending: "ending";
720
- }>;
721
- variant: z.ZodOptional<z.ZodEnum<{
812
+ }>>;
813
+ id: z.ZodOptional<z.ZodString>;
814
+ placeholder: z.ZodOptional<z.ZodLiteral<true>>;
815
+ layout: z.ZodOptional<z.ZodString>;
816
+ arrangement: z.ZodOptional<z.ZodEnum<{
722
817
  code: "code";
723
818
  quote: "quote";
724
819
  single: "single";
@@ -727,15 +822,11 @@ declare const PptxIRSchema: z.ZodObject<{
727
822
  image_focus: "image_focus";
728
823
  big_number: "big_number";
729
824
  assertion_evidence: "assertion_evidence";
730
- image_split: "image_split";
731
- image_top: "image_top";
732
- image_bottom: "image_bottom";
733
- image_annotate: "image_annotate";
734
825
  aside: "aside";
735
826
  }>>;
736
827
  heading: z.ZodOptional<z.ZodString>;
737
828
  subheading: z.ZodOptional<z.ZodString>;
738
- blocks: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
829
+ components: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
739
830
  type: z.ZodLiteral<"bullets">;
740
831
  items: z.ZodArray<z.ZodString>;
741
832
  style: z.ZodOptional<z.ZodEnum<{
@@ -788,9 +879,9 @@ declare const PptxIRSchema: z.ZodObject<{
788
879
  chart_type: z.ZodEnum<{
789
880
  line: "line";
790
881
  dumbbell: "dumbbell";
882
+ funnel: "funnel";
791
883
  bar: "bar";
792
884
  pie: "pie";
793
- funnel: "funnel";
794
885
  }>;
795
886
  direction: z.ZodOptional<z.ZodEnum<{
796
887
  horizontal: "horizontal";
@@ -1027,7 +1118,7 @@ declare const PptxIRSchema: z.ZodObject<{
1027
1118
  }, z.core.$strict>>;
1028
1119
  }, z.core.$strict>;
1029
1120
  type PptxIR = z.infer<typeof PptxIRSchema>;
1030
- type Block = z.infer<typeof BlockSchema>;
1121
+ type Component = z.infer<typeof ComponentSchema>;
1031
1122
  type BackgroundSpec = z.infer<typeof BackgroundSpecSchema>;
1032
1123
  type Slide = z.infer<typeof SlideSchema>;
1033
1124
  type Assets = z.infer<typeof AssetsSchema>;
@@ -1038,39 +1129,855 @@ interface ValidationIssue {
1038
1129
  message: string;
1039
1130
  /** 1-based slide number when the issue is scoped to a slide. */
1040
1131
  page?: number;
1132
+ /**
1133
+ * The offending slide's own `id` (`Slide.id`, `ir/index.ts`) — W5
1134
+ * whole-branch review finding 2: the README already claimed "validation
1135
+ * error messages reference [a slide] by [its] id"; this is what makes
1136
+ * that true. Set by every page-scoped issue producer
1137
+ * ({@link checkLayoutApplicability}, the content-quality-gate
1138
+ * translation in {@link validateIr}, {@link checkDuplicateSlideIds})
1139
+ * when the slide in question has an `id` — absent when the slide has
1140
+ * none (bare, pre-W5 IR) or the issue is deck-level, not scoped to any
1141
+ * single slide. {@link formatIssues} appends it in parens after the page
1142
+ * number.
1143
+ */
1144
+ slideId?: string;
1041
1145
  }
1042
1146
  interface ValidateResult {
1043
1147
  ok: boolean;
1044
1148
  ir?: PptxIR;
1045
1149
  errors: ValidationIssue[];
1150
+ /**
1151
+ * Human-readable "`path`: `alias` → `canonical`" entries for every
1152
+ * deterministic field-alias rewrite `validateIr` applied before parsing
1153
+ * (W5 task 4, `ir/field-aliases.ts`'s `normalizeComponentAliases`) — e.g. a
1154
+ * kpi item's `title` silently adopted as `label`. Present only when at
1155
+ * least one rewrite happened; informational, never gates `ok` on its own.
1156
+ */
1157
+ normalized?: string[];
1046
1158
  }
1047
1159
  /**
1048
- * Validate raw JSON against the IR schema, then — once it parses — run the
1049
- * content-quality gate (`checkIrQuality`) against the parsed IR. Both stages
1050
- * must pass for `ok: true`. Quality findings are reported the same way as
1051
- * schema errors (page-scoped, 1-based). `checkIrQuality` itself tags findings
1052
- * "warn" vs "error", but that split was designed for its original consumer
1053
- * (surfacing informational warnings in a UI after generation, per its own
1054
- * docstring) here it is the pre-generation hard gate, so any finding blocks
1055
- * (`ok: false`), not only "error"-severity ones. Treating "warn" findings
1056
- * (e.g. a cover with no heading) as advisory-only would defeat the point of
1057
- * wiring this in: the spec's core principle is a hard protocol gate, not
1058
- * hoped-for prompt compliance.
1160
+ * Validate raw JSON against the IR schema, then — once it parses — resolve
1161
+ * `scenario` (`resolveScenario`, spec §5: an unrecognized preset name is a
1162
+ * `scenario`-path error, page-less) and run the content-quality gate
1163
+ * (`checkIrQuality`, passed the resolved axes) against the parsed IR. All
1164
+ * stages must pass for `ok: true`. Quality findings are reported the same
1165
+ * way as schema errors (page-scoped, 1-based). `checkIrQuality` itself tags
1166
+ * findings "warn" vs "error", but that split was designed for its original
1167
+ * consumer (surfacing informational warnings in a UI after generation, per
1168
+ * its own docstring) here it is the pre-generation hard gate, so any
1169
+ * finding blocks (`ok: false`), not only "error"-severity ones. Treating
1170
+ * "warn" findings (e.g. a cover with no heading) as advisory-only would
1171
+ * defeat the point of wiring this in: the spec's core principle is a hard
1172
+ * protocol gate, not hoped-for prompt compliance.
1173
+ *
1174
+ * Before any of that, `normalizeComponentAliases` (W5 task 4,
1175
+ * `ir/field-aliases.ts`) gets one deterministic pass at `input` — rewriting
1176
+ * a known synonym field name (kpi `title`→`label`, quote `content`→`text`,
1177
+ * …) only where the canonical key is absent, so the schema parse below never
1178
+ * sees the alias as an "unrecognized key" in the first place. Ordering
1179
+ * versus the v2 migration check right above it is unobservable (v2 IR uses
1180
+ * `blocks`, never `components`, so the normalizer's slide/component walk is
1181
+ * always a no-op on it) — it runs second here only so a doomed v2 input
1182
+ * skips the extra walk. Purely informational: every rewrite is recorded as a
1183
+ * human-readable `path: alias → canonical` string and threaded onto
1184
+ * `ValidateResult.normalized` on *every* return path below via
1185
+ * `withNormalized`, success or failure alike — it never itself gates `ok`.
1059
1186
  */
1060
1187
  declare function validateIr(input: unknown): ValidateResult;
1188
+ /**
1189
+ * `"page 2 (p-kpi) — path: message"` when the issue carries both a `page`
1190
+ * and a `slideId` (W5 whole-branch review finding 2 — the README's own
1191
+ * claim that a validation error "references [a slide] by [its] id", made
1192
+ * true) — the parenthesized id is appended only alongside a `page` number,
1193
+ * never on its own: a deck-level issue that happens to set a representative
1194
+ * `slideId` with no `page` ({@link checkDuplicateSlideIds} above) keeps its
1195
+ * pre-existing, unchanged format. Every other combination — `page` with no
1196
+ * `slideId` (an id-less slide), or neither — is byte-identical to before
1197
+ * this task.
1198
+ */
1061
1199
  declare function formatIssues(errors: ValidationIssue[]): string;
1062
1200
  /** Render a single slide to standalone SVG markup (preview / self-check). */
1063
1201
  declare function renderSlideSvg(ir: PptxIR, slideIndex: number): string;
1064
1202
  /** Full pipeline: validate → SVG → DrawingML → animation patches → pptx bytes. */
1065
- declare function generatePptx(input: unknown): Promise<Uint8Array>;
1203
+ declare function generatePptx(input: unknown, opts?: {
1204
+ draft?: boolean;
1205
+ }): Promise<Uint8Array>;
1066
1206
  interface ThemeInfo {
1067
1207
  id: string;
1068
1208
  label: string;
1069
1209
  colors: Record<string, unknown>;
1070
1210
  }
1071
- /** Built-in theme catalog with labels and color tokens. */
1211
+ /** Built-in theme catalog with labels and style color tokens. */
1072
1212
  declare function listThemes(): ThemeInfo[];
1073
1213
  /** JSON Schema for the IR — feed this to a model before it writes IR. */
1074
1214
  declare function irJsonSchema(): Record<string, unknown>;
1215
+ /** JSON Schema for style-token overrides (IR theme.style, --style files, config "style"). */
1216
+ declare function styleJsonSchema(): Record<string, unknown>;
1217
+
1218
+ type LayoutType = "cover" | "chapter" | "content" | "ending";
1219
+ interface StyleColors {
1220
+ bg: string;
1221
+ surface: string;
1222
+ panel?: string;
1223
+ primary: string;
1224
+ accent: string;
1225
+ text: string;
1226
+ muted: string;
1227
+ border?: string;
1228
+ chartPalette: string[];
1229
+ /**
1230
+ * Optional accent color pool for themes whose layout grammar cycles through
1231
+ * multiple accent colors (e.g. tech card corner stripes). Themes that
1232
+ * only use a single accent color can omit this.
1233
+ */
1234
+ accentPool?: string[];
1235
+ /**
1236
+ * Optional hairline stroke color for the shared "surface card" shells
1237
+ * (kpi_cards / icon_cards / callout — see components/kpi.tsx,
1238
+ * components/icon-cards.tsx, components/callout.tsx). Those component renderers are
1239
+ * shared across every theme and must stay theme-agnostic (no
1240
+ * `if (themeId === ...)` branching), so a theme that wants its cards
1241
+ * outlined sets this token instead — the renderers just consult it and add
1242
+ * a 1px stroke when it's defined. Omitted (`undefined`, the default) draws
1243
+ * no stroke, so every theme that doesn't set it keeps its current
1244
+ * fill-only card unchanged.
1245
+ */
1246
+ cardStroke?: string;
1247
+ }
1248
+ interface StyleFonts {
1249
+ heading: string[];
1250
+ body: string[];
1251
+ mono?: string[];
1252
+ }
1253
+ interface StyleTokens {
1254
+ id: string;
1255
+ allowCustomBackground?: boolean;
1256
+ colors: StyleColors;
1257
+ fonts: StyleFonts;
1258
+ shape?: StyleShape;
1259
+ defaultBackgrounds: Record<LayoutType, BackgroundSpec>;
1260
+ }
1261
+ /**
1262
+ * 主题细节 shape token(2026-07-10 用户立项,spec:
1263
+ * .issues/specs/2026-07-10-pptx-theme-detail-tokens-design.md)。
1264
+ * 全部可选——缺省时各消费点沿用自己的 baked 值(零观感)。
1265
+ */
1266
+ interface StyleShape {
1267
+ /** 卡片/横幅圆角 px(统一值)。缺省=各消费点原 baked 值(kpi 8/architecture 6/白卡 14 等)。 */
1268
+ radius?: number;
1269
+ /** 块间距缩放(1=现 BLOCK_GAP)。建议范围 [0.8, 1.3]。 */
1270
+ gapScale?: number;
1271
+ }
1272
+
1273
+ type MotifArchetypeId = "banner-motif" | "rail-motif" | "poster-motif" | "constellation-motif" | "corner-ornament-motif" | "tone-adaptive-motif" | "campaign-motif" | "bloom-motif" | "classroom-motif" | "ink-motif" | "luxe-motif" | "enterprise-motif" | "heritage-motif";
1274
+
1275
+ /**
1276
+ * A theme = distributable bundle: `style` (style tokens) + `brand` (brand
1277
+ * chrome) + affinity tags (filled in W4).
1278
+ *
1279
+ * `id` is a plain `string`, not `CanonicalThemeId` — the 13 builtins satisfy
1280
+ * this (`CanonicalThemeId` is a subtype of `string`), but `registerTheme`
1281
+ * below (W3 task 4's SDK registration seam) must also accept ids outside that
1282
+ * closed union.
1283
+ */
1284
+ interface ThemeDefinition {
1285
+ id: string;
1286
+ style: StyleTokens;
1287
+ brand: BrandConfig;
1288
+ tags: readonly string[];
1289
+ /**
1290
+ * 主题的「选择权」配置(spec §3 theme.layouts 命名裁决;W2 任务 2 由
1291
+ * src/themes/manifest.ts〔已删除〕的旧选择权类型原地迁居于此)——四页型
1292
+ * 各自允许哪些 archetype 参与自动选型。排印/色彩在 style,这里只放集合。
1293
+ * **W4 全集放开**(spec §3「缺省 = 全集,策展收窄塑造个性」,design
1294
+ * decision 7):十三内置主题四页型默认均为 {@link fullArchetypeSet} 的
1295
+ * 全集。design decision 7/8 曾经的六处对比度策展排除(luxe/campaign/
1296
+ * classroom 的 content 排除 banner-heading、tech 的 cover/content、
1297
+ * consulting 的 chapter)已在 W4 fix round 随对比度自适应 ink helper
1298
+ * (`src/svg/ink.ts`)的根因修复全部撤销。fix round 自身新发现的三处
1299
+ * (bloom/classroom/heritage 的 chapter 排除 fashion-chapter)也已在
1300
+ * post-v0.3 W8 fix round 随 `readableOn` 两墨实测对比度取优的根因修复一并
1301
+ * 撤销(backlog item 2)——十三主题四页型现在均为不折不扣的全集,无任何
1302
+ * 排除残留。页型空集 = 该页型回落调用侧兜底(十三主题四页型均非空,
1303
+ * `definitions.test.ts` 锁死)。id 是通用 string(不再按页型区分
1304
+ * archetype id 联合类型)。
1305
+ */
1306
+ layouts: Record<Slide["type"], readonly string[]>;
1307
+ /** Motif:单值,非 allowed-set(spec §3 示意)。undefined = 该主题无 motif 装饰(十三主题中 runway 留空,其余均已设)。 */
1308
+ motif?: MotifArchetypeId;
1309
+ }
1310
+ /**
1311
+ * `registerTheme`'s input shape (W4, spec §3 "缺省 = 全集"): identical to
1312
+ * {@link ThemeDefinition} except `layouts` is optional, and — when present —
1313
+ * each of its four slide-type entries is independently optional too. A
1314
+ * slide type this theme doesn't narrow (its own key omitted, or the whole
1315
+ * `layouts` object omitted) defaults to that type's full registered-
1316
+ * archetype set ({@link FULL_LAYOUTS}) — the exact same default every
1317
+ * builtin theme in `LAYOUTS` above resolves to for a slide type it doesn't
1318
+ * curate away from. `getThemeDefinition`/`REGISTERED_THEMES` still only ever
1319
+ * hold the fully-resolved `ThemeDefinition` shape (`layouts` total over all
1320
+ * four types) — `registerTheme` performs the defaulting once, here, so
1321
+ * every downstream reader (`resolveArchetypeId` foremost) can keep assuming
1322
+ * a total record and never re-derive "was this slide type curated or
1323
+ * defaulted".
1324
+ */
1325
+ type ThemeRegistration = Omit<ThemeDefinition, "layouts"> & {
1326
+ layouts?: Partial<Record<Slide["type"], readonly string[]>>;
1327
+ };
1328
+ /**
1329
+ * Register a theme at runtime (SDK seam, not the v0.4 distribution
1330
+ * protocol). Validates just enough to keep the render chain from silently
1331
+ * breaking on a malformed registration — not a full schema:
1332
+ *
1333
+ * - `id` must not collide with a builtin or an already-registered theme.
1334
+ * - each of the four slide types, once defaulted ({@link ThemeRegistration}),
1335
+ * must have at least one layout id that is both registered in
1336
+ * `LAYOUT_REGISTRY` and valid for that slide type (the same registry
1337
+ * `resolveArchetypeId`/`FullSlideSvg` select from — a theme never ships
1338
+ * new render code, only a curated subset of the existing 30 archetypes +
1339
+ * 4 takeovers, per `docs/architecture.md`'s "Adding a theme" section). An
1340
+ * *explicit* empty array for a slide type still fails this check (the
1341
+ * default only kicks in when the key — or `layouts` itself — is omitted
1342
+ * entirely, `undefined`, never for a caller-supplied `[]`).
1343
+ * - `style` must be present (a JS caller can bypass the TS type).
1344
+ *
1345
+ * Once registered, the theme participates in `getInstalledThemeIds`,
1346
+ * `getThemeDefinition` (hence `effective-layout.ts`/`FullSlideSvg`'s
1347
+ * selection and `resolveBrand`), and `themes/index.ts`'s `resolveStyle` —
1348
+ * every internal theme lookup, with no separate "registered theme" branch
1349
+ * for callers to remember.
1350
+ */
1351
+ declare function registerTheme(def: ThemeRegistration): void;
1352
+ /** Every installed theme id: the 13 builtins, then registered themes in registration order. */
1353
+ declare function getInstalledThemeIds(): readonly string[];
1354
+ /**
1355
+ * Resolve a theme id to its full definition — a registered theme first, then
1356
+ * the builtin fallback (`THEME_DEFINITIONS[resolveThemeId(id)]`, which itself
1357
+ * folds an unrecognized id to consulting). The one lookup every internal
1358
+ * consumer that used to read `THEME_DEFINITIONS[resolveThemeId(id)]`
1359
+ * directly (`effective-layout.ts`, `FullSlideSvg.tsx`) now calls instead, so
1360
+ * a registered theme's curated layouts actually drive selection end-to-end.
1361
+ */
1362
+ declare function getThemeDefinition(id: string): ThemeDefinition;
1363
+
1364
+ declare const MODE_VALUES: readonly ["pyramid", "narrative", "instructional", "showcase", "briefing"];
1365
+ declare const DELIVERY_VALUES: readonly ["text", "balanced", "presentation"];
1366
+ declare const AUDIENCE_VALUES: readonly ["executive", "technical", "customer", "public"];
1367
+
1368
+ /**
1369
+ * Narrative argument style (spec §5's five-way mode classification). Each
1370
+ * mode carries a component/layout tendency set (soft-weight material,
1371
+ * consumed by W4's weighted selection — nothing consumes it this wave) and
1372
+ * a rhythm policy (consumed by W5's plan-validate rotation gate).
1373
+ */
1374
+ type Mode = (typeof MODE_VALUES)[number];
1375
+ /**
1376
+ * Content density budget + typographic baseline (spec §5 delivery table).
1377
+ * See {@link DeliveryBudget} for the dual-attribute capacity split this axis
1378
+ * is one half of.
1379
+ */
1380
+ type Delivery = (typeof DELIVERY_VALUES)[number];
1381
+ /**
1382
+ * Tone anchor only (spec §5: audience is tone-anchoring only, no rendering
1383
+ * effect on the IR yet). Reserved for a future lint pass (e.g. executive ×
1384
+ * long paragraphs → suggest kpi_cards/verdict_banner instead). The rule set
1385
+ * itself is explicitly out of scope this wave (spec §10 open questions).
1386
+ */
1387
+ type Audience = (typeof AUDIENCE_VALUES)[number];
1388
+ interface ScenarioAxes {
1389
+ readonly mode: Mode;
1390
+ readonly delivery: Delivery;
1391
+ readonly audience: Audience;
1392
+ }
1393
+
1394
+ interface ModeDefinition {
1395
+ id: Mode;
1396
+ /**
1397
+ * Layout/component tendency set (soft-weight material). Filled row-for-row
1398
+ * from spec §5's mode table for W4's weighted selection step (spec §6
1399
+ * step 4: in-set candidates get ×3 weight, out-of-set ×1 floor — not
1400
+ * implemented yet, this module only stores the data).
1401
+ *
1402
+ * The set deliberately mixes two different vocabularies: component `type`
1403
+ * names (e.g. "kpi_cards", "chart" — see the `Component` discriminated
1404
+ * union in `ir/index.ts`) and layout `id`s (e.g. "image-split" — see
1405
+ * `LAYOUT_REGISTRY` in `svg/layouts/registry.ts`). This is intentional,
1406
+ * not an oversight: W4's weighting step resolves each entry against
1407
+ * whichever vocabulary it belongs to (component types when scoring a
1408
+ * candidate's components, layout ids when scoring the candidate layout
1409
+ * itself). Nothing in this wave (W3) reads this field.
1410
+ */
1411
+ tendencies: readonly string[];
1412
+ /**
1413
+ * Content-archetype soft-weight set for W4's weighted layout selection
1414
+ * (spec §6 step 4: in-set candidates get `TENDENCY_WEIGHT` (×3), out-of-set
1415
+ * get the `BASE_WEIGHT` floor (×1) — both named constants live in
1416
+ * `svg/effective-layout.ts`, next to `resolveArchetypeId`, the sole
1417
+ * consumer). Deliberately a separate field from {@link tendencies} above,
1418
+ * not a reinterpretation of it: `tendencies` mixes component-type names and
1419
+ * layout ids drawn from spec §5's mode table verbatim and also feeds W5's
1420
+ * plan `focus` vocabulary gate, so narrowing its meaning here would be a
1421
+ * breaking change to an existing consumer. This field holds only
1422
+ * `LAYOUT_REGISTRY` content-archetype ids (`svg/layouts/registry.ts`'s
1423
+ * `CONTENT_LAYOUTS` keys) — cover/chapter/ending ids never appear in any
1424
+ * mode's list here, which is exactly why `resolveArchetypeId`'s weighting
1425
+ * is a no-op for those three slide types (spec: "身份页个性来自 theme 不来自
1426
+ * mode,均匀取样") without needing a slide-type special case — a weight
1427
+ * lookup against an id that can never match falls through to the ×1 floor
1428
+ * for every candidate, uniform by construction. `tone-adaptive-content`
1429
+ * appears in no mode's list either (spec's "万金油" call-out: it is the one
1430
+ * content archetype meant to read as mode-neutral, so it always gets the
1431
+ * ×1 floor too).
1432
+ */
1433
+ layoutTendencies: readonly string[];
1434
+ /**
1435
+ * Rhythm template descriptor (spec §5's per-mode rhythm-default column),
1436
+ * parameterized by mode for W5's plan-validate rotation gate — e.g.
1437
+ * briefing is exempt from a generic "three same-rhythm pages in a row is
1438
+ * an error" rule because uniform-dense *is* briefing's correct default,
1439
+ * not a violation of it (spec §5's plan-gate section calls out that a
1440
+ * generic same-rhythm-streak rule would reject briefing's own default).
1441
+ * Not consumed this wave.
1442
+ */
1443
+ rhythmPolicy: "anchor-open" | "alternate" | "repetition-ok" | "anchor-sparse" | "uniform-dense";
1444
+ }
1445
+ declare const MODE_DEFINITIONS: Record<Mode, ModeDefinition>;
1446
+
1447
+ interface DeliveryBudget {
1448
+ /**
1449
+ * Body-text baseline, in px, at 1280×720 slide geometry (spec §5
1450
+ * delivery table's body-baseline column). Wired into rendering as of W4
1451
+ * task 3 (design decision 9): `src/svg/FullSlideSvg.tsx` resolves
1452
+ * `DELIVERY_BUDGETS[resolveScenario(ir.scenario).delivery].bodyBaselinePx`
1453
+ * once and passes it into `buildCtx`, which stores it as
1454
+ * `ComponentCtx.bodyFontPx` — the sole font-size input for the
1455
+ * paragraph/bullets/callout trio ("正文" = continuous running text).
1456
+ * Every other component's own bespoke type scale, the heading system, and
1457
+ * quote's fixed 26px attribution line don't read this field. Since
1458
+ * `balanced` (24px) is the scenario default, an omitted-scenario deck now
1459
+ * renders body text at 24px, not the previous fixed 20px — the one-time,
1460
+ * spec-sanctioned snapshot re-pin that landed alongside this wiring.
1461
+ */
1462
+ bodyBaselinePx: number;
1463
+ /**
1464
+ * Per-slide editorial budget (component count) — content discipline
1465
+ * ("how many things belong on this slide"), not geometry. Spec §5's
1466
+ * dual-attribute capacity split keeps *physical* capacity ("how many
1467
+ * things fit in this layout's slots") on the layout registry's body-slot
1468
+ * `capacity` metadata (`svg/layouts/registry.ts`). The W3 quality gate
1469
+ * takes `min(this budget, the resolved layout's body capacity)`.
1470
+ */
1471
+ maxComponentsPerSlide: number;
1472
+ bullets: {
1473
+ maxItems: number;
1474
+ /**
1475
+ * Same "unit" concept the deleted `CAPACITY.bullets` used
1476
+ * (`measureTextUnits`, CJK weight = 1.0) — a visual-width-weighted
1477
+ * character count, not a raw `.length`. The old physical ceiling (53,
1478
+ * derived from render geometry) was deleted in W3 without a replacement
1479
+ * check because every delivery's editorial budget here (30/40/48) is
1480
+ * already strictly tighter than it — the quality gate now applies
1481
+ * exactly this one number per delivery.
1482
+ */
1483
+ maxUnitsPerItem: number;
1484
+ };
1485
+ }
1486
+ /**
1487
+ * Pinned to spec §5's delivery table (`bodyBaselinePx` / editorial budget /
1488
+ * bullets budget columns): text 20/5/6×48, balanced 24/4/5×40,
1489
+ * presentation 32/3/4×30.
1490
+ */
1491
+ declare const DELIVERY_BUDGETS: Record<Delivery, DeliveryBudget>;
1492
+
1493
+ interface ScenarioPreset {
1494
+ id: string;
1495
+ axes: ScenarioAxes;
1496
+ /**
1497
+ * Soft theme recommendations — a suggestion, never a hard constraint
1498
+ * (spec §5). Surfaced in workflow step ① so an agent can open with a
1499
+ * themed proposal — the user may still pick any theme. Every entry here
1500
+ * must be a real `BUILTIN_THEME_IDS` member (`ir/index.ts`) — enforced by
1501
+ * this module's test suite, which imports and tests against it.
1502
+ */
1503
+ themeRecommendations: readonly string[];
1504
+ }
1505
+ /** Pinned to spec §5's 7 named presets and their theme recommendation table. */
1506
+ declare const SCENARIO_PRESETS: Record<string, ScenarioPreset>;
1507
+ /**
1508
+ * = `SCENARIO_PRESETS.general.axes` (briefing × balanced × public) — the
1509
+ * global default when scenario is omitted entirely (spec §5's defaults
1510
+ * chain).
1511
+ */
1512
+ declare const DEFAULT_SCENARIO: ScenarioAxes;
1513
+ /**
1514
+ * Resolve a scenario input down to concrete axes, per spec §5's design
1515
+ * principle "omission gets the default, a typo is a hard error" (weak-model
1516
+ * friendly: a model that leaves a field out gets a sane deck, a model that
1517
+ * misspells a value gets a loud, actionable error instead of a silently
1518
+ * wrong deck):
1519
+ *
1520
+ * - `undefined` → {@link DEFAULT_SCENARIO} (the `general` preset's axes)
1521
+ * - a preset id string → that preset's axes (unknown id throws
1522
+ * {@link PptfastError}, listing the available preset ids)
1523
+ * - a partial axes object → each axis defaults independently
1524
+ * (mode → "briefing", delivery → "balanced", audience → "public" — these
1525
+ * happen to equal `DEFAULT_SCENARIO`'s values because `general` *is* that
1526
+ * exact combination, but the fallback here is per-axis, not "any omitted
1527
+ * axis falls back to the whole default object")
1528
+ *
1529
+ * An unknown axis value, or an unknown key on the partial axes object,
1530
+ * always throws {@link PptfastError} (never silently ignored or dropped) —
1531
+ * omission and a typo are different intents, and only the former has a
1532
+ * reasonable default.
1533
+ */
1534
+ declare function resolveScenario(input: string | Partial<ScenarioAxes> | undefined): ScenarioAxes;
1535
+
1536
+ /**
1537
+ * Deck plan schema + validation (spec §5 "plan artifact and hard gates", W5 task 2).
1538
+ *
1539
+ * A plan is a workflow artifact, not a render prerequisite (spec §5's
1540
+ * "escape hatch": a bare IR v3 renders directly, plan validate is a separate,
1541
+ * optional gate for the plan-authoring stage of the six-phase workflow).
1542
+ * This module stays pure and Node-free — no `fs`, no CLI concerns — so it
1543
+ * can sit in `src/index.ts`'s dependency closure exactly like `src/ir` and
1544
+ * `src/scenario` already do (`AGENTS.md`'s layout rule). `src/cli/commands.ts`
1545
+ * owns the one Node-touching wrapper (`runPlanValidate`: read file, call
1546
+ * {@link validatePlan}, format the result).
1547
+ *
1548
+ * Design mirrors `api.ts`'s `validateIr`/`ValidateResult` throughout
1549
+ * (structural zod pass first, then a sequential chain of hard-gate
1550
+ * categories, each short-circuiting the chain on its own failure — see
1551
+ * {@link validatePlan}'s own comment for why): same overall shape, adapted
1552
+ * for plan pages being keyed by an author-assigned `id` instead of IR's
1553
+ * positional slide index.
1554
+ */
1555
+
1556
+ /**
1557
+ * Mirrors `SlideSchema.type` / `SlideType` (`ir/index.ts`,
1558
+ * `svg/layouts/registry.ts`) exactly. Kept as an independent literal tuple
1559
+ * here (not imported from either) — a plan page's `type` is a 4-value enum
1560
+ * on its own schema, not a re-export of IR's — but the `satisfies` clause
1561
+ * makes any future drift between the two a compile error instead of a
1562
+ * silent mismatch.
1563
+ */
1564
+ declare const PAGE_TYPES: readonly ["cover", "chapter", "content", "ending"];
1565
+ declare const RHYTHM_VALUES: readonly ["anchor", "dense", "breathing"];
1566
+ type PlanPageType = (typeof PAGE_TYPES)[number];
1567
+ type PlanRhythm = (typeof RHYTHM_VALUES)[number];
1568
+ /**
1569
+ * A single plan page (spec §5). `type`/`heading` are required — unlike IR's
1570
+ * own `SlideSchema` (where both default/omit for weak-model friendliness),
1571
+ * a plan is the authoring artifact those fields get *locked* from at
1572
+ * assemble time (W5 task 3), so leaving either implicit here would defeat
1573
+ * the point. `rhythm`/`focus`/`summary` stay optional per spec §5's defaults
1574
+ * chain ("rhythm omitted → auto-rotates by page position — focus/summary/layout/slot can all be omitted").
1575
+ */
1576
+ declare const PlanPageSchema: z.ZodObject<{
1577
+ id: z.ZodString;
1578
+ type: z.ZodEnum<{
1579
+ cover: "cover";
1580
+ chapter: "chapter";
1581
+ content: "content";
1582
+ ending: "ending";
1583
+ }>;
1584
+ heading: z.ZodString;
1585
+ rhythm: z.ZodOptional<z.ZodEnum<{
1586
+ anchor: "anchor";
1587
+ dense: "dense";
1588
+ breathing: "breathing";
1589
+ }>>;
1590
+ focus: z.ZodOptional<z.ZodString>;
1591
+ summary: z.ZodOptional<z.ZodString>;
1592
+ }, z.core.$strict>;
1593
+ type PlanPage = z.infer<typeof PlanPageSchema>;
1594
+ /**
1595
+ * Top-level deck plan shape (spec §5). `scenario`/`theme` deliberately have
1596
+ * no schema-level `.default(...)` — same reasoning as `PptxIRSchema`'s own
1597
+ * `scenario` field (`ir/index.ts`): the resolved value is never baked back
1598
+ * into the parsed shape, {@link validatePlan} (here) and, later, assemble
1599
+ * (W5 task 3) each resolve it themselves. `seed` is accepted but entirely
1600
+ * unexamined by this module — "not validatePlan's concern" (spec's own wording):
1601
+ * assemble generates and suggests writing one back on first materialization.
1602
+ */
1603
+ declare const DeckPlanSchema: z.ZodObject<{
1604
+ version: z.ZodDefault<z.ZodLiteral<"1">>;
1605
+ scenario: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
1606
+ theme: z.ZodOptional<z.ZodString>;
1607
+ filename: z.ZodOptional<z.ZodString>;
1608
+ seed: z.ZodOptional<z.ZodNumber>;
1609
+ meta: z.ZodDefault<z.ZodObject<{
1610
+ organization: z.ZodOptional<z.ZodString>;
1611
+ authors: z.ZodOptional<z.ZodArray<z.ZodObject<{
1612
+ name: z.ZodString;
1613
+ role: z.ZodOptional<z.ZodString>;
1614
+ org: z.ZodOptional<z.ZodString>;
1615
+ }, z.core.$strict>>>;
1616
+ date: z.ZodOptional<z.ZodString>;
1617
+ version: z.ZodOptional<z.ZodString>;
1618
+ confidentiality: z.ZodOptional<z.ZodEnum<{
1619
+ public: "public";
1620
+ internal: "internal";
1621
+ confidential: "confidential";
1622
+ restricted: "restricted";
1623
+ }>>;
1624
+ contact: z.ZodOptional<z.ZodObject<{
1625
+ name: z.ZodOptional<z.ZodString>;
1626
+ email: z.ZodOptional<z.ZodString>;
1627
+ phone: z.ZodOptional<z.ZodString>;
1628
+ website: z.ZodOptional<z.ZodString>;
1629
+ }, z.core.$strict>>;
1630
+ copyright: z.ZodOptional<z.ZodString>;
1631
+ animation: z.ZodOptional<z.ZodObject<{
1632
+ transition: z.ZodOptional<z.ZodEnum<{
1633
+ fade: "fade";
1634
+ push: "push";
1635
+ wipe: "wipe";
1636
+ none: "none";
1637
+ }>>;
1638
+ elements: z.ZodOptional<z.ZodEnum<{
1639
+ none: "none";
1640
+ auto: "auto";
1641
+ }>>;
1642
+ }, z.core.$strict>>;
1643
+ }, z.core.$strict>>;
1644
+ brand: z.ZodOptional<z.ZodObject<{
1645
+ logo_asset_id: z.ZodOptional<z.ZodString>;
1646
+ position: z.ZodOptional<z.ZodEnum<{
1647
+ tl: "tl";
1648
+ tr: "tr";
1649
+ bl: "bl";
1650
+ br: "br";
1651
+ }>>;
1652
+ }, z.core.$strict>>;
1653
+ pages: z.ZodArray<z.ZodObject<{
1654
+ id: z.ZodString;
1655
+ type: z.ZodEnum<{
1656
+ cover: "cover";
1657
+ chapter: "chapter";
1658
+ content: "content";
1659
+ ending: "ending";
1660
+ }>;
1661
+ heading: z.ZodString;
1662
+ rhythm: z.ZodOptional<z.ZodEnum<{
1663
+ anchor: "anchor";
1664
+ dense: "dense";
1665
+ breathing: "breathing";
1666
+ }>>;
1667
+ focus: z.ZodOptional<z.ZodString>;
1668
+ summary: z.ZodOptional<z.ZodString>;
1669
+ }, z.core.$strict>>;
1670
+ }, z.core.$strict>;
1671
+ type DeckPlan = z.infer<typeof DeckPlanSchema>;
1672
+ /** JSON Schema for the deck plan — feed this to a model before it writes a plan (see `pptfast schema --plan`). */
1673
+ declare function planJsonSchema(): Record<string, unknown>;
1674
+ interface PlanValidationIssue {
1675
+ path: string;
1676
+ message: string;
1677
+ /** The offending page's `id`, when the issue is scoped to one specific
1678
+ * page and that page's `id` could be determined — absent for deck-level
1679
+ * issues (e.g. "pages non-empty") and for structural issues on a page
1680
+ * whose own `id` itself failed to parse. */
1681
+ pageId?: string;
1682
+ }
1683
+ interface PlanValidateResult {
1684
+ ok: boolean;
1685
+ plan?: DeckPlan;
1686
+ errors: PlanValidationIssue[];
1687
+ }
1688
+ declare function formatPlanIssues(errors: PlanValidationIssue[]): string;
1689
+ /**
1690
+ * Plan-level theme default (spec §5's defaults chain: "theme omitted →
1691
+ * consulting") — the same default IR's own `theme.id` field carries
1692
+ * (`ThemeSchema` in `ir/index.ts`). Exported so a caller already holding a
1693
+ * validated {@link DeckPlan} (the CLI's OK-summary line) doesn't re-derive
1694
+ * the fallback itself.
1695
+ */
1696
+ declare function resolvePlanThemeId(plan: DeckPlan): string;
1697
+ /**
1698
+ * Deck-level page-count range per delivery (spec §5's delivery table,
1699
+ * initial values — "text 8-30 / balanced 6-24 / presentation 4-16", not yet
1700
+ * tuned against real usage). Independent of `DELIVERY_BUDGETS`
1701
+ * (`scenario/index.ts`, per-slide component-count/bullets editorial
1702
+ * budget) — this is a separate, deck-wide page-count concern the spec calls
1703
+ * out as its own hard gate ("page count vs. delivery recommended range").
1704
+ */
1705
+ declare const PLAN_PAGE_COUNT_RANGE: Record<Delivery, {
1706
+ min: number;
1707
+ max: number;
1708
+ }>;
1709
+ /**
1710
+ * Validate raw JSON against the plan schema, then — once it parses — run the
1711
+ * spec §5 hard-gate chain. Mirrors `validateIr`'s (`api.ts`) overall shape: a
1712
+ * structural zod pass first, then a sequence of isolated hard-gate
1713
+ * categories, each short-circuiting the whole chain on its own failure
1714
+ * (rather than accumulating errors across categories) so a later category
1715
+ * never has to guess at what an earlier, already-broken one would have
1716
+ * meant — e.g. rhythm-rotation and page-count both need a resolved
1717
+ * scenario, so nothing past the scenario/theme stage runs until that
1718
+ * resolves cleanly. Every plan-gate philosophy here is "hard block, no soft
1719
+ * warning" (spec §5's "escape hatch" section — a plan that doesn't fit this shape
1720
+ * should be authored as bare IR instead, not warned-and-shipped).
1721
+ */
1722
+ declare function validatePlan(input: unknown): PlanValidateResult;
1723
+
1724
+ /**
1725
+ * One page's fillable content — everything a plan page's `id` does *not*
1726
+ * already lock in. Deliberately excludes `type`/`heading` (plan-owned, see
1727
+ * {@link assembleDeck}'s locked-field gate) and `subheading`/`decor`
1728
+ * (legitimate `Slide` fields, but outside this record's shape by spec §7's
1729
+ * own layout — "pages/<id>.json contains only components" — a plan/pages deck can't
1730
+ * author either one — a hand-authored bare IR still can). Every field here is
1731
+ * a same-name, same-shape subset of `Slide`'s own optional fields
1732
+ * (`../ir`'s `SlideSchema`) — reused, not redeclared, so the two can't drift.
1733
+ */
1734
+ interface PageContent {
1735
+ components?: Component[];
1736
+ layout?: string;
1737
+ arrangement?: NonNullable<Slide["arrangement"]>;
1738
+ background?: BackgroundSpec;
1739
+ image_side?: "left" | "right";
1740
+ footnote?: string;
1741
+ }
1742
+ interface AssembleResult {
1743
+ ir: PptxIR;
1744
+ /**
1745
+ * Set only when `plan.seed` was absent and {@link assembleDeck} generated
1746
+ * one deterministically (see the seed section of this function's doc
1747
+ * comment) — `undefined` when `plan.seed` was already present and simply
1748
+ * passed through. The CLI shell (W5 task 5) is the one that acts on this:
1749
+ * suggest writing the value back into the plan file, never rewrite it
1750
+ * itself (assemble stays a pure function, no fs side effects here).
1751
+ */
1752
+ generatedSeed?: number;
1753
+ /**
1754
+ * Count of pages whose `layout` field this call filled in via
1755
+ * {@link materializeEffectiveLayouts} (W4 design decision 10) —
1756
+ * `undefined` when zero, same "absent means nothing to report" posture as
1757
+ * {@link generatedSeed} just above (a page with an explicit pin already in
1758
+ * its page file, or every omitted page landing on the image-cover
1759
+ * takeover's `null` bypass, both leave this unset). Purely informational:
1760
+ * the CLI shell (`runAssemble`, `../cli/commands.ts`) surfaces it as a
1761
+ * one-line note, nothing here or downstream acts on the number itself.
1762
+ */
1763
+ materializedLayoutCount?: number;
1764
+ }
1765
+ /**
1766
+ * Assemble a validated plan plus a per-page content record into a renderable
1767
+ * IR. See this module's own top comment for the overall shape. Step numbers
1768
+ * below match the W5 task-3 brief's own numbered "inject semantics" list verbatim —
1769
+ * kept in that exact order because two of them (locked-field vs. orphan)
1770
+ * both throw and their relative order is otherwise unobservable from either
1771
+ * doc comment alone.
1772
+ *
1773
+ * 1. `plan` is `unknown` (same boundary `validatePlan` itself has — a plan
1774
+ * is almost always freshly `JSON.parse`d off disk by the caller) —
1775
+ * invalid shape or a failed hard gate throws {@link PptfastError} with
1776
+ * `validatePlan`'s own formatted issue list, not a re-derived message.
1777
+ * 2. Shape guard + locked-field protection: a `pages[id]` entry must first be
1778
+ * a plain object — not `null`, an array, or a primitive — else throws.
1779
+ * `pages` is `unknown`-shaped off disk same as `plan` itself (step 1), so
1780
+ * a JSON `null`/string/array content value is a real possibility, not
1781
+ * just a type-system hole, and `Object.hasOwn` throws its own
1782
+ * uninformative native `TypeError` on `null` (and silently no-ops on a
1783
+ * string/array/number) rather than this gate's own readable message.
1784
+ * Once that holds, a `pages[id]` entry that carries a `type` or `heading`
1785
+ * *key* — even set to `undefined` — throws. `Object.hasOwn`, not a
1786
+ * `!== undefined` read, is what makes that "even `undefined`" case
1787
+ * catchable: `PageContent` itself never declares either field, but a
1788
+ * page file freshly parsed off disk is `unknown` before it reaches this
1789
+ * function's declared `PageContent` parameter type, so a stray
1790
+ * `"heading": null`-turned-`undefined` or a copy-pasted empty key is
1791
+ * exactly the drift this gate exists to catch instead of silently
1792
+ * ignoring.
1793
+ * 3. Orphan keys: a `pages` entry whose id isn't any plan page's id. Listed
1794
+ * together with a fix suggestion, checked only after every present page
1795
+ * has cleared the locked-field gate (step 2) — an orphan file that
1796
+ * *also* happens to redeclare `heading` reports as locked-field first.
1797
+ * 4. Missing pages (a plan id with no `pages` entry) become a placeholder
1798
+ * slide — never an error. Spec §7's own words: "assemble's precise
1799
+ * semantics — a missing page always succeeds (placeholder), a structural
1800
+ * contradiction (orphan file / bad plan / id conflict) errors". A
1801
+ * declared `summary` becomes the placeholder's `subheading` (the one spot
1802
+ * `summary` — otherwise a plan-only anchor, see step 5 — does reach the
1803
+ * IR) so a `--draft` preview of an unfilled deck still reads as more than
1804
+ * a bare "Untitled".
1805
+ * 5. Present pages become a full slide: `id`/`type`/`heading` from the plan
1806
+ * page (never the content record — see step 2), plus whichever of
1807
+ * {@link PageContent}'s six fields the content record actually set.
1808
+ * `rhythm`/`focus`/`summary` never reach the IR for a present page —
1809
+ * they are plan-only authoring anchors (rhythm/focus steer a future
1810
+ * fill/select step, summary is "for the fill step's own reading only"), not slide content.
1811
+ * 6. Top-level: `version` is always the literal `"3"` (IR's own version,
1812
+ * unrelated to the plan's own `version: "1"`). `scenario`/`theme`/
1813
+ * `filename`/`brand`/`meta`/`seed` (step 7) carry over from the plan when
1814
+ * present. When absent, this function omits the field from the raw
1815
+ * object it hands to {@link PptxIRSchema} rather than re-deriving IR's
1816
+ * own default value a second time — one default source of truth, the
1817
+ * schema itself (e.g. `theme` omitted here becomes `{ id: "consulting" }`,
1818
+ * exactly like a bare hand-authored IR that never mentions theme at all —
1819
+ * not a value this function needs to know).
1820
+ * 7. Seed: `plan.seed` present → passed through, `generatedSeed` stays
1821
+ * `undefined` on the result. Absent → {@link generateSeed} derives one
1822
+ * from `filename` + the plan's own ordered page-id list (never page
1823
+ * *content* — see that function's own doc comment for why), written to
1824
+ * `ir.seed` *and* returned as `generatedSeed` so a CLI shell can suggest
1825
+ * writing it back into the plan file (this function never touches disk
1826
+ * itself).
1827
+ * 8. Idempotence: every step above is a pure function of its inputs (no
1828
+ * randomness, no wall-clock, no reliance on unordered iteration) — two
1829
+ * calls with structurally-equal `plan`/`pages` produce deep-equal
1830
+ * results, `generatedSeed` included. Exercised directly by this module's
1831
+ * test suite rather than asserted here.
1832
+ */
1833
+ declare function assembleDeck(plan: unknown, pages: Record<string, PageContent>): AssembleResult;
1834
+ /**
1835
+ * Inverse of {@link assembleDeck}: reconstructs `{ plan, pages }` from an
1836
+ * existing IR well enough that `assembleDeck(...disassembleDeck(ir))`
1837
+ * reproduces every slide's content — but the map is not lossless in both
1838
+ * directions, only in the direction the round trip actually exercises
1839
+ * (IR → plan/pages → IR). Fields with no IR-side home never survive being
1840
+ * written *to* the IR in the first place, so there is nothing here to read
1841
+ * back:
1842
+ *
1843
+ * - `rhythm` and `focus` never appear on any produced {@link PlanPage} —
1844
+ * both are plan-only authoring anchors with no corresponding `Slide`
1845
+ * field at all (see {@link assembleDeck} step 5's doc comment). Nothing
1846
+ * here could recover a value that was never written anywhere.
1847
+ * - `summary` is recovered *only* for a placeholder slide, by reversing
1848
+ * step 4's `summary` → `subheading` injection (`slide.subheading` back to
1849
+ * `planPage.summary`). A non-placeholder slide's own `subheading` — a
1850
+ * legitimate, independent `Slide` field a hand-authored bare IR is free to
1851
+ * set — has no {@link PageContent} field to land in (spec §7's pages/
1852
+ * record is deliberately narrower than `Slide` itself, see that
1853
+ * interface's own doc comment) and no `summary` semantics either (`summary`
1854
+ * only ever flows to a *placeholder*'s `subheading`, never the reverse for
1855
+ * a filled page) — so it is dropped. Same for `decor`: a real `Slide`
1856
+ * field, absent from `PageContent`'s shape entirely.
1857
+ * - `theme.style` / `theme.brand` overrides collapse to a bare theme-id
1858
+ * string (`DeckPlanSchema.theme` has no shape for either) — only `theme.id`
1859
+ * survives. That `theme.brand` is `ThemeSchema.brand` (`BrandConfigSchema`
1860
+ * — `suppressFooterOnCardContent`/`suppressFooterRule`, footer-chrome
1861
+ * flags owned by the *theme*) — not to be confused with the deck-level
1862
+ * `brand` field below, a different, unrelated schema despite the shared
1863
+ * name.
1864
+ * - `ir.assets.images` is not part of this function's return value at all —
1865
+ * `{ plan, pages }` has no `assets` field, and this module stays zero-fs
1866
+ * by design (this file's own top comment), so it has no way to write an
1867
+ * `assets/` directory itself. Any `asset_id` reference inside a copied
1868
+ * `components`/`background` survives untouched ({@link extractPageContent}
1869
+ * copies both as-is), but the underlying image bytes are deliberately left
1870
+ * for the caller: the CLI shell closes that gap. `runDisassemble`
1871
+ * (`../cli/commands.ts`) walks the *input* IR's `ir.assets.images` itself
1872
+ * and materializes every entry into `<outDir>/assets/<id>.<ext>`
1873
+ * (`writeDeckAssets`, `../cli/deck-dir.ts`) so a later `readDeckDir`'s
1874
+ * `scanAssets` re-registers them exactly like any other deck-directory
1875
+ * asset. Skipping that shell-side step is not a doc-comment nuance — it
1876
+ * reproduces as a real bug: an image deck disassembles with every
1877
+ * `asset_id` left dangling, then re-assembles and renders with the image
1878
+ * silently missing.
1879
+ *
1880
+ * Round-trip-safe despite the above, worth calling out because of that name
1881
+ * collision: the top-level `brand` field (`BrandSchema` — `logo_asset_id` /
1882
+ * `position`, the deck logo/position `BrandChrome` reads,
1883
+ * `src/svg/BrandChrome.tsx`) is a plain passthrough on both sides
1884
+ * ({@link assembleDeck} step 6 reads `plan.brand` into `ir.brand` — this
1885
+ * function reads `ir.brand` back into `plan.brand` below) — carried through
1886
+ * unmodified, same as `scenario`/`filename`/`seed`, never synthesized or
1887
+ * dropped.
1888
+ *
1889
+ * `layout` deserves a different kind of callout: it round-trips as plain
1890
+ * content like any other field ({@link extractPageContent} copies
1891
+ * `slide.layout` into `PageContent.layout` whenever the slide has one, no
1892
+ * special case) — but a `deck.json` produced by {@link assembleDeck} now
1893
+ * carries a `layout` on nearly every slide (W4 design decision 10's
1894
+ * materialization, see that function's own doc comment), even on pages whose
1895
+ * *original page file* never set one. Nothing on `Slide` marks which is
1896
+ * which — `layout` is just a string either way — so disassembling an
1897
+ * already-assembled `deck.json` writes every materialized pick into the
1898
+ * regenerated page file as if it had been an explicit pin all along, and a
1899
+ * later re-`assembleDeck` call skips materialization for that page from then
1900
+ * on, same as any hand-authored pin. Accepted, not a bug: `disassembleDeck`
1901
+ * is a one-time bare-IR importer into an editable project directory, not a
1902
+ * round-trip channel for `deck.json` itself. A deck project's own
1903
+ * `pages/<id>.json` files are the durable, edit-in-place artifacts —
1904
+ * `deck.json` is downstream output, and feeding it back through
1905
+ * `disassembleDeck` (instead of editing the project directory that produced
1906
+ * it) is what actually costs those pages their revision-stability
1907
+ * re-selection eligibility going forward. A real, user-visible narrowing,
1908
+ * worth knowing about here, not worth adding code to guard against for a
1909
+ * usage pattern nothing in this codebase actually exercises.
1910
+ *
1911
+ * Two structural fields are synthesized rather than copied when the source
1912
+ * slide omits them, each documented where it is generated:
1913
+ * {@link UNTITLED_HEADING} for a missing/blank `heading`, and a positional
1914
+ * `p-<1-based-ordinal>-<type>` scheme for a missing `id` (stable across
1915
+ * repeated calls on the same IR — it is a pure function of slide position
1916
+ * and type — but, unlike a plan-assigned id, *not* stable across inserting
1917
+ * or reordering slides — out of scope here, since a bare IR with no `id` at
1918
+ * all has no stabler identity to fall back to in the first place).
1919
+ *
1920
+ * That generated `p-<ordinal>-<type>` id is safe by construction as a
1921
+ * page/asset file-name segment (W5 whole-branch review finding 1, verified
1922
+ * — not just asserted — rather than also routing it through
1923
+ * `assertSafeFileSegment`, `../cli/deck-dir.ts`): `<ordinal>` is
1924
+ * `index + 1`, always a plain non-negative integer, and `<type>` is a
1925
+ * `Slide["type"]`, a closed schema enum (`"cover" | "chapter" | "content" |
1926
+ * "ending"`, `SlideSchema.type` in `../ir/index.ts`) — neither half can ever
1927
+ * contain a `/`, a `\`, or resolve to `".."`, so the joined id can't either.
1928
+ * A carried-over `slide.id` (the `??` branch's other side) has no such
1929
+ * guarantee — that is the one this function passes straight through
1930
+ * unchecked, same as every other field {@link extractPageContent} copies —
1931
+ * which is exactly why the write-time gate in `../cli/deck-dir.ts` (not this
1932
+ * function) is what actually closes the vulnerability.
1933
+ */
1934
+ declare function disassembleDeck(ir: PptxIR): {
1935
+ plan: DeckPlan;
1936
+ pages: Record<string, PageContent>;
1937
+ };
1938
+
1939
+ /**
1940
+ * `pptfast audit` finding shape (v0.3 W6, spec §7 workflow ④). `page` is
1941
+ * 1-based (matches `ValidationIssue.page` in `api.ts`); `slideId` is set
1942
+ * whenever the offending slide has a stable `id` (assemble-stamped or
1943
+ * author-supplied), same "page + optional id" convention `formatIssues`
1944
+ * already uses for validate errors.
1945
+ */
1946
+ interface AuditFinding {
1947
+ page: number;
1948
+ slideId?: string;
1949
+ code: "overflow" | "out-of-bounds" | "low-contrast" | "overlap";
1950
+ message: string;
1951
+ detail?: Record<string, unknown>;
1952
+ }
1953
+ interface AuditReport {
1954
+ findings: AuditFinding[];
1955
+ pagesAudited: number;
1956
+ pagesSkipped: number;
1957
+ }
1958
+ /**
1959
+ * Deterministic geometry audit over an already-valid deck (v0.3 W6, spec §7
1960
+ * workflow ④): render every non-placeholder slide off-screen
1961
+ * (`renderSlideSvg`, the same single-source SVG the preview and exporter
1962
+ * both use) and run three check families against the rendered markup —
1963
+ * overflow/out-of-bounds (reusing `svg-audit.ts`'s existing walker
1964
+ * verbatim), low-contrast (WCAG relative luminance), and overlap (pairwise
1965
+ * `data-audit-box` intersection). Pure — no I/O, no Node dependency (see
1966
+ * `parseSvg`'s doc comment) — `auditDeck` itself never calls
1967
+ * `installNodePlatform()`; that's the caller's job (the CLI does it
1968
+ * automatically).
1969
+ *
1970
+ * Advisory, not a hard gate: `validateIr` already rejects structurally
1971
+ * invalid or over-dense decks before a caller ever gets this far; this
1972
+ * function looks for the visual problems that can still slip through a
1973
+ * valid deck at render time (an author-chosen near-background text color,
1974
+ * two components whose combined content happens to collide). A non-empty
1975
+ * `findings` array is a prompt for a human/agent to look, not a rejection.
1976
+ *
1977
+ * Placeholder pages (`slide.placeholder === true`) are skipped — assemble's
1978
+ * stand-in for content nobody has written yet has nothing to audit, same
1979
+ * reasoning `checkIrQuality` already uses to skip them (`ir-quality.ts`).
1980
+ */
1981
+ declare function auditDeck(ir: PptxIR): AuditReport;
1075
1982
 
1076
- export { type Assets, type BackgroundSpec, type Block, type Meta, PptfastError, type PptxIR, PptxIRSchema, type Slide, THEME_IDS, type ThemeInfo, VERSION, type ValidateResult, type ValidationIssue, formatIssues, generatePptx, irJsonSchema, listThemes, renderSlideSvg, validateIr };
1983
+ export { AUDIENCE_VALUES, type AssembleResult, type Assets, type Audience, type AuditFinding, type AuditReport, BUILTIN_THEME_IDS, type BackgroundSpec, type BrandConfig, BrandConfigSchema, COMPONENT_TYPES, type Component, DEFAULT_SCENARIO, DELIVERY_BUDGETS, DELIVERY_VALUES, type DeckPlan, DeckPlanSchema, type Delivery, type DeliveryBudget, MODE_DEFINITIONS, MODE_VALUES, type Meta, type Mode, type ModeDefinition, PLAN_PAGE_COUNT_RANGE, type PageContent, type PlanPage, PlanPageSchema, type PlanPageType, type PlanRhythm, type PlanValidateResult, type PlanValidationIssue, PptfastError, type PptxIR, PptxIRSchema, SCENARIO_PRESETS, type ScenarioAxes, type ScenarioPreset, type Slide, type StyleOverride, StyleOverrideSchema, type ThemeDefinition, type ThemeInfo, type ThemeRegistration, ThemeSchema, VERSION, type ValidateResult, type ValidationIssue, assembleDeck, auditDeck, disassembleDeck, formatIssues, formatPlanIssues, generatePptx, getInstalledThemeIds, getThemeDefinition, irJsonSchema, listThemes, planJsonSchema, registerTheme, renderSlideSvg, resolvePlanThemeId, resolveScenario, styleJsonSchema, validateIr, validatePlan };