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,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";
|