ppt-template-reuse 0.4.0-rc.2

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
@@ -0,0 +1,573 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ parseArgs,
8
+ readJson,
9
+ requireArg,
10
+ writeJson,
11
+ } from "./planner-lib.mjs";
12
+
13
+ const DEFAULT_EXEMPT_ROLES = new Set([
14
+ "opening",
15
+ "agenda",
16
+ "roadmap",
17
+ "closing",
18
+ "section",
19
+ "reference",
20
+ "references",
21
+ ]);
22
+
23
+ function usage() {
24
+ return [
25
+ "Usage:",
26
+ " node content-audit.mjs --content-plan <plan.json> [--report <audit.json>]",
27
+ ].join("\n");
28
+ }
29
+
30
+ function text(value) {
31
+ return typeof value === "string" ? value.trim() : "";
32
+ }
33
+
34
+ function nonWhitespaceChars(value) {
35
+ return [...String(value ?? "").replace(/\s+/gu, "")].length;
36
+ }
37
+
38
+ function collectScalarStrings(value, output = []) {
39
+ if (typeof value === "string") {
40
+ if (value.trim()) output.push(value.trim());
41
+ return output;
42
+ }
43
+ if (Array.isArray(value)) {
44
+ for (const item of value) collectScalarStrings(item, output);
45
+ return output;
46
+ }
47
+ if (value && typeof value === "object") {
48
+ for (const [key, item] of Object.entries(value)) {
49
+ if (["path", "alt", "contentType", "prompt"].includes(key)) continue;
50
+ collectScalarStrings(item, output);
51
+ }
52
+ }
53
+ return output;
54
+ }
55
+
56
+ function visibleStrings(slide) {
57
+ const values = collectScalarStrings(slide.fields || {});
58
+ const seen = new Set(values);
59
+ for (const key of ["title", "subtitle", "body"]) {
60
+ const value = text(slide[key]);
61
+ if (value && !seen.has(value)) {
62
+ seen.add(value);
63
+ values.push(value);
64
+ }
65
+ }
66
+ for (const value of collectScalarStrings(slide.bullets || [])) {
67
+ if (!seen.has(value)) {
68
+ seen.add(value);
69
+ values.push(value);
70
+ }
71
+ }
72
+ return values;
73
+ }
74
+
75
+ function hasSubstantiveValue(value) {
76
+ if (Array.isArray(value)) return value.some(hasSubstantiveValue);
77
+ if (value && typeof value === "object") {
78
+ return Object.values(value).some(hasSubstantiveValue);
79
+ }
80
+ return text(value).length > 0;
81
+ }
82
+
83
+ function comparableText(value) {
84
+ return String(value || "").replace(/\s+/gu, "").trim();
85
+ }
86
+
87
+ function traceMatchesDestination(slide, record) {
88
+ const expected = comparableText(record?.text);
89
+ if (!expected) return false;
90
+ const destination = String(record?.destination || "");
91
+ if (destination === "speakerNotes") {
92
+ return comparableText(slide.speakerNotes).includes(expected);
93
+ }
94
+ if (destination.startsWith("fields.")) {
95
+ const field = destination.slice("fields.".length);
96
+ return collectScalarStrings(slide.fields?.[field]).some(
97
+ (value) => comparableText(value) === expected,
98
+ );
99
+ }
100
+ return false;
101
+ }
102
+
103
+ function normalizePriority(value) {
104
+ return String(value || "").trim().toLowerCase();
105
+ }
106
+
107
+ function normalizedRole(slide) {
108
+ return String(slide.narrativeRole || "").trim().toLowerCase();
109
+ }
110
+
111
+ function isExemptSlide(slide, configuredRoles) {
112
+ const roles = configuredRoles?.length
113
+ ? new Set(configuredRoles.map((role) => String(role).toLowerCase()))
114
+ : DEFAULT_EXEMPT_ROLES;
115
+ return roles.has(normalizedRole(slide));
116
+ }
117
+
118
+ function boundedNumber(value, fallback) {
119
+ const number = Number(value);
120
+ return Number.isFinite(number) ? number : fallback;
121
+ }
122
+
123
+ function ratio(numerator, denominator) {
124
+ return denominator > 0 ? numerator / denominator : 1;
125
+ }
126
+
127
+ export function auditContentPlan(contentPlan, { planPath = "" } = {}) {
128
+ const errors = [];
129
+ const warnings = [];
130
+ const slides = Array.isArray(contentPlan?.slides) ? contentPlan.slides : [];
131
+ const sourceUnits = Array.isArray(contentPlan?.sourceUnits)
132
+ ? contentPlan.sourceUnits
133
+ : [];
134
+ const policy = contentPlan?.deckPolicy?.contentAudit || {};
135
+ const strictRequested =
136
+ Object.keys(policy).length > 0 || sourceUnits.length > 0;
137
+
138
+ if (!slides.length) {
139
+ errors.push("Content plan must contain a non-empty slides array");
140
+ }
141
+
142
+ if (!strictRequested) {
143
+ return {
144
+ schemaVersion: 1,
145
+ planPath,
146
+ passed: errors.length === 0,
147
+ legacyMode: true,
148
+ errors,
149
+ warnings: [
150
+ "Legacy content plan: source-unit coverage, visible-density, notes, and duration gates were not enabled.",
151
+ ],
152
+ metrics: {
153
+ slideCount: slides.length,
154
+ },
155
+ };
156
+ }
157
+
158
+ if (!sourceUnits.length) {
159
+ errors.push("Strict content audit requires a non-empty sourceUnits array");
160
+ }
161
+
162
+ const unitById = new Map();
163
+ for (const [index, unit] of sourceUnits.entries()) {
164
+ const id = text(unit?.id);
165
+ const priority = normalizePriority(unit?.priority);
166
+ if (!id) {
167
+ errors.push(`Source unit ${index + 1} is missing id`);
168
+ continue;
169
+ }
170
+ if (unitById.has(id)) {
171
+ errors.push(`Duplicate source unit id: ${id}`);
172
+ continue;
173
+ }
174
+ if (!["critical", "supporting", "optional"].includes(priority)) {
175
+ errors.push(
176
+ `Source unit ${id} has invalid priority ${JSON.stringify(unit?.priority)}; use critical, supporting, or optional`,
177
+ );
178
+ }
179
+ if (!text(unit?.text) && !text(unit?.summary)) {
180
+ errors.push(`Source unit ${id} is missing text or summary`);
181
+ }
182
+ unitById.set(id, { ...unit, id, priority });
183
+ }
184
+
185
+ const configuredExemptRoles = Array.isArray(policy.exemptNarrativeRoles)
186
+ ? policy.exemptNarrativeRoles
187
+ : undefined;
188
+ const visibleRange = policy.visibleCharsPerContentSlide || {};
189
+ const notesRange = policy.speakerNotesCharsPerContentSlide || {};
190
+ const durationRange = policy.durationSeconds || {};
191
+ const visibleMin = boundedNumber(visibleRange.min, 70);
192
+ const visibleMax = boundedNumber(visibleRange.max, 140);
193
+ const notesMin = boundedNumber(notesRange.min, 150);
194
+ const notesMax = boundedNumber(notesRange.max, 300);
195
+ const enforceVisibleRange = visibleRange.enforce !== false;
196
+ const enforceNotesRange = notesRange.enforce !== false;
197
+ const totalVisibleMin = boundedNumber(policy.totalVisibleCharsMin, 0);
198
+ const totalSpeakerNotesMin = boundedNumber(
199
+ policy.totalSpeakerNotesCharsMin,
200
+ 0,
201
+ );
202
+ const totalSpeakerNotesMax = boundedNumber(
203
+ policy.totalSpeakerNotesCharsMax,
204
+ Number.POSITIVE_INFINITY,
205
+ );
206
+ const durationMin = boundedNumber(durationRange.min, 0);
207
+ const durationMax = boundedNumber(durationRange.max, Number.POSITIVE_INFINITY);
208
+ const criticalCoverageMin = boundedNumber(policy.criticalCoverageMin, 1);
209
+ const weightedCoverageMin = boundedNumber(policy.weightedCoverageMin, 0.9);
210
+ const requireTraceableCoverage = policy.requireTraceableCoverage === true;
211
+ const criticalVisibleCoverageMin = boundedNumber(
212
+ policy.criticalVisibleCoverageMin,
213
+ requireTraceableCoverage ? 1 : 0,
214
+ );
215
+ const weightedVisibleCoverageMin = boundedNumber(
216
+ policy.weightedVisibleCoverageMin,
217
+ requireTraceableCoverage ? 0.9 : 0,
218
+ );
219
+ const sourceTextCoverageMin = boundedNumber(
220
+ policy.sourceTextCoverageMin,
221
+ requireTraceableCoverage ? 1 : 0,
222
+ );
223
+
224
+ const mappedUnitIds = new Set();
225
+ const coveredUnitIds = new Set();
226
+ const visibleTraceUnitIds = new Set();
227
+ const notesTraceUnitIds = new Set();
228
+ const sourceTextTraceUnitIds = new Set();
229
+ const slideMetrics = [];
230
+ let totalVisibleChars = 0;
231
+ let totalSpeakerNotesChars = 0;
232
+ let totalEstimatedSeconds = 0;
233
+ let coreEstimatedSeconds = 0;
234
+
235
+ for (const [index, slide] of slides.entries()) {
236
+ const outputSlide = Number(slide.outputSlide || index + 1);
237
+ const unitIds = Array.isArray(slide.sourceUnitIds)
238
+ ? slide.sourceUnitIds.map(String)
239
+ : [];
240
+ for (const id of unitIds) {
241
+ if (!unitById.has(id)) {
242
+ errors.push(`Slide ${outputSlide} references unknown source unit ${id}`);
243
+ } else {
244
+ mappedUnitIds.add(id);
245
+ }
246
+ }
247
+
248
+ if (requireTraceableCoverage) {
249
+ const visibleTrace = Array.isArray(slide.sourceTrace?.visible)
250
+ ? slide.sourceTrace.visible
251
+ : [];
252
+ const notesTrace = Array.isArray(slide.sourceTrace?.speakerNotes)
253
+ ? slide.sourceTrace.speakerNotes
254
+ : [];
255
+ for (const [traceType, records, destinationSet] of [
256
+ ["visible", visibleTrace, visibleTraceUnitIds],
257
+ ["speakerNotes", notesTrace, notesTraceUnitIds],
258
+ ]) {
259
+ for (const record of records) {
260
+ const sourceUnitId = String(record?.sourceUnitId || "");
261
+ if (!unitById.has(sourceUnitId)) {
262
+ errors.push(
263
+ `Slide ${outputSlide} ${traceType} trace references unknown source unit ${sourceUnitId || "(missing)"}`,
264
+ );
265
+ continue;
266
+ }
267
+ if (!unitIds.includes(sourceUnitId)) {
268
+ errors.push(
269
+ `Slide ${outputSlide} ${traceType} trace references ${sourceUnitId} without mapping it in sourceUnitIds`,
270
+ );
271
+ continue;
272
+ }
273
+ if (!traceMatchesDestination(slide, record)) {
274
+ errors.push(
275
+ `Slide ${outputSlide} ${traceType} trace for ${sourceUnitId} does not match its declared destination`,
276
+ );
277
+ continue;
278
+ }
279
+ destinationSet.add(sourceUnitId);
280
+ coveredUnitIds.add(sourceUnitId);
281
+ if (
282
+ traceType === "speakerNotes" &&
283
+ String(record?.sourceField || "") === "text"
284
+ ) {
285
+ sourceTextTraceUnitIds.add(sourceUnitId);
286
+ }
287
+ }
288
+ }
289
+ for (const id of unitIds) {
290
+ if (
291
+ !visibleTrace.some((record) => String(record?.sourceUnitId) === id) &&
292
+ !notesTrace.some((record) => String(record?.sourceUnitId) === id)
293
+ ) {
294
+ errors.push(
295
+ `Slide ${outputSlide} maps source unit ${id} without a visible or speaker-notes trace`,
296
+ );
297
+ }
298
+ }
299
+ } else {
300
+ for (const id of unitIds) {
301
+ if (unitById.has(id)) coveredUnitIds.add(id);
302
+ }
303
+ }
304
+
305
+ const visibleChars = visibleStrings(slide).reduce(
306
+ (sum, item) => sum + nonWhitespaceChars(item),
307
+ 0,
308
+ );
309
+ const notesChars = nonWhitespaceChars(slide.speakerNotes);
310
+ const estimatedSeconds = boundedNumber(slide.estimatedSeconds, 0);
311
+ const exempt = isExemptSlide(slide, configuredExemptRoles);
312
+ totalVisibleChars += visibleChars;
313
+ totalSpeakerNotesChars += notesChars;
314
+ totalEstimatedSeconds += estimatedSeconds;
315
+ if (String(slide.section || "core").toLowerCase() !== "appendix") {
316
+ coreEstimatedSeconds += estimatedSeconds;
317
+ }
318
+
319
+ if (!exempt) {
320
+ for (const key of ["claim", "evidence", "whyItMatters"]) {
321
+ if (!hasSubstantiveValue(slide[key])) {
322
+ errors.push(
323
+ `Slide ${outputSlide} is an analysis slide but is missing ${key}`,
324
+ );
325
+ }
326
+ }
327
+ if (!unitIds.length) {
328
+ errors.push(`Slide ${outputSlide} does not map any sourceUnitIds`);
329
+ }
330
+ if (visibleChars < visibleMin || visibleChars > visibleMax) {
331
+ const message =
332
+ `Slide ${outputSlide} visible content has ${visibleChars} non-whitespace characters; ` +
333
+ `expected ${visibleMin}-${visibleMax}. Split or remap instead of shrinking typography.`;
334
+ (enforceVisibleRange ? errors : warnings).push(message);
335
+ }
336
+ if (notesChars < notesMin || notesChars > notesMax) {
337
+ const message =
338
+ `Slide ${outputSlide} speaker notes have ${notesChars} non-whitespace characters; ` +
339
+ `expected ${notesMin}-${notesMax}.`;
340
+ (enforceNotesRange ? errors : warnings).push(message);
341
+ }
342
+ if (visibleChars < 20) {
343
+ errors.push(
344
+ `Slide ${outputSlide} contains only a title or an abstract conclusion`,
345
+ );
346
+ }
347
+ }
348
+
349
+ if (hasSubstantiveValue(slide.counterargument) && !hasSubstantiveValue(slide.rebuttal)) {
350
+ errors.push(
351
+ `Slide ${outputSlide} includes a counterargument without a mapped rebuttal`,
352
+ );
353
+ }
354
+ if (!Number.isFinite(Number(slide.estimatedSeconds)) || estimatedSeconds <= 0) {
355
+ errors.push(`Slide ${outputSlide} must declare a positive estimatedSeconds value`);
356
+ }
357
+
358
+ slideMetrics.push({
359
+ outputSlide,
360
+ narrativeRole: slide.narrativeRole || "",
361
+ exempt,
362
+ visibleChars,
363
+ speakerNotesChars: notesChars,
364
+ estimatedSeconds,
365
+ sourceUnitIds: unitIds,
366
+ });
367
+ }
368
+
369
+ for (const unit of unitById.values()) {
370
+ if (
371
+ String(unit.kind || "").toLowerCase() === "counterargument" &&
372
+ coveredUnitIds.has(unit.id)
373
+ ) {
374
+ const mappedSlides = slides.filter((slide) =>
375
+ (slide.sourceUnitIds || []).map(String).includes(unit.id),
376
+ );
377
+ if (!mappedSlides.some((slide) => hasSubstantiveValue(slide.rebuttal))) {
378
+ errors.push(
379
+ `Counterargument source unit ${unit.id} is covered without a rebuttal`,
380
+ );
381
+ }
382
+ }
383
+ }
384
+
385
+ const eligibleUnits = [...unitById.values()].filter(
386
+ (unit) =>
387
+ String(unit.presentationRelevance || "core").toLowerCase() !== "exclude",
388
+ );
389
+ const criticalUnits = eligibleUnits.filter(
390
+ (unit) => unit.priority === "critical",
391
+ );
392
+ const coveredCritical = criticalUnits.filter((unit) =>
393
+ coveredUnitIds.has(unit.id),
394
+ );
395
+ const visiblyCoveredCritical = criticalUnits.filter((unit) =>
396
+ visibleTraceUnitIds.has(unit.id),
397
+ );
398
+ const unitWeight = (unit) =>
399
+ unit.priority === "critical" ? 3 : unit.priority === "optional" ? 0.5 : 1;
400
+ const totalWeight = eligibleUnits.reduce(
401
+ (sum, unit) => sum + unitWeight(unit),
402
+ 0,
403
+ );
404
+ const coveredWeight = eligibleUnits.reduce(
405
+ (sum, unit) =>
406
+ sum + (coveredUnitIds.has(unit.id) ? unitWeight(unit) : 0),
407
+ 0,
408
+ );
409
+ const criticalCoverage = ratio(coveredCritical.length, criticalUnits.length);
410
+ const weightedCoverage = ratio(coveredWeight, totalWeight);
411
+ const criticalVisibleCoverage = requireTraceableCoverage
412
+ ? ratio(visiblyCoveredCritical.length, criticalUnits.length)
413
+ : 1;
414
+ const visibleWeight = eligibleUnits.reduce(
415
+ (sum, unit) =>
416
+ sum + (visibleTraceUnitIds.has(unit.id) ? unitWeight(unit) : 0),
417
+ 0,
418
+ );
419
+ const weightedVisibleCoverage = requireTraceableCoverage
420
+ ? ratio(visibleWeight, totalWeight)
421
+ : 1;
422
+ const sourceTextCoverage = requireTraceableCoverage
423
+ ? ratio(
424
+ eligibleUnits.filter((unit) => sourceTextTraceUnitIds.has(unit.id))
425
+ .length,
426
+ eligibleUnits.length,
427
+ )
428
+ : 1;
429
+
430
+ if (criticalCoverage + Number.EPSILON < criticalCoverageMin) {
431
+ const missing = criticalUnits
432
+ .filter((unit) => !coveredUnitIds.has(unit.id))
433
+ .map((unit) => unit.id);
434
+ errors.push(
435
+ `Critical source-unit coverage is ${(criticalCoverage * 100).toFixed(1)}%; ` +
436
+ `minimum is ${(criticalCoverageMin * 100).toFixed(1)}%. Missing: ${missing.join(", ")}`,
437
+ );
438
+ }
439
+ if (weightedCoverage + Number.EPSILON < weightedCoverageMin) {
440
+ const missing = eligibleUnits
441
+ .filter((unit) => !coveredUnitIds.has(unit.id))
442
+ .map((unit) => unit.id);
443
+ errors.push(
444
+ `Weighted source-unit coverage is ${(weightedCoverage * 100).toFixed(1)}%; ` +
445
+ `minimum is ${(weightedCoverageMin * 100).toFixed(1)}%. Missing: ${missing.join(", ")}`,
446
+ );
447
+ }
448
+ if (
449
+ criticalVisibleCoverage + Number.EPSILON <
450
+ criticalVisibleCoverageMin
451
+ ) {
452
+ const missing = criticalUnits
453
+ .filter((unit) => !visibleTraceUnitIds.has(unit.id))
454
+ .map((unit) => unit.id);
455
+ errors.push(
456
+ `Critical visible source coverage is ${(criticalVisibleCoverage * 100).toFixed(1)}%; ` +
457
+ `minimum is ${(criticalVisibleCoverageMin * 100).toFixed(1)}%. Missing: ${missing.join(", ")}`,
458
+ );
459
+ }
460
+ if (
461
+ weightedVisibleCoverage + Number.EPSILON <
462
+ weightedVisibleCoverageMin
463
+ ) {
464
+ const missing = eligibleUnits
465
+ .filter((unit) => !visibleTraceUnitIds.has(unit.id))
466
+ .map((unit) => unit.id);
467
+ errors.push(
468
+ `Weighted visible source coverage is ${(weightedVisibleCoverage * 100).toFixed(1)}%; ` +
469
+ `minimum is ${(weightedVisibleCoverageMin * 100).toFixed(1)}%. Missing: ${missing.join(", ")}`,
470
+ );
471
+ }
472
+ if (sourceTextCoverage + Number.EPSILON < sourceTextCoverageMin) {
473
+ const missing = eligibleUnits
474
+ .filter((unit) => !sourceTextTraceUnitIds.has(unit.id))
475
+ .map((unit) => unit.id);
476
+ errors.push(
477
+ `Source-text trace coverage is ${(sourceTextCoverage * 100).toFixed(1)}%; ` +
478
+ `minimum is ${(sourceTextCoverageMin * 100).toFixed(1)}%. Missing: ${missing.join(", ")}`,
479
+ );
480
+ }
481
+ if (totalVisibleChars < totalVisibleMin) {
482
+ errors.push(
483
+ `Deck visible content has ${totalVisibleChars} non-whitespace characters; minimum is ${totalVisibleMin}`,
484
+ );
485
+ }
486
+ if (
487
+ totalSpeakerNotesChars < totalSpeakerNotesMin ||
488
+ totalSpeakerNotesChars > totalSpeakerNotesMax
489
+ ) {
490
+ errors.push(
491
+ `Deck speaker notes have ${totalSpeakerNotesChars} non-whitespace characters; expected ${totalSpeakerNotesMin}-${totalSpeakerNotesMax}`,
492
+ );
493
+ }
494
+ const auditedDuration =
495
+ String(policy.durationScope || "").toLowerCase() === "core-only"
496
+ ? coreEstimatedSeconds
497
+ : totalEstimatedSeconds;
498
+ if (auditedDuration < durationMin) {
499
+ warnings.push(
500
+ `Estimated duration is ${auditedDuration} seconds, below the requested minimum ${durationMin}; keep the shorter deck unless the user explicitly asks for expansion.`,
501
+ );
502
+ }
503
+ if (auditedDuration > durationMax) {
504
+ errors.push(
505
+ `Estimated duration is ${auditedDuration} seconds; expected ${durationMin}-${durationMax}`,
506
+ );
507
+ }
508
+
509
+ return {
510
+ schemaVersion: 1,
511
+ planPath,
512
+ passed: errors.length === 0,
513
+ legacyMode: false,
514
+ errors,
515
+ warnings,
516
+ metrics: {
517
+ slideCount: slides.length,
518
+ sourceUnitCount: unitById.size,
519
+ coveredSourceUnitCount: [...coveredUnitIds].filter((id) => unitById.has(id))
520
+ .length,
521
+ mappedSourceUnitCount: [...mappedUnitIds].filter((id) => unitById.has(id))
522
+ .length,
523
+ visiblyCoveredSourceUnitCount: [...visibleTraceUnitIds].filter((id) =>
524
+ unitById.has(id),
525
+ ).length,
526
+ notesCoveredSourceUnitCount: [...notesTraceUnitIds].filter((id) =>
527
+ unitById.has(id),
528
+ ).length,
529
+ criticalCoverage,
530
+ weightedCoverage,
531
+ criticalVisibleCoverage,
532
+ weightedVisibleCoverage,
533
+ sourceTextCoverage,
534
+ totalVisibleChars,
535
+ totalSpeakerNotesChars,
536
+ totalEstimatedSeconds,
537
+ coreEstimatedSeconds,
538
+ auditedDuration,
539
+ durationScope:
540
+ String(policy.durationScope || "").toLowerCase() === "core-only"
541
+ ? "core-only"
542
+ : "all-slides",
543
+ slideMetrics,
544
+ },
545
+ };
546
+ }
547
+
548
+ async function main() {
549
+ const args = parseArgs(process.argv.slice(2));
550
+ if (args.help) {
551
+ console.log(usage());
552
+ return;
553
+ }
554
+ const planPath = path.resolve(requireArg(args, "content-plan"));
555
+ const contentPlan = await readJson(planPath);
556
+ const report = auditContentPlan(contentPlan, { planPath });
557
+ if (args.report) {
558
+ await writeJson(path.resolve(args.report), report);
559
+ }
560
+ console.log(JSON.stringify(report, null, 2));
561
+ if (!report.passed) process.exitCode = 2;
562
+ }
563
+
564
+ const isMain =
565
+ process.argv[1] &&
566
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
567
+ if (isMain) {
568
+ main().catch((error) => {
569
+ console.error(error.stack || error.message || String(error));
570
+ console.error(usage());
571
+ process.exit(1);
572
+ });
573
+ }
@@ -0,0 +1,91 @@
1
+ import path from "node:path";
2
+
3
+ import { fingerprint } from "./io.mjs";
4
+
5
+ export function buildFrameMap(contentPlan, templateProfile, options = {}) {
6
+ const bySlide = new Map(
7
+ (templateProfile.slides || []).map((slide) => [
8
+ Number(slide.sourceSlide),
9
+ slide,
10
+ ]),
11
+ );
12
+ const outputSlides = (contentPlan.slides || []).map((planned, index) => {
13
+ const sourceSlide = Number(planned.sourceSlide);
14
+ const recipe = bySlide.get(sourceSlide);
15
+ if (!recipe) {
16
+ throw new Error(`Content plan references unknown source slide ${sourceSlide}`);
17
+ }
18
+ if (recipe.safetyStatus !== "automatic") {
19
+ throw new Error(
20
+ `Source slide ${sourceSlide} is not approved for automatic editing`,
21
+ );
22
+ }
23
+ const editTargets = [];
24
+ for (const [slotId, value] of Object.entries(planned.fields || {})) {
25
+ const slot = (recipe.slotSchema || []).find(
26
+ (candidate) => candidate.slotId === slotId,
27
+ );
28
+ if (!slot) throw new Error(`Unknown slot ${slotId} on slide ${sourceSlide}`);
29
+ const type = slot.type || "text";
30
+ editTargets.push({
31
+ slotId,
32
+ action:
33
+ type === "text"
34
+ ? "rewrite"
35
+ : ["table", "chart"].includes(type)
36
+ ? "update-data"
37
+ : "replace",
38
+ type,
39
+ semanticType: slot.semanticType || type,
40
+ sourceShapeId: slot.sourceShapeId,
41
+ sourceElementName: slot.sourceElementName,
42
+ sourceBbox: slot.bbox,
43
+ originalText: slot.sourceText || "",
44
+ value,
45
+ });
46
+ }
47
+ const requiredSlots = (recipe.slotSchema || []).filter(
48
+ (slot) => slot.required === true,
49
+ );
50
+ const missing = requiredSlots.filter(
51
+ (slot) => planned.fields?.[slot.slotId] === undefined,
52
+ );
53
+ if (missing.length) {
54
+ throw new Error(
55
+ `Output slide ${index + 1} is missing required slots: ${missing
56
+ .map((slot) => slot.slotId)
57
+ .join(", ")}`,
58
+ );
59
+ }
60
+ return {
61
+ outputSlide: index + 1,
62
+ sourceSlide,
63
+ narrativeRole: planned.narrativeRole,
64
+ layoutFamily: planned.layoutFamily,
65
+ sourceUnitIds: planned.sourceUnitIds || [],
66
+ speakerNotes: planned.speakerNotes || "",
67
+ estimatedSeconds: Number(planned.estimatedSeconds || 0),
68
+ editTargets,
69
+ };
70
+ });
71
+ const frameMap = {
72
+ schemaVersion: 2,
73
+ template: {
74
+ path: templateProfile.source.path,
75
+ sha256: templateProfile.source.sha256,
76
+ profileFingerprint: templateProfile.profileFingerprint,
77
+ },
78
+ contentPlan: options.contentPlanPath
79
+ ? path.resolve(options.contentPlanPath)
80
+ : undefined,
81
+ contentPlanningFingerprint: contentPlan.planningFingerprint,
82
+ fidelityMode:
83
+ contentPlan.deckPolicy?.fidelityMode ||
84
+ (templateProfile.templateKind === "flattened-deck"
85
+ ? "hybrid-locked"
86
+ : "exact-native"),
87
+ outputSlides,
88
+ };
89
+ frameMap.frameMapFingerprint = fingerprint(frameMap);
90
+ return frameMap;
91
+ }
@@ -0,0 +1,38 @@
1
+ export {
2
+ engineVersion,
3
+ createSession,
4
+ inspectSession,
5
+ submitSemantics,
6
+ planSession,
7
+ supplyAssets,
8
+ generateSession,
9
+ startGeneration,
10
+ validateSession,
11
+ cancelSession,
12
+ getSessionStatus,
13
+ engineDoctor,
14
+ } from "./universal-engine.mjs";
15
+ export {
16
+ resolveEngineHome,
17
+ resolveLibraryDir,
18
+ resolveSessionsDir,
19
+ migrateLegacyCapsules,
20
+ } from "./paths.mjs";
21
+ export { extractSource } from "./source-extractor.mjs";
22
+ export { inspectTemplate } from "./template-inspector.mjs";
23
+ export { importLegacyCapsule } from "./legacy-capsule-import.mjs";
24
+ export { buildFrameMap } from "./frame-map.mjs";
25
+ export { buildAdaptiveContentPlan } from "./adaptive-planner.mjs";
26
+ export { auditContentPlan } from "./content-audit.mjs";
27
+ export {
28
+ assessTemplateFit,
29
+ buildTemplateIntelligence,
30
+ } from "./template-intelligence.mjs";
31
+ export { editPptxNative, inspectPptxPackage } from "./ooxml-editor.mjs";
32
+ export {
33
+ renderPptx,
34
+ renderPdfPages,
35
+ rendererDoctor,
36
+ compareProtectedPng,
37
+ compareRenderedTextFidelity,
38
+ } from "./renderer.mjs";