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,675 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import {
8
+ exists,
9
+ parseArgs,
10
+ readJson,
11
+ requireArg,
12
+ writeJson,
13
+ } from "./planner-lib.mjs";
14
+
15
+ const USE_CASE_SIGNALS = {
16
+ "academic-defense": [
17
+ /论文|研究|答辩|课题|文献|方法|假设|实验|结论|参考文献|thesis|research|methodology|hypothesis/i,
18
+ ],
19
+ "course-presentation": [
20
+ /课程|作业|课堂|汇报|小组|姓名|学号|指导教师|course|homework|class|assignment/i,
21
+ ],
22
+ "legal-analysis": [
23
+ /法律|民法|刑法|案例|法院|法条|争点|请求权|规范|判决|law|legal|court|case/i,
24
+ ],
25
+ "teaching-training": [
26
+ /教学|培训|学习目标|知识点|练习|课程目标|课堂互动|training|lesson|learning objective/i,
27
+ ],
28
+ "business-pitch": [
29
+ /融资|商业模式|市场规模|竞争优势|投资|团队|痛点|解决方案|pitch|market|investor|business model/i,
30
+ ],
31
+ "product-proposal": [
32
+ /产品|用户|功能|方案|路线图|里程碑|需求|体验|product|feature|roadmap|solution/i,
33
+ ],
34
+ "corporate-report": [
35
+ /年度|季度|业绩|工作总结|经营|指标|部门|计划|复盘|annual|quarter|performance|department/i,
36
+ ],
37
+ "data-report": [
38
+ /数据|指标|统计|趋势|同比|环比|样本|调查|分析结果|data|metric|trend|survey|analysis/i,
39
+ ],
40
+ "policy-public-affairs": [
41
+ /政策|治理|公共|监管|制度|改革|社会|政府|policy|governance|regulation|public/i,
42
+ ],
43
+ "visual-portfolio": [
44
+ /作品集|摄影|案例展示|品牌|视觉|设计|portfolio|gallery|photography|brand/i,
45
+ ],
46
+ };
47
+
48
+ const USE_CASE_REQUIRED_CAPABILITIES = {
49
+ "academic-defense": ["cover", "roadmap", "analysis", "evidence", "conclusion"],
50
+ "course-presentation": ["cover", "roadmap", "analysis", "conclusion"],
51
+ "legal-analysis": ["cover", "issue", "analysis", "counterargument", "conclusion"],
52
+ "teaching-training": ["cover", "roadmap", "process", "summary"],
53
+ "business-pitch": ["cover", "problem", "solution", "evidence", "closing"],
54
+ "product-proposal": ["cover", "problem", "process", "comparison", "closing"],
55
+ "corporate-report": ["cover", "roadmap", "evidence", "summary"],
56
+ "data-report": ["cover", "evidence", "comparison", "summary"],
57
+ "policy-public-affairs": ["cover", "background", "analysis", "recommendation"],
58
+ "visual-portfolio": ["cover", "image-evidence", "closing"],
59
+ "general-report": ["cover", "roadmap", "analysis", "summary"],
60
+ };
61
+
62
+ const FAMILY_FUNCTIONS = {
63
+ cover: ["opening", "identify-topic"],
64
+ agenda: ["roadmap", "preview-structure"],
65
+ divider: ["section", "signal-transition"],
66
+ timeline: ["chronology", "show-change"],
67
+ process: ["explain-process", "show-sequence"],
68
+ comparison: ["compare", "evaluate-options"],
69
+ table: ["organize-evidence", "compare"],
70
+ chart: ["quantify", "show-trend"],
71
+ "image-led": ["image-evidence", "case-example"],
72
+ image: ["image-evidence", "case-example"],
73
+ metric: ["quantify", "highlight-metric"],
74
+ "three-column": ["parallel-reasons", "compare-categories"],
75
+ "four-column": ["parallel-reasons", "compare-categories"],
76
+ "multi-column": ["parallel-reasons", "compare-categories"],
77
+ "two-column": ["compare", "claim-and-evidence"],
78
+ matrix: ["compare", "evaluate-options"],
79
+ statement: ["claim", "highlight-conclusion"],
80
+ quote: ["quote", "support-claim"],
81
+ "case-study": ["case-example", "claim-and-evidence"],
82
+ team: ["introduce-people", "establish-credibility"],
83
+ reference: ["reference", "source-ledger"],
84
+ references: ["reference", "source-ledger"],
85
+ closing: ["closing", "call-to-action"],
86
+ summary: ["summary", "synthesize"],
87
+ "text-content": ["analysis", "explain"],
88
+ };
89
+
90
+ const FUNCTION_CAPABILITY_ALIASES = {
91
+ opening: "cover",
92
+ roadmap: "roadmap",
93
+ chronology: "timeline",
94
+ "show-change": "timeline",
95
+ "explain-process": "process",
96
+ compare: "comparison",
97
+ "evaluate-options": "comparison",
98
+ "organize-evidence": "evidence",
99
+ quantify: "evidence",
100
+ "show-trend": "evidence",
101
+ "image-evidence": "image-evidence",
102
+ "highlight-metric": "evidence",
103
+ "parallel-reasons": "analysis",
104
+ "claim-and-evidence": "analysis",
105
+ analysis: "analysis",
106
+ claim: "analysis",
107
+ "highlight-conclusion": "summary",
108
+ explain: "analysis",
109
+ "support-claim": "evidence",
110
+ "case-example": "evidence",
111
+ "introduce-people": "team",
112
+ "establish-credibility": "team",
113
+ summary: "summary",
114
+ synthesize: "summary",
115
+ closing: "closing",
116
+ reference: "reference",
117
+ };
118
+
119
+ function stableObject(value) {
120
+ if (Array.isArray(value)) return value.map(stableObject);
121
+ if (value && typeof value === "object") {
122
+ return Object.fromEntries(
123
+ Object.keys(value)
124
+ .sort()
125
+ .map((key) => [key, stableObject(value[key])]),
126
+ );
127
+ }
128
+ return value;
129
+ }
130
+
131
+ function fingerprint(value) {
132
+ return crypto
133
+ .createHash("sha256")
134
+ .update(JSON.stringify(stableObject(value)))
135
+ .digest("hex");
136
+ }
137
+
138
+ function normalized(value) {
139
+ return String(value || "").trim().toLowerCase();
140
+ }
141
+
142
+ function recipeText(recipe) {
143
+ return (recipe.slots || [])
144
+ .map((slot) => String(slot.originalText || ""))
145
+ .filter(Boolean)
146
+ .join(" ");
147
+ }
148
+
149
+ function effectiveRecipe(recipe, correction = {}) {
150
+ const next = structuredClone(recipe);
151
+ for (const key of [
152
+ "layoutFamily",
153
+ "layoutVariant",
154
+ "narrativeRole",
155
+ "confidence",
156
+ "autoUseStatus",
157
+ "approvedForAutoUse",
158
+ "excludedFromAutoUse",
159
+ "exclusionReason",
160
+ ]) {
161
+ if (correction[key] !== undefined) next[key] = correction[key];
162
+ }
163
+ if (correction.slots) {
164
+ next.slots = (next.slots || []).map((slot) => ({
165
+ ...slot,
166
+ ...(correction.slots[slot.slotId] || {}),
167
+ }));
168
+ }
169
+ if (Array.isArray(correction.protectedElementIds)) {
170
+ next.protectedElementIds = correction.protectedElementIds;
171
+ }
172
+ return next;
173
+ }
174
+
175
+ function slotStats(recipe) {
176
+ const slots = recipe.slots || [];
177
+ const byType = {};
178
+ const byPurpose = {};
179
+ const bySemantic = {};
180
+ for (const slot of slots) {
181
+ const type = normalized(slot.type || "unknown");
182
+ const purpose = normalized(slot.purpose || "unknown");
183
+ const semantic = normalized(slot.semanticType || slot.type || "unknown");
184
+ byType[type] = Number(byType[type] || 0) + 1;
185
+ byPurpose[purpose] = Number(byPurpose[purpose] || 0) + 1;
186
+ bySemantic[semantic] = Number(bySemantic[semantic] || 0) + 1;
187
+ }
188
+ return { total: slots.length, byType, byPurpose, bySemantic };
189
+ }
190
+
191
+ function communicationFunctions(recipe) {
192
+ const family = normalized(recipe.layoutFamily);
193
+ const role = normalized(recipe.narrativeRole);
194
+ const functions = new Set(FAMILY_FUNCTIONS[family] || []);
195
+ if (role) functions.add(role);
196
+ if (/时间|日期|阶段|年|月|timeline|milestone/i.test(recipeText(recipe))) {
197
+ functions.add("chronology");
198
+ }
199
+ if (/对比|比较|优势|不足|vs\.?|compare/i.test(recipeText(recipe))) {
200
+ functions.add("compare");
201
+ }
202
+ if (/结论|总结|建议|conclusion|summary/i.test(recipeText(recipe))) {
203
+ functions.add("summary");
204
+ }
205
+ return [...functions].sort();
206
+ }
207
+
208
+ function inferContentRecipe(recipe, stats, functions) {
209
+ const bodySlots = Number(stats.byPurpose.body || 0);
210
+ const labelSlots = Number(stats.byPurpose.label || 0);
211
+ const imageSlots = Number(stats.byType.image || 0);
212
+ const tableSlots = Number(stats.byType.table || 0);
213
+ const chartSlots = Number(stats.byType.chart || 0);
214
+ const itemSlots = Math.max(bodySlots, labelSlots);
215
+ const parts = ["title"];
216
+ if (itemSlots) parts.push(`${itemSlots} parallel text item${itemSlots > 1 ? "s" : ""}`);
217
+ if (imageSlots) parts.push(`${imageSlots} image${imageSlots > 1 ? "s" : ""}`);
218
+ if (tableSlots) parts.push(`${tableSlots} fixed table${tableSlots > 1 ? "s" : ""}`);
219
+ if (chartSlots) parts.push(`${chartSlots} native chart${chartSlots > 1 ? "s" : ""}`);
220
+ return {
221
+ pattern: parts.join(" + "),
222
+ functions,
223
+ recommendedSourceUnits: {
224
+ min: Math.max(1, itemSlots || 1),
225
+ max: Math.max(1, Math.min(7, itemSlots ? itemSlots * 2 : 3)),
226
+ },
227
+ onePrimaryClaim: !["cover", "agenda", "divider", "closing"].includes(
228
+ normalized(recipe.layoutFamily),
229
+ ),
230
+ };
231
+ }
232
+
233
+ function inferTemplateKind(recipes, manifest) {
234
+ const totalSlots = recipes.reduce((sum, recipe) => sum + (recipe.slots || []).length, 0);
235
+ const sampleSlots = recipes.reduce(
236
+ (sum, recipe) =>
237
+ sum +
238
+ (recipe.slots || []).filter(
239
+ (slot) => normalized(slot.semanticType) === "sample-content",
240
+ ).length,
241
+ 0,
242
+ );
243
+ const instructionSlots = recipes.reduce(
244
+ (sum, recipe) =>
245
+ sum +
246
+ (recipe.slots || []).filter((slot) =>
247
+ /请输入|请在此|点击添加|插入图片|replace|type here|add title/i.test(
248
+ String(slot.originalText || ""),
249
+ ),
250
+ ).length,
251
+ 0,
252
+ );
253
+ const mediaCount = Number(manifest?.packageParts?.mediaCount || 0);
254
+ const slideCount = Math.max(1, recipes.length);
255
+ const editableSlideRatio =
256
+ recipes.filter((recipe) => (recipe.slots || []).length > 0).length / slideCount;
257
+ const familyCount = new Set(recipes.map((recipe) => recipe.layoutFamily)).size;
258
+ const galleryRatio = familyCount / slideCount;
259
+ const sampleRatio = totalSlots ? sampleSlots / totalSlots : 0;
260
+ const instructionRatio = totalSlots ? instructionSlots / totalSlots : 0;
261
+
262
+ if (editableSlideRatio < 0.2 && mediaCount >= slideCount * 0.8) {
263
+ return { kind: "flattened-deck", confidence: 0.92 };
264
+ }
265
+ if (galleryRatio >= 0.4 && slideCount >= 10) {
266
+ return { kind: "layout-gallery", confidence: 0.84 };
267
+ }
268
+ if (sampleRatio >= 0.12 && instructionRatio >= 0.08) {
269
+ return { kind: "hybrid-template", confidence: 0.82 };
270
+ }
271
+ if (instructionRatio >= 0.12) {
272
+ return { kind: "blank-template", confidence: 0.8 };
273
+ }
274
+ if (sampleRatio >= 0.12) {
275
+ return { kind: "filled-example", confidence: 0.78 };
276
+ }
277
+ return { kind: "narrative-deck", confidence: 0.68 };
278
+ }
279
+
280
+ function inferPurposeProfile(recipes) {
281
+ const text = recipes.map(recipeText).join(" ");
282
+ const rawScores = {};
283
+ for (const [useCase, patterns] of Object.entries(USE_CASE_SIGNALS)) {
284
+ rawScores[useCase] = patterns.reduce((sum, pattern) => {
285
+ const matches = text.match(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`));
286
+ return sum + (matches?.length || 0);
287
+ }, 0);
288
+ }
289
+
290
+ const families = new Set(recipes.map((recipe) => normalized(recipe.layoutFamily)));
291
+ if (families.has("reference") || families.has("references")) {
292
+ rawScores["academic-defense"] = Number(rawScores["academic-defense"] || 0) + 2;
293
+ rawScores["legal-analysis"] = Number(rawScores["legal-analysis"] || 0) + 1;
294
+ }
295
+ if (families.has("chart") || families.has("table")) {
296
+ rawScores["data-report"] = Number(rawScores["data-report"] || 0) + 2;
297
+ rawScores["corporate-report"] = Number(rawScores["corporate-report"] || 0) + 1;
298
+ }
299
+ if (families.has("image-led")) {
300
+ rawScores["visual-portfolio"] = Number(rawScores["visual-portfolio"] || 0) + 2;
301
+ }
302
+ const ranked = Object.entries(rawScores)
303
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
304
+ const maximum = ranked[0]?.[1] || 0;
305
+ const primary = maximum
306
+ ? ranked.filter(([, score]) => score === maximum).slice(0, 2).map(([name]) => name)
307
+ : ["general-report"];
308
+ const secondary = ranked
309
+ .filter(([, score]) => score > 0 && score < maximum)
310
+ .slice(0, 3)
311
+ .map(([name]) => name);
312
+ const confidence = maximum
313
+ ? Math.min(0.95, 0.55 + maximum / Math.max(10, recipes.length * 0.8))
314
+ : 0.45;
315
+ return {
316
+ primaryUseCases: primary,
317
+ secondaryUseCases: secondary,
318
+ incompatibleUseCases: [],
319
+ evidenceScores: rawScores,
320
+ confidence: Number(confidence.toFixed(3)),
321
+ };
322
+ }
323
+
324
+ function inferDensity(recipes) {
325
+ const bodyChars = recipes.map((recipe) => Number(recipe.capacity?.bodyChars || 0));
326
+ const averageBody =
327
+ bodyChars.reduce((sum, value) => sum + value, 0) / Math.max(1, bodyChars.length);
328
+ if (averageBody >= 260) return "high";
329
+ if (averageBody >= 110) return "medium";
330
+ return "low";
331
+ }
332
+
333
+ function inferVisualBalance(recipes) {
334
+ const totals = recipes.reduce(
335
+ (accumulator, recipe) => {
336
+ for (const slot of recipe.slots || []) {
337
+ if (slot.type === "text") accumulator.text += 1;
338
+ else if (["image", "chart", "table"].includes(slot.type)) accumulator.visual += 1;
339
+ }
340
+ return accumulator;
341
+ },
342
+ { text: 0, visual: 0 },
343
+ );
344
+ const ratio = totals.visual / Math.max(1, totals.text + totals.visual);
345
+ if (ratio >= 0.45) return "visual-led";
346
+ if (ratio <= 0.15) return "text-led";
347
+ return "mixed";
348
+ }
349
+
350
+ function capabilityInventory(slides) {
351
+ const capabilities = {};
352
+ for (const slide of slides) {
353
+ const tokens = new Set([
354
+ slide.layoutFamily,
355
+ ...slide.communicationFunctions.map(
356
+ (item) => FUNCTION_CAPABILITY_ALIASES[item] || item,
357
+ ),
358
+ ]);
359
+ for (const token of tokens) {
360
+ if (!token) continue;
361
+ capabilities[token] ||= {
362
+ count: 0,
363
+ safeCount: 0,
364
+ sourceSlides: [],
365
+ };
366
+ capabilities[token].count += 1;
367
+ if (slide.safetyStatus === "automatic") capabilities[token].safeCount += 1;
368
+ capabilities[token].sourceSlides.push(slide.sourceSlide);
369
+ }
370
+ }
371
+ return capabilities;
372
+ }
373
+
374
+ function applySemanticReview(profile, review) {
375
+ if (!review || typeof review !== "object") return profile;
376
+ const next = structuredClone(profile);
377
+ const purpose = review.purposeProfile;
378
+ if (purpose && typeof purpose === "object") {
379
+ for (const key of [
380
+ "primaryUseCases",
381
+ "secondaryUseCases",
382
+ "incompatibleUseCases",
383
+ "confidence",
384
+ ]) {
385
+ if (purpose[key] !== undefined) next.purposeProfile[key] = purpose[key];
386
+ }
387
+ }
388
+ const overrides = review.slides || {};
389
+ next.slides = next.slides.map((slide) => {
390
+ const override = overrides[String(slide.sourceSlide)];
391
+ if (!override) return slide;
392
+ const nextSlide = {
393
+ ...slide,
394
+ ...Object.fromEntries(
395
+ Object.entries(override).filter(([key]) =>
396
+ [
397
+ "layoutFamily",
398
+ "layoutVariant",
399
+ "communicationFunctions",
400
+ "narrativeRoles",
401
+ "contentRecipe",
402
+ "confidence",
403
+ "reviewNotes",
404
+ ].includes(key),
405
+ ),
406
+ ),
407
+ };
408
+ if (
409
+ override.safetyStatus &&
410
+ (
411
+ slide.safetyStatus === "automatic" ||
412
+ (slide.safetyStatus === "review" && override.safetyStatus === "blocked")
413
+ ) &&
414
+ ["review", "blocked"].includes(override.safetyStatus)
415
+ ) {
416
+ nextSlide.safetyStatus = override.safetyStatus;
417
+ }
418
+ return nextSlide;
419
+ });
420
+ next.capabilities = capabilityInventory(next.slides);
421
+ next.reviewSummary = {
422
+ automaticSlides: next.slides
423
+ .filter((slide) => slide.safetyStatus === "automatic")
424
+ .map((slide) => slide.sourceSlide),
425
+ reviewSlides: next.slides
426
+ .filter((slide) => slide.safetyStatus === "review")
427
+ .map((slide) => slide.sourceSlide),
428
+ blockedSlides: next.slides
429
+ .filter((slide) => slide.safetyStatus === "blocked")
430
+ .map((slide) => slide.sourceSlide),
431
+ };
432
+ next.semanticReview = {
433
+ applied: true,
434
+ reviewer: String(review.reviewer || "host-ai"),
435
+ reviewedAt: String(review.reviewedAt || ""),
436
+ reviewFingerprint: fingerprint(review),
437
+ };
438
+ return next;
439
+ }
440
+
441
+ export function assessTemplateFit(profile, brief = {}) {
442
+ const useCase = normalized(brief.useCase || "general-report");
443
+ const primary = new Set(profile.purposeProfile?.primaryUseCases || []);
444
+ const secondary = new Set(profile.purposeProfile?.secondaryUseCases || []);
445
+ const incompatible = new Set(profile.purposeProfile?.incompatibleUseCases || []);
446
+ const required = Array.isArray(brief.requiredCapabilities)
447
+ ? brief.requiredCapabilities.map(normalized)
448
+ : USE_CASE_REQUIRED_CAPABILITIES[useCase] || USE_CASE_REQUIRED_CAPABILITIES["general-report"];
449
+ const supported = required.filter(
450
+ (item) => Number(profile.capabilities?.[item]?.safeCount || 0) > 0,
451
+ );
452
+ const purposeScore = incompatible.has(useCase)
453
+ ? 0
454
+ : primary.has(useCase)
455
+ ? 1
456
+ : secondary.has(useCase)
457
+ ? 0.72
458
+ : useCase === "general-report"
459
+ ? 0.7
460
+ : 0.45;
461
+ const capabilityScore = supported.length / Math.max(1, required.length);
462
+ const requestedDensity = normalized(brief.density);
463
+ const densityScore =
464
+ !requestedDensity || requestedDensity === profile.densityRange ? 1 : 0.65;
465
+ const requestedVisual = normalized(brief.visualMode);
466
+ const visualScore =
467
+ !requestedVisual || requestedVisual === profile.visualBalance ? 1 : 0.72;
468
+ const editabilityScore = profile.templateKind === "flattened-deck" ? 0.35 : 1;
469
+ const score =
470
+ purposeScore * 0.4 +
471
+ capabilityScore * 0.25 +
472
+ densityScore * 0.15 +
473
+ visualScore * 0.1 +
474
+ editabilityScore * 0.1;
475
+ return {
476
+ useCase,
477
+ score: Number(score.toFixed(3)),
478
+ purposeScore,
479
+ capabilityScore: Number(capabilityScore.toFixed(3)),
480
+ requiredCapabilities: required,
481
+ supportedCapabilities: supported,
482
+ missingCapabilities: required.filter((item) => !supported.includes(item)),
483
+ recommendedMode:
484
+ score >= 0.75
485
+ ? "inherit-narrative"
486
+ : score >= 0.5
487
+ ? "style-only"
488
+ : "recommend-other-template",
489
+ };
490
+ }
491
+
492
+ export function buildTemplateIntelligence({
493
+ manifest,
494
+ recipeDoc,
495
+ deckGrammar = {},
496
+ corrections = {},
497
+ semanticReview = null,
498
+ }) {
499
+ const recipes = (recipeDoc.recipes || []).map((recipe) =>
500
+ effectiveRecipe(
501
+ recipe,
502
+ corrections.slides?.[String(recipe.sourceSlide)] || {},
503
+ ),
504
+ );
505
+ const kind = inferTemplateKind(recipes, manifest);
506
+ const slides = recipes.map((recipe) => {
507
+ const stats = slotStats(recipe);
508
+ const functions = communicationFunctions(recipe);
509
+ const blocked =
510
+ recipe.excludedFromAutoUse === true ||
511
+ (recipe.hardBlockReasons || []).length > 0;
512
+ const automatic =
513
+ !blocked &&
514
+ (recipe.approvedForAutoUse === true ||
515
+ (recipe.autoUseStatus === "ready" && Number(recipe.confidence) >= 0.65));
516
+ return {
517
+ sourceSlide: Number(recipe.sourceSlide),
518
+ layoutFamily: normalized(recipe.layoutFamily || "unknown"),
519
+ layoutVariant: recipe.layoutVariant || null,
520
+ narrativeRoles: [...new Set([normalized(recipe.narrativeRole), ...functions])]
521
+ .filter(Boolean)
522
+ .sort(),
523
+ communicationFunctions: functions,
524
+ density: recipe.density || null,
525
+ confidence: Number(recipe.confidence || 0),
526
+ safetyStatus: blocked ? "blocked" : automatic ? "automatic" : "review",
527
+ autoUseStatus: recipe.autoUseStatus || "review",
528
+ approvedForAutoUse: recipe.approvedForAutoUse === true,
529
+ hardBlockReasons: recipe.hardBlockReasons || [],
530
+ exclusionReason: recipe.exclusionReason || null,
531
+ capacity: {
532
+ ...(recipe.capacity || {}),
533
+ textSlotCount: Number(stats.byType.text || 0),
534
+ imageSlotCount: Number(stats.byType.image || 0),
535
+ tableSlotCount: Number(stats.byType.table || 0),
536
+ chartSlotCount: Number(stats.byType.chart || 0),
537
+ },
538
+ contentRecipe: inferContentRecipe(recipe, stats, functions),
539
+ slotSchema: (recipe.slots || []).map((slot) => ({
540
+ slotId: slot.slotId,
541
+ type: slot.type,
542
+ semanticType: slot.semanticType || slot.type,
543
+ purpose: slot.purpose,
544
+ required: slot.required === true,
545
+ maxChars: slot.maxChars,
546
+ maxTextUnits: slot.maxTextUnits,
547
+ maxLines: slot.maxLines,
548
+ rows: slot.rows,
549
+ columns: slot.columns,
550
+ operation: slot.operation || slot.action,
551
+ })),
552
+ assetRequirements: (recipe.visualRequirements || []).map((item) => ({
553
+ requirementId: item.requirementId,
554
+ type: item.type,
555
+ required: item.required === true,
556
+ status: item.status,
557
+ slotId: item.slotId,
558
+ })),
559
+ protectedElementCount: (recipe.protectedElementIds || []).length,
560
+ recipeFingerprint: recipe.recipeFingerprint,
561
+ previewSha256: recipe.previewSha256,
562
+ };
563
+ });
564
+ let profile = {
565
+ schemaVersion: 2,
566
+ engine: "ppt-template-reuse-template-intelligence",
567
+ templateSource: manifest.source,
568
+ templateKind: kind.kind,
569
+ templateKindConfidence: kind.confidence,
570
+ purposeProfile: inferPurposeProfile(recipes),
571
+ densityRange: inferDensity(recipes),
572
+ visualBalance: inferVisualBalance(recipes),
573
+ editability:
574
+ kind.kind === "flattened-deck" ? "selective-editability" : "native-editable",
575
+ narrativeGrammar: {
576
+ sourceOrderIsEvidence: kind.kind !== "layout-gallery",
577
+ sourceSequence: deckGrammar.sourceSequence || [],
578
+ positionProfiles: deckGrammar.positionProfiles || {},
579
+ observedTransitions: deckGrammar.observedTransitions || {},
580
+ },
581
+ slides,
582
+ capabilities: capabilityInventory(slides),
583
+ reviewSummary: {
584
+ automaticSlides: slides.filter((slide) => slide.safetyStatus === "automatic")
585
+ .map((slide) => slide.sourceSlide),
586
+ reviewSlides: slides.filter((slide) => slide.safetyStatus === "review")
587
+ .map((slide) => slide.sourceSlide),
588
+ blockedSlides: slides.filter((slide) => slide.safetyStatus === "blocked")
589
+ .map((slide) => slide.sourceSlide),
590
+ },
591
+ semanticReview: {
592
+ applied: false,
593
+ },
594
+ };
595
+ profile = applySemanticReview(profile, semanticReview);
596
+ profile.profileFingerprint = fingerprint({
597
+ ...profile,
598
+ profileFingerprint: undefined,
599
+ });
600
+ return profile;
601
+ }
602
+
603
+ function usage() {
604
+ return [
605
+ "Usage:",
606
+ " node template-intelligence.mjs --capsule <capsule-dir> [--out <template-profile.json>]",
607
+ " [--semantic-review <review.json>] [--brief <presentation-brief.json>]",
608
+ ].join("\n");
609
+ }
610
+
611
+ async function main() {
612
+ const args = parseArgs(process.argv.slice(2));
613
+ if (args.help) {
614
+ console.log(usage());
615
+ return;
616
+ }
617
+ const capsuleDir = path.resolve(requireArg(args, "capsule"));
618
+ const manifest = await readJson(path.join(capsuleDir, "manifest.json"));
619
+ const recipeDoc = await readJson(path.join(capsuleDir, "slide-recipes.json"));
620
+ const correctionsPath = path.join(capsuleDir, "user-corrections.json");
621
+ const grammarPath = path.join(capsuleDir, "deck-grammar.json");
622
+ const corrections = (await exists(correctionsPath))
623
+ ? await readJson(correctionsPath)
624
+ : { slides: {} };
625
+ const deckGrammar = (await exists(grammarPath))
626
+ ? await readJson(grammarPath)
627
+ : {};
628
+ const semanticReview = args["semantic-review"]
629
+ ? await readJson(path.resolve(args["semantic-review"]))
630
+ : null;
631
+ const profile = buildTemplateIntelligence({
632
+ manifest,
633
+ recipeDoc,
634
+ deckGrammar,
635
+ corrections,
636
+ semanticReview,
637
+ });
638
+ if (args.brief) {
639
+ profile.fit = assessTemplateFit(
640
+ profile,
641
+ await readJson(path.resolve(args.brief)),
642
+ );
643
+ }
644
+ const outPath = path.resolve(
645
+ String(args.out || path.join(capsuleDir, "template-profile-v2.json")),
646
+ );
647
+ await writeJson(outPath, profile);
648
+ console.log(
649
+ JSON.stringify(
650
+ {
651
+ out: outPath,
652
+ templateKind: profile.templateKind,
653
+ primaryUseCases: profile.purposeProfile.primaryUseCases,
654
+ automaticSlides: profile.reviewSummary.automaticSlides.length,
655
+ reviewSlides: profile.reviewSummary.reviewSlides.length,
656
+ blockedSlides: profile.reviewSummary.blockedSlides.length,
657
+ fit: profile.fit || null,
658
+ profileFingerprint: profile.profileFingerprint,
659
+ },
660
+ null,
661
+ 2,
662
+ ),
663
+ );
664
+ }
665
+
666
+ const isMain =
667
+ process.argv[1] &&
668
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
669
+ if (isMain) {
670
+ main().catch((error) => {
671
+ console.error(error.stack || error.message || String(error));
672
+ console.error(usage());
673
+ process.exit(1);
674
+ });
675
+ }