@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/README.md +108 -11
- package/README.zh-CN.md +102 -11
- package/dist/{chunk-4WISUDXE.js → chunk-SROEW6BH.js} +1 -1
- package/dist/chunk-SROEW6BH.js.map +1 -0
- package/dist/{chunk-DVMJWFDL.js → chunk-ZXWCOWJI.js} +5149 -1995
- package/dist/chunk-ZXWCOWJI.js.map +1 -0
- package/dist/cli.js +830 -41
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +968 -61
- package/dist/index.js +57 -5
- package/dist/node.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-4WISUDXE.js.map +0 -1
- package/dist/chunk-DVMJWFDL.js.map +0 -1
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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<"
|
|
639
|
-
filename: z.ZodString
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 —
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
* (
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
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
|
|
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
|
|
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 };
|