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.
- package/LICENSE +21 -0
- package/NOTICE.md +8 -0
- package/README.md +233 -0
- package/adapters/README.md +13 -0
- package/adapters/claude/.claude-plugin/plugin.json +16 -0
- package/adapters/claude/.mcp.json +8 -0
- package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
- package/adapters/kimi/kimi.plugin.json +12 -0
- package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
- package/adapters/kimi-work/mcp.json +8 -0
- package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
- package/adapters/workbuddy/mcp.json +8 -0
- package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
- package/package.json +53 -0
- package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
- package/plugins/ppt-template-reuse/.mcp.json +9 -0
- package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
- package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
- package/skills/ppt-template-reuse/SKILL.md +105 -0
- package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
- package/src/cli/ppt-reuse.mjs +203 -0
- package/src/core/adaptive-planner.mjs +1192 -0
- package/src/core/content-audit.mjs +573 -0
- package/src/core/frame-map.mjs +91 -0
- package/src/core/index.mjs +38 -0
- package/src/core/io.mjs +81 -0
- package/src/core/legacy-capsule-import.mjs +382 -0
- package/src/core/ooxml-editor.mjs +753 -0
- package/src/core/paths.mjs +60 -0
- package/src/core/planner-lib.mjs +59 -0
- package/src/core/pptx-package.mjs +227 -0
- package/src/core/renderer.mjs +402 -0
- package/src/core/source-extractor.mjs +412 -0
- package/src/core/template-inspector.mjs +489 -0
- package/src/core/template-intelligence.mjs +675 -0
- package/src/core/universal-engine.mjs +1075 -0
- package/src/index.mjs +2 -0
- package/src/install/installer.mjs +293 -0
- 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
|
+
}
|