claude-presentation-master 5.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -26,8 +26,280 @@ var TemplateNotFoundError = class extends Error {
26
26
  }
27
27
  };
28
28
 
29
+ // src/kb/KnowledgeGateway.ts
30
+ import { readFileSync } from "fs";
31
+ import { join, dirname } from "path";
32
+ import { fileURLToPath } from "url";
33
+ import * as yaml from "yaml";
34
+ function getModuleDir() {
35
+ if (typeof import.meta !== "undefined" && import.meta.url) {
36
+ return dirname(fileURLToPath(import.meta.url));
37
+ }
38
+ if (typeof __dirname !== "undefined") {
39
+ return __dirname;
40
+ }
41
+ return process.cwd();
42
+ }
43
+ var KnowledgeGateway = class {
44
+ kb;
45
+ loaded = false;
46
+ constructor() {
47
+ }
48
+ /**
49
+ * Load the knowledge base from YAML file
50
+ */
51
+ async load() {
52
+ if (this.loaded) return;
53
+ const moduleDir = getModuleDir();
54
+ const possiblePaths = [
55
+ join(moduleDir, "../../assets/presentation-knowledge.yaml"),
56
+ join(moduleDir, "../assets/presentation-knowledge.yaml"),
57
+ join(moduleDir, "../../../assets/presentation-knowledge.yaml"),
58
+ join(moduleDir, "assets/presentation-knowledge.yaml"),
59
+ join(process.cwd(), "assets/presentation-knowledge.yaml"),
60
+ join(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
61
+ ];
62
+ for (const path of possiblePaths) {
63
+ try {
64
+ const content = readFileSync(path, "utf-8");
65
+ this.kb = yaml.parse(content);
66
+ this.loaded = true;
67
+ console.log(` \u2713 Knowledge base loaded from ${path}`);
68
+ return;
69
+ } catch {
70
+ }
71
+ }
72
+ throw new Error(`Knowledge base not found. Tried: ${possiblePaths.join(", ")}`);
73
+ }
74
+ /**
75
+ * Get presentation mode configuration (keynote or business)
76
+ */
77
+ getMode(mode) {
78
+ this.ensureLoaded();
79
+ return this.kb.presentation_modes[mode];
80
+ }
81
+ /**
82
+ * Determine which mode is best for a given presentation type
83
+ */
84
+ getModeForType(type) {
85
+ const keynoteTypes = ["ted_keynote", "sales_pitch", "all_hands"];
86
+ const businessTypes = ["consulting_deck", "investment_banking", "investor_pitch", "technical_presentation"];
87
+ if (keynoteTypes.includes(type)) return "keynote";
88
+ if (businessTypes.includes(type)) return "business";
89
+ return "keynote";
90
+ }
91
+ /**
92
+ * Get word limits for the specified mode
93
+ */
94
+ getWordLimits(mode) {
95
+ const modeConfig = this.getMode(mode);
96
+ const range = modeConfig.characteristics.words_per_slide;
97
+ if (mode === "keynote") {
98
+ return { min: 6, max: 15, ideal: 10 };
99
+ } else {
100
+ return { min: 40, max: 80, ideal: 60 };
101
+ }
102
+ }
103
+ /**
104
+ * Get bullet point limits
105
+ */
106
+ getBulletLimits(mode) {
107
+ if (mode === "keynote") {
108
+ return { maxBullets: 3, maxWordsPerBullet: 8 };
109
+ } else {
110
+ return { maxBullets: 5, maxWordsPerBullet: 20 };
111
+ }
112
+ }
113
+ /**
114
+ * Get whitespace percentage requirement
115
+ */
116
+ getWhitespaceRequirement(mode) {
117
+ if (mode === "keynote") return 0.4;
118
+ return 0.25;
119
+ }
120
+ /**
121
+ * Get slide templates for the specified mode
122
+ */
123
+ getSlideTemplates(mode) {
124
+ this.ensureLoaded();
125
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
126
+ return this.kb.slide_templates[key] || [];
127
+ }
128
+ /**
129
+ * Get a specific slide template by name
130
+ */
131
+ getSlideTemplate(mode, templateName) {
132
+ const templates = this.getSlideTemplates(mode);
133
+ return templates.find((t) => t.name.toLowerCase().includes(templateName.toLowerCase()));
134
+ }
135
+ /**
136
+ * Get quality metrics for grading
137
+ */
138
+ getQualityMetrics() {
139
+ this.ensureLoaded();
140
+ return this.kb.quality_metrics;
141
+ }
142
+ /**
143
+ * Get anti-patterns to avoid
144
+ */
145
+ getAntiPatterns(mode) {
146
+ this.ensureLoaded();
147
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
148
+ return this.kb.anti_patterns[key] || [];
149
+ }
150
+ /**
151
+ * Get data visualization anti-patterns
152
+ */
153
+ getDataVizAntiPatterns() {
154
+ this.ensureLoaded();
155
+ return this.kb.anti_patterns.data_visualization || [];
156
+ }
157
+ /**
158
+ * Get accessibility anti-patterns
159
+ */
160
+ getAccessibilityAntiPatterns() {
161
+ this.ensureLoaded();
162
+ return this.kb.anti_patterns.accessibility || [];
163
+ }
164
+ /**
165
+ * Get color palette by name
166
+ */
167
+ getColorPalette(name) {
168
+ this.ensureLoaded();
169
+ const palettes = this.kb.design_system.color_palettes;
170
+ return palettes[name];
171
+ }
172
+ /**
173
+ * Get recommended color palette for presentation type
174
+ */
175
+ getRecommendedPalette(type) {
176
+ this.ensureLoaded();
177
+ const palettes = this.kb.design_system.color_palettes;
178
+ switch (type) {
179
+ case "consulting_deck":
180
+ return palettes.consulting_classic;
181
+ case "investment_banking":
182
+ case "investor_pitch":
183
+ return palettes.executive_professional;
184
+ case "ted_keynote":
185
+ return palettes.dark_executive;
186
+ case "sales_pitch":
187
+ return palettes.modern_business;
188
+ case "technical_presentation":
189
+ return palettes.modern_business;
190
+ case "all_hands":
191
+ return palettes.strategy_growth;
192
+ default:
193
+ return palettes.consulting_classic;
194
+ }
195
+ }
196
+ /**
197
+ * Get typography guidelines for mode
198
+ */
199
+ getTypography(mode) {
200
+ this.ensureLoaded();
201
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
202
+ return this.kb.design_system.typography[key];
203
+ }
204
+ /**
205
+ * Get story framework by name
206
+ */
207
+ getStoryFramework(name) {
208
+ this.ensureLoaded();
209
+ return this.kb.story_frameworks[name];
210
+ }
211
+ /**
212
+ * Get expert principles by name
213
+ */
214
+ getExpertPrinciples(expertName) {
215
+ this.ensureLoaded();
216
+ if (this.kb.experts[expertName]) {
217
+ return this.kb.experts[expertName];
218
+ }
219
+ if (this.kb.data_visualization_experts) {
220
+ for (const key of Object.keys(this.kb.data_visualization_experts)) {
221
+ if (key.includes(expertName.toLowerCase())) {
222
+ return this.kb.data_visualization_experts[key];
223
+ }
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+ /**
229
+ * Get Duarte's glance test requirements
230
+ */
231
+ getDuarteGlanceTest() {
232
+ this.ensureLoaded();
233
+ const duarte = this.kb.experts.nancy_duarte;
234
+ return {
235
+ wordLimit: duarte.core_principles.glance_test.word_limit || 25,
236
+ timeSeconds: 3
237
+ };
238
+ }
239
+ /**
240
+ * Get Miller's Law constraints
241
+ */
242
+ getMillersLaw() {
243
+ this.ensureLoaded();
244
+ return { minItems: 5, maxItems: 9 };
245
+ }
246
+ /**
247
+ * Get cognitive load constraints
248
+ */
249
+ getCognitiveLoadLimits() {
250
+ this.ensureLoaded();
251
+ return { maxItemsPerChunk: 7 };
252
+ }
253
+ /**
254
+ * Get chart selection guidance
255
+ */
256
+ getChartGuidance(purpose) {
257
+ this.ensureLoaded();
258
+ return this.kb.chart_selection_guide.by_purpose[purpose];
259
+ }
260
+ /**
261
+ * Get charts to avoid
262
+ */
263
+ getChartsToAvoid() {
264
+ this.ensureLoaded();
265
+ const knaflic = this.kb.data_visualization_experts.cole_nussbaumer_knaflic;
266
+ return knaflic.chart_guidance.avoid_these || [];
267
+ }
268
+ /**
269
+ * Get investment banking pitch book structure
270
+ */
271
+ getIBPitchBookStructure(type) {
272
+ this.ensureLoaded();
273
+ return this.kb.investment_banking?.pitch_book_types[type];
274
+ }
275
+ /**
276
+ * Check if knowledge base is loaded
277
+ */
278
+ ensureLoaded() {
279
+ if (!this.loaded) {
280
+ throw new Error("Knowledge base not loaded. Call load() first.");
281
+ }
282
+ }
283
+ /**
284
+ * Get the full knowledge base (for advanced queries)
285
+ */
286
+ getRawKB() {
287
+ this.ensureLoaded();
288
+ return this.kb;
289
+ }
290
+ };
291
+ var gatewayInstance = null;
292
+ async function getKnowledgeGateway() {
293
+ if (!gatewayInstance) {
294
+ gatewayInstance = new KnowledgeGateway();
295
+ await gatewayInstance.load();
296
+ }
297
+ return gatewayInstance;
298
+ }
299
+
29
300
  // src/core/ContentAnalyzer.ts
30
301
  var ContentAnalyzer = class {
302
+ kb;
31
303
  // Signal words for SCQA detection
32
304
  situationSignals = [
33
305
  "currently",
@@ -39,7 +311,8 @@ var ContentAnalyzer = class {
39
311
  "our",
40
312
  "the market",
41
313
  "industry",
42
- "context"
314
+ "context",
315
+ "background"
43
316
  ];
44
317
  complicationSignals = [
45
318
  "however",
@@ -57,20 +330,10 @@ var ContentAnalyzer = class {
57
330
  "yet",
58
331
  "although",
59
332
  "despite",
60
- "while"
61
- ];
62
- questionSignals = [
63
- "how",
64
- "what",
65
- "why",
66
- "when",
67
- "where",
68
- "which",
69
- "should",
70
- "could",
71
- "can we",
72
- "is it possible",
73
- "?"
333
+ "while",
334
+ "crisis",
335
+ "gap",
336
+ "broken"
74
337
  ];
75
338
  answerSignals = [
76
339
  "therefore",
@@ -86,249 +349,389 @@ var ContentAnalyzer = class {
86
349
  "we should",
87
350
  "must",
88
351
  "need to",
89
- "the answer"
90
- ];
91
- // Sparkline detection
92
- whatIsSignals = [
93
- "currently",
94
- "today",
95
- "status quo",
96
- "reality",
97
- "actual",
98
- "now",
99
- "existing",
100
- "present state",
101
- "as-is",
102
- "problem"
352
+ "the answer",
353
+ "introducing",
354
+ "presenting",
355
+ "platform"
103
356
  ];
104
- whatCouldBeSignals = [
105
- "imagine",
106
- "vision",
107
- "future",
108
- "could be",
109
- "what if",
110
- "possibility",
111
- "potential",
112
- "opportunity",
113
- "transform",
114
- "envision",
115
- "ideal",
116
- "dream",
117
- "goal",
118
- "aspiration"
357
+ // CTA signals - used to filter these OUT of answer detection
358
+ ctaSignals = [
359
+ "contact",
360
+ "call us",
361
+ "email",
362
+ "schedule",
363
+ "sign up",
364
+ "register",
365
+ "next steps",
366
+ "get started",
367
+ "reach out",
368
+ "visit",
369
+ "follow"
119
370
  ];
371
+ // Type detection signals
372
+ typeSignals = {
373
+ ted_keynote: ["inspire", "vision", "imagine", "dream", "transform", "story", "journey"],
374
+ sales_pitch: ["buy", "purchase", "pricing", "offer", "deal", "discount", "roi", "value"],
375
+ consulting_deck: ["recommend", "analysis", "assessment", "strategy", "findings", "options", "mece"],
376
+ investment_banking: ["valuation", "dcf", "multiple", "enterprise value", "ebitda", "ipo", "m&a"],
377
+ investor_pitch: ["investment", "funding", "series", "traction", "market size", "tam", "runway"],
378
+ technical_presentation: ["architecture", "api", "system", "implementation", "code", "database", "deploy"],
379
+ all_hands: ["team", "quarter", "update", "progress", "celebrate", "recognize", "company"]
380
+ };
381
+ async initialize() {
382
+ this.kb = await getKnowledgeGateway();
383
+ }
120
384
  /**
121
385
  * Analyze content and extract structural elements.
122
386
  */
123
387
  async analyze(content, contentType) {
388
+ await this.initialize();
124
389
  const text = this.parseContent(content, contentType);
125
- const paragraphs = this.splitIntoParagraphs(text);
126
- const sentences = this.splitIntoSentences(text);
127
- const scqa = this.extractSCQA(paragraphs, sentences);
128
- const sparkline = this.extractSparkline(paragraphs);
129
- const keyMessages = this.extractKeyMessages(text, sentences);
130
- const titles = this.generateActionTitles(keyMessages, paragraphs);
131
- const starMoments = this.identifyStarMoments(paragraphs);
132
- const estimatedSlideCount = this.estimateSlideCount(text, paragraphs);
390
+ const title = this.extractTitle(text);
391
+ const detectedType = this.detectPresentationType(text);
392
+ console.log(` \u2713 Detected type: ${detectedType}`);
393
+ const sections = this.extractSections(text);
394
+ console.log(` \u2713 Found ${sections.length} sections`);
395
+ const scqa = this.extractSCQA(text);
396
+ const sparkline = this.extractSparkline(text);
397
+ const keyMessages = this.extractKeyMessages(text);
398
+ const dataPoints = this.extractDataPoints(text);
399
+ console.log(` \u2713 Found ${dataPoints.length} data points`);
400
+ const titles = sections.map((s) => s.header).filter((h) => h.length > 0);
401
+ const starMoments = this.extractStarMoments(text);
402
+ const estimatedSlideCount = Math.max(5, sections.length + 3);
133
403
  return {
134
- scqa,
135
- sparkline,
404
+ detectedType,
405
+ title,
406
+ sections,
136
407
  keyMessages,
408
+ dataPoints,
409
+ scqa: {
410
+ situation: scqa.situation || "",
411
+ complication: scqa.complication || "",
412
+ question: scqa.question || "",
413
+ answer: scqa.answer || ""
414
+ },
415
+ sparkline: {
416
+ whatIs: sparkline.callToAction ? [sparkline.callToAction] : [],
417
+ whatCouldBe: keyMessages,
418
+ callToAdventure: sparkline.callToAction || ""
419
+ },
137
420
  titles,
138
421
  starMoments,
139
422
  estimatedSlideCount
140
423
  };
141
424
  }
142
425
  /**
143
- * Parse content based on its type.
426
+ * Parse content based on type
144
427
  */
145
428
  parseContent(content, contentType) {
146
- switch (contentType) {
147
- case "markdown":
148
- return this.parseMarkdown(content);
149
- case "json":
150
- return this.parseJSON(content);
151
- case "yaml":
152
- return this.parseYAML(content);
153
- case "text":
154
- default:
155
- return content;
429
+ if (contentType === "markdown" || content.includes("#") || content.includes("- ")) {
430
+ return content;
156
431
  }
432
+ return content;
157
433
  }
158
434
  /**
159
- * Parse markdown content to plain text (preserving structure hints).
435
+ * Extract the main title from content
160
436
  */
161
- parseMarkdown(content) {
162
- let text = content;
163
- text = text.replace(/^#{1,6}\s+(.+)$/gm, "\n[HEADER] $1\n");
164
- text = text.replace(/^[-*+]\s+(.+)$/gm, "[BULLET] $1");
165
- text = text.replace(/^\d+\.\s+(.+)$/gm, "[NUMBERED] $1");
166
- text = text.replace(/\*\*(.+?)\*\*/g, "[EMPHASIS] $1 [/EMPHASIS]");
167
- text = text.replace(/\*(.+?)\*/g, "$1");
168
- text = text.replace(/```[\s\S]*?```/g, "[CODE BLOCK]");
169
- text = text.replace(/`(.+?)`/g, "$1");
170
- text = text.replace(/\[(.+?)\]\(.+?\)/g, "$1");
171
- text = text.replace(/!\[.*?\]\(.+?\)/g, "[IMAGE]");
172
- return text.trim();
173
- }
174
- /**
175
- * Parse JSON content.
176
- */
177
- parseJSON(content) {
178
- try {
179
- const data = JSON.parse(content);
180
- return this.flattenObject(data);
181
- } catch {
182
- return content;
437
+ extractTitle(text) {
438
+ const h1Match = text.match(/^#\s+(.+)$/m);
439
+ if (h1Match && h1Match[1]) {
440
+ return h1Match[1].replace(/\*\*/g, "").trim();
183
441
  }
442
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
443
+ if (lines.length > 0 && lines[0]) {
444
+ return lines[0].replace(/^#+\s*/, "").replace(/\*\*/g, "").trim().slice(0, 80);
445
+ }
446
+ return "Presentation";
184
447
  }
185
448
  /**
186
- * Parse YAML content.
449
+ * Detect presentation type from content signals
187
450
  */
188
- parseYAML(content) {
189
- const lines = content.split("\n");
190
- const values = [];
191
- for (const line of lines) {
192
- const match = line.match(/^[\s-]*(?:\w+:\s*)?(.+)$/);
193
- if (match?.[1] && !match[1].includes(":")) {
194
- values.push(match[1].trim());
451
+ detectPresentationType(text) {
452
+ const lowerText = text.toLowerCase();
453
+ const scores = {
454
+ ted_keynote: 0,
455
+ sales_pitch: 0,
456
+ consulting_deck: 0,
457
+ investment_banking: 0,
458
+ investor_pitch: 0,
459
+ technical_presentation: 0,
460
+ all_hands: 0
461
+ };
462
+ for (const [type, signals] of Object.entries(this.typeSignals)) {
463
+ for (const signal of signals) {
464
+ if (lowerText.includes(signal)) {
465
+ scores[type]++;
466
+ }
195
467
  }
196
468
  }
197
- return values.join("\n");
469
+ let maxScore = 0;
470
+ let detectedType = "consulting_deck";
471
+ for (const [type, score] of Object.entries(scores)) {
472
+ if (score > maxScore) {
473
+ maxScore = score;
474
+ detectedType = type;
475
+ }
476
+ }
477
+ return detectedType;
198
478
  }
199
479
  /**
200
- * Flatten object to text.
480
+ * Extract sections from content (headers, bullets, content)
201
481
  */
202
- flattenObject(obj, prefix = "") {
203
- const parts = [];
204
- if (typeof obj === "string") {
205
- return obj;
206
- }
207
- if (Array.isArray(obj)) {
208
- for (const item of obj) {
209
- parts.push(this.flattenObject(item, prefix));
482
+ extractSections(text) {
483
+ const sections = [];
484
+ const lines = text.split("\n");
485
+ let currentSection = null;
486
+ let contentLines = [];
487
+ for (const line of lines) {
488
+ const trimmedLine = line.trim();
489
+ const headerMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/);
490
+ if (headerMatch) {
491
+ if (currentSection) {
492
+ currentSection.content = contentLines.join("\n").trim();
493
+ if (currentSection.header || currentSection.content || currentSection.bullets.length > 0) {
494
+ sections.push(currentSection);
495
+ }
496
+ }
497
+ currentSection = {
498
+ header: (headerMatch[2] || "").replace(/\*\*/g, "").trim(),
499
+ level: (headerMatch[1] || "").length,
500
+ content: "",
501
+ bullets: [],
502
+ metrics: []
503
+ };
504
+ contentLines = [];
505
+ continue;
506
+ }
507
+ const bulletMatch = trimmedLine.match(/^[-*+]\s+(.+)$/);
508
+ if (bulletMatch && bulletMatch[1] && currentSection) {
509
+ currentSection.bullets.push(bulletMatch[1]);
510
+ continue;
511
+ }
512
+ const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/);
513
+ if (numberedMatch && numberedMatch[1] && currentSection) {
514
+ currentSection.bullets.push(numberedMatch[1]);
515
+ continue;
516
+ }
517
+ const metricMatch = trimmedLine.match(/\$?([\d,]+\.?\d*)[%]?\s*[-–—:]\s*(.+)/);
518
+ if (metricMatch && metricMatch[1] && metricMatch[2] && currentSection) {
519
+ currentSection.metrics.push({
520
+ value: metricMatch[1],
521
+ label: metricMatch[2].slice(0, 50)
522
+ });
523
+ continue;
524
+ }
525
+ if (trimmedLine.includes("|") && currentSection) {
526
+ const cells = trimmedLine.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^-+$/));
527
+ if (cells.length >= 2) {
528
+ const value = cells.find((c) => c.match(/^\$?[\d,]+\.?\d*[%]?$/));
529
+ const label = cells.find((c) => !c.match(/^\$?[\d,]+\.?\d*[%]?$/) && c.length > 2);
530
+ if (value && label) {
531
+ currentSection.metrics.push({ value, label: label.slice(0, 50) });
532
+ }
533
+ }
534
+ continue;
210
535
  }
211
- } else if (typeof obj === "object" && obj !== null) {
212
- for (const [key, value] of Object.entries(obj)) {
213
- const newPrefix = prefix ? `${prefix}.${key}` : key;
214
- parts.push(this.flattenObject(value, newPrefix));
536
+ if (trimmedLine && currentSection) {
537
+ contentLines.push(trimmedLine);
215
538
  }
216
- } else if (obj !== null && obj !== void 0) {
217
- parts.push(String(obj));
218
539
  }
219
- return parts.join("\n");
220
- }
221
- /**
222
- * Split text into paragraphs.
223
- */
224
- splitIntoParagraphs(text) {
225
- return text.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
226
- }
227
- /**
228
- * Split text into sentences.
229
- */
230
- splitIntoSentences(text) {
231
- const cleaned = text.replace(/Mr\./g, "Mr").replace(/Mrs\./g, "Mrs").replace(/Dr\./g, "Dr").replace(/vs\./g, "vs").replace(/etc\./g, "etc").replace(/e\.g\./g, "eg").replace(/i\.e\./g, "ie");
232
- return cleaned.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 10);
540
+ if (currentSection) {
541
+ currentSection.content = contentLines.join("\n").trim();
542
+ if (currentSection.header || currentSection.content || currentSection.bullets.length > 0) {
543
+ sections.push(currentSection);
544
+ }
545
+ }
546
+ return sections;
233
547
  }
234
548
  /**
235
- * Extract SCQA structure (Barbara Minto's Pyramid Principle).
549
+ * Extract SCQA structure (Barbara Minto)
236
550
  */
237
- extractSCQA(paragraphs, sentences) {
551
+ extractSCQA(text) {
552
+ const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
238
553
  let situation = "";
239
554
  let complication = "";
240
555
  let question = "";
241
556
  let answer = "";
242
557
  for (const para of paragraphs.slice(0, 3)) {
243
- if (this.containsSignals(para, this.situationSignals)) {
244
- situation = this.extractRelevantSentence(para, this.situationSignals);
558
+ if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
559
+ situation = this.extractFirstSentence(para);
245
560
  break;
246
561
  }
247
562
  }
248
563
  for (const para of paragraphs) {
249
- if (this.containsSignals(para, this.complicationSignals)) {
250
- complication = this.extractRelevantSentence(para, this.complicationSignals);
564
+ if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
565
+ complication = this.extractFirstSentence(para);
251
566
  break;
252
567
  }
253
568
  }
254
- for (const sentence of sentences) {
255
- if (sentence.includes("?") || this.containsSignals(sentence, this.questionSignals)) {
256
- question = sentence;
569
+ const middleStart = Math.floor(paragraphs.length * 0.2);
570
+ const middleEnd = Math.floor(paragraphs.length * 0.8);
571
+ for (const para of paragraphs.slice(middleStart, middleEnd)) {
572
+ const lowerPara = para.toLowerCase();
573
+ if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
574
+ answer = this.extractFirstSentence(para);
257
575
  break;
258
576
  }
259
577
  }
260
- for (const para of paragraphs.slice(-3)) {
261
- if (this.containsSignals(para, this.answerSignals)) {
262
- answer = this.extractRelevantSentence(para, this.answerSignals);
263
- break;
264
- }
578
+ if (!situation && paragraphs.length > 0 && paragraphs[0]) {
579
+ situation = this.extractFirstSentence(paragraphs[0]);
265
580
  }
266
- if (!situation && paragraphs.length > 0) {
267
- situation = this.truncateToSentence(paragraphs[0] ?? "", 150);
581
+ if (!answer && paragraphs.length > 2) {
582
+ for (const para of paragraphs.slice(1, Math.floor(paragraphs.length * 0.5))) {
583
+ const lowerPara = para.toLowerCase();
584
+ if (lowerPara.includes("recommend") || lowerPara.includes("strategy") || lowerPara.includes("solution") || lowerPara.includes("approach")) {
585
+ answer = this.extractFirstSentence(para);
586
+ break;
587
+ }
588
+ }
268
589
  }
269
- if (!answer && paragraphs.length > 1) {
270
- const lastPara = paragraphs[paragraphs.length - 1];
271
- answer = lastPara ? this.truncateToSentence(lastPara, 150) : "";
590
+ if (complication && !question) {
591
+ question = `How do we address: ${complication.slice(0, 80)}?`;
272
592
  }
273
593
  return { situation, complication, question, answer };
274
594
  }
275
595
  /**
276
- * Extract Sparkline structure (Nancy Duarte).
596
+ * Extract STAR moments (Something They'll Always Remember)
597
+ * Per Nancy Duarte: These should be memorable, COMPLETE thoughts with impact
598
+ *
599
+ * CRITICAL REQUIREMENTS:
600
+ * - Must be complete sentences with subject + verb structure
601
+ * - Must have 5+ words minimum (no fragments!)
602
+ * - Must be 30+ characters
603
+ * - Must contain a verb
604
+ * - NOT fragments like "significant growth" or "cloud-first strategy"
605
+ * - NOT headers or topic labels
277
606
  */
278
- extractSparkline(paragraphs) {
279
- const whatIs = [];
280
- const whatCouldBe = [];
281
- let callToAdventure = "";
282
- for (const para of paragraphs) {
283
- const lowerPara = para.toLowerCase();
284
- if (this.containsSignals(lowerPara, this.whatIsSignals)) {
285
- whatIs.push(this.truncateToSentence(para, 100));
607
+ extractStarMoments(text) {
608
+ const starMoments = [];
609
+ const usedPhrases = /* @__PURE__ */ new Set();
610
+ const fragmentPatterns = [
611
+ /^[a-z]+-[a-z]+\s+(strategy|approach|solution|platform|architecture)$/i,
612
+ // "cloud-first strategy"
613
+ /^(significant|substantial|dramatic|rapid|strong)\s+(growth|increase|decline|change)$/i,
614
+ // "significant growth"
615
+ /^(digital|cloud|data|ai)\s+(transformation|strategy|platform|solution)$/i,
616
+ // "digital transformation"
617
+ /^(our|your|the|a)\s+\w+\s*$/i,
618
+ // "our solution", "the platform"
619
+ /^[a-z]+\s+[a-z]+$/i
620
+ // Any two-word phrase
621
+ ];
622
+ const verbPatterns = /\b(is|are|was|were|will|can|could|should|must|has|have|had|does|do|provides?|enables?|allows?|offers?|delivers?|creates?|achieves?|exceeds?|results?|generates?|grows?|increases?|decreases?|transforms?|improves?|reduces?)\b/i;
623
+ const boldMatches = text.match(/\*\*([^*]+)\*\*/g);
624
+ if (boldMatches) {
625
+ for (const match of boldMatches) {
626
+ const cleaned = match.replace(/\*\*/g, "").trim();
627
+ const lowerCleaned = cleaned.toLowerCase();
628
+ const wordCount = cleaned.split(/\s+/).length;
629
+ const normalized = cleaned.toLowerCase().slice(0, 30);
630
+ const hasVerb = verbPatterns.test(cleaned);
631
+ const isLongEnough = wordCount >= 5 && cleaned.length >= 30;
632
+ const isNotFragment = !fragmentPatterns.some((p) => p.test(cleaned));
633
+ const isNotHeader = !/^(The |Our |Your |Next |Key |Overview|Introduction|Conclusion|Summary|Background)/i.test(cleaned);
634
+ const isNotCTA = !this.containsSignals(lowerCleaned, this.ctaSignals);
635
+ const hasMetricContext = /\d+[%xX]|\$[\d,]+/.test(cleaned) && wordCount >= 5;
636
+ if ((hasVerb || hasMetricContext) && isLongEnough && isNotFragment && isNotHeader && isNotCTA && wordCount <= 25 && !usedPhrases.has(normalized)) {
637
+ starMoments.push(cleaned);
638
+ usedPhrases.add(normalized);
639
+ if (starMoments.length >= 3) break;
640
+ }
286
641
  }
287
- if (this.containsSignals(lowerPara, this.whatCouldBeSignals)) {
288
- whatCouldBe.push(this.truncateToSentence(para, 100));
642
+ }
643
+ if (starMoments.length < 3) {
644
+ const cleanedText = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
645
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g) || [];
646
+ for (const sentence of sentences) {
647
+ const cleaned = sentence.trim();
648
+ const normalized = cleaned.toLowerCase().slice(0, 30);
649
+ const wordCount = cleaned.split(/\s+/).length;
650
+ const hasHighImpact = /(\d{2,3}%|\d+x|billion|million|\$[\d,]+\s*(million|billion)?)/i.test(cleaned);
651
+ const hasVerb = verbPatterns.test(cleaned);
652
+ if (hasHighImpact && hasVerb && wordCount >= 6 && wordCount <= 25 && cleaned.length >= 40 && cleaned.length <= 200 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
653
+ starMoments.push(cleaned);
654
+ usedPhrases.add(normalized);
655
+ if (starMoments.length >= 3) break;
656
+ }
289
657
  }
290
658
  }
291
- for (const para of paragraphs.slice(-2)) {
292
- if (this.containsSignals(para.toLowerCase(), ["join", "together", "action", "start", "begin", "now"])) {
293
- callToAdventure = this.truncateToSentence(para, 150);
659
+ return starMoments.slice(0, 3);
660
+ }
661
+ /**
662
+ * Extract Sparkline elements (Nancy Duarte)
663
+ */
664
+ extractSparkline(text) {
665
+ const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
666
+ let callToAction = "";
667
+ const ctaSignals = ["next steps", "call to action", "take action", "start now", "join us", "contact"];
668
+ for (const para of paragraphs.slice(-3)) {
669
+ if (this.containsSignals(para.toLowerCase(), ctaSignals)) {
670
+ callToAction = this.extractFirstSentence(para);
294
671
  break;
295
672
  }
296
673
  }
297
- return { whatIs, whatCouldBe, callToAdventure };
674
+ return { callToAction };
298
675
  }
299
676
  /**
300
- * Extract key messages (max 3 - Rule of Three).
677
+ * Extract key messages - the actual insights from the content
678
+ * Per Carmine Gallo: Rule of Three - max 3 key messages
679
+ * Per Barbara Minto: Messages should be actionable conclusions, not topics
301
680
  */
302
- extractKeyMessages(text, sentences) {
681
+ extractKeyMessages(text) {
303
682
  const messages = [];
304
- const emphasisMatches = text.match(/\[EMPHASIS\](.+?)\[\/EMPHASIS\]/g);
305
- if (emphasisMatches) {
306
- for (const match of emphasisMatches.slice(0, 3)) {
307
- const content = match.replace(/\[EMPHASIS\]|\[\/EMPHASIS\]/g, "").trim();
308
- if (content.length > 10 && content.length < 100) {
309
- messages.push(content);
310
- }
311
- }
312
- }
313
- const headerMatches = text.match(/\[HEADER\](.+)/g);
314
- if (headerMatches && messages.length < 3) {
315
- for (const match of headerMatches.slice(0, 3 - messages.length)) {
316
- const content = match.replace("[HEADER]", "").trim();
317
- if (content.length > 5 && content.length < 80) {
318
- messages.push(content);
683
+ const usedPhrases = /* @__PURE__ */ new Set();
684
+ let cleanText = text.replace(/^#+\s+.+$/gm, "").replace(/\[.+?\]/g, "").replace(/\*\*/g, "");
685
+ cleanText = cleanText.replace(/(\d)\.(\d)/g, "$1<DECIMAL>$2");
686
+ const rawSentences = cleanText.match(/[^.!?]+[.!?]/g) || [];
687
+ const sentences = rawSentences.map((s) => s.replace(/<DECIMAL>/g, "."));
688
+ const insightSignals = [
689
+ "we recommend",
690
+ "we suggest",
691
+ "our strategy",
692
+ "the solution",
693
+ "key finding",
694
+ "result",
695
+ "achieve",
696
+ "improve",
697
+ "increase",
698
+ "decrease",
699
+ "expect",
700
+ "roi",
701
+ "benefit",
702
+ "opportunity",
703
+ "transform"
704
+ ];
705
+ for (const sentence of sentences) {
706
+ const cleaned = sentence.trim();
707
+ const normalized = cleaned.toLowerCase().slice(0, 30);
708
+ if (cleaned.length > 30 && cleaned.length < 150 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
709
+ if (this.containsSignals(cleaned.toLowerCase(), insightSignals)) {
710
+ messages.push(cleaned);
711
+ usedPhrases.add(normalized);
712
+ if (messages.length >= 3) break;
319
713
  }
320
714
  }
321
715
  }
322
716
  if (messages.length < 3) {
323
- const strongSentences = sentences.filter((s) => s.length > 20 && s.length < 100).filter((s) => this.containsSignals(s.toLowerCase(), ["key", "important", "critical", "essential", "must", "need"]));
324
- for (const sentence of strongSentences.slice(0, 3 - messages.length)) {
325
- messages.push(sentence);
717
+ for (const sentence of sentences) {
718
+ const cleaned = sentence.trim();
719
+ const normalized = cleaned.toLowerCase().slice(0, 30);
720
+ if (cleaned.length > 25 && cleaned.length < 150 && !usedPhrases.has(normalized) && /\d+[%xX]|\$[\d,]+/.test(cleaned)) {
721
+ messages.push(cleaned);
722
+ usedPhrases.add(normalized);
723
+ if (messages.length >= 3) break;
724
+ }
326
725
  }
327
726
  }
328
- if (messages.length < 3) {
727
+ if (messages.length < 2) {
329
728
  for (const sentence of sentences) {
330
- if (sentence.length > 30 && sentence.length < 100 && !messages.includes(sentence)) {
331
- messages.push(sentence);
729
+ const cleaned = sentence.trim();
730
+ const wordCount = cleaned.split(/\s+/).length;
731
+ const normalized = cleaned.toLowerCase().slice(0, 30);
732
+ if (wordCount >= 6 && wordCount <= 25 && !usedPhrases.has(normalized) && /\b(is|are|will|can|should|must|has|have|provides?|enables?|allows?)\b/.test(cleaned.toLowerCase())) {
733
+ messages.push(cleaned);
734
+ usedPhrases.add(normalized);
332
735
  if (messages.length >= 3) break;
333
736
  }
334
737
  }
@@ -336,195 +739,253 @@ var ContentAnalyzer = class {
336
739
  return messages.slice(0, 3);
337
740
  }
338
741
  /**
339
- * Generate action titles (McKinsey-style).
742
+ * Extract data points (metrics with values)
743
+ * IMPROVED: Smarter label extraction that understands markdown tables
340
744
  */
341
- generateActionTitles(keyMessages, paragraphs) {
342
- const titles = [];
343
- for (const message of keyMessages) {
344
- const actionTitle = this.transformToActionTitle(message);
345
- if (actionTitle) {
346
- titles.push(actionTitle);
745
+ extractDataPoints(text) {
746
+ const dataPoints = [];
747
+ const usedValues = /* @__PURE__ */ new Set();
748
+ const tableLines = text.split("\n").filter((line) => line.includes("|"));
749
+ if (tableLines.length >= 3) {
750
+ const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
751
+ if (dataRows.length >= 2) {
752
+ for (const row of dataRows.slice(1)) {
753
+ const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
754
+ if (cells.length >= 2) {
755
+ const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
756
+ const labelCell = cells[0];
757
+ if (valueCell && labelCell && !usedValues.has(valueCell)) {
758
+ usedValues.add(valueCell);
759
+ dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
760
+ }
761
+ }
762
+ }
347
763
  }
348
764
  }
349
- for (const para of paragraphs) {
350
- if (titles.length >= 10) break;
351
- const headerMatch = para.match(/\[HEADER\]\s*(.+)/);
352
- if (headerMatch?.[1]) {
353
- const title = this.transformToActionTitle(headerMatch[1]);
354
- if (title && !titles.includes(title)) {
355
- titles.push(title);
765
+ const lines = text.split("\n");
766
+ for (const line of lines) {
767
+ if (line.includes("|")) continue;
768
+ const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
769
+ if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
770
+ usedValues.add(percentMatch[2]);
771
+ dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
772
+ }
773
+ const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
774
+ if (dollarMatch && dataPoints.length < 6) {
775
+ const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
776
+ if (!usedValues.has(fullValue)) {
777
+ usedValues.add(fullValue);
778
+ const label = this.extractLabelFromLine(line, fullValue);
779
+ dataPoints.push({ value: fullValue, label });
356
780
  }
357
781
  }
358
782
  }
359
- return titles;
783
+ return dataPoints.slice(0, 4);
360
784
  }
361
785
  /**
362
- * Transform a statement into an action title.
786
+ * Extract a meaningful label from a line containing a metric
363
787
  */
364
- transformToActionTitle(statement) {
365
- let title = statement.replace(/\[(HEADER|EMPHASIS|BULLET|NUMBERED)\]/g, "").trim();
366
- const actionVerbs = ["increase", "decrease", "improve", "reduce", "achieve", "deliver", "create", "build", "launch", "transform", "enable", "drive"];
367
- const firstWord = title.split(" ")[0]?.toLowerCase();
368
- if (firstWord && actionVerbs.includes(firstWord)) {
369
- return this.capitalizeFirst(title);
370
- }
371
- if (title.toLowerCase().includes("should")) {
372
- title = title.replace(/we should|you should|should/gi, "").trim();
373
- return this.capitalizeFirst(title);
374
- }
375
- if (title.toLowerCase().includes("need to")) {
376
- title = title.replace(/we need to|you need to|need to/gi, "").trim();
377
- return this.capitalizeFirst(title);
378
- }
379
- if (title.length < 50) {
380
- return title;
381
- }
382
- return this.truncateToWords(title, 8);
788
+ extractLabelFromLine(line, value) {
789
+ const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
790
+ const beforeValue = cleaned.split(value)[0] || "";
791
+ const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
792
+ return words.slice(-4).join(" ").slice(0, 40) || "Value";
383
793
  }
384
794
  /**
385
- * Identify STAR moments (Something They'll Always Remember).
795
+ * Check if text contains any of the signals
386
796
  */
387
- identifyStarMoments(paragraphs) {
388
- const starMoments = [];
389
- const starSignals = [
390
- "surprising",
391
- "amazing",
392
- "incredible",
393
- "remarkable",
394
- "stunning",
395
- "imagine",
396
- "what if",
397
- "breakthrough",
398
- "revolutionary",
399
- "never before",
400
- "first time",
401
- "unprecedented",
402
- "game-changing",
403
- "dramatic"
404
- ];
405
- for (const para of paragraphs) {
406
- if (this.containsSignals(para.toLowerCase(), starSignals)) {
407
- const moment = this.truncateToSentence(para, 120);
408
- if (moment.length > 20) {
409
- starMoments.push(moment);
410
- }
411
- }
412
- }
413
- const statPattern = /\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion|trillion))?|\d+(?:\s*(?:million|billion|trillion))/g;
414
- for (const para of paragraphs) {
415
- if (statPattern.test(para) && starMoments.length < 5) {
416
- const moment = this.truncateToSentence(para, 100);
417
- if (!starMoments.includes(moment)) {
418
- starMoments.push(moment);
419
- }
420
- }
421
- }
422
- return starMoments.slice(0, 5);
797
+ containsSignals(text, signals) {
798
+ return signals.some((signal) => text.includes(signal));
423
799
  }
424
800
  /**
425
- * Estimate slide count based on content.
801
+ * Extract first meaningful sentence from text
802
+ * CRITICAL: Must strip headers, CTA text, and fragments
426
803
  */
427
- estimateSlideCount(text, paragraphs) {
428
- const wordCount = text.split(/\s+/).length;
429
- const headerCount = (text.match(/\[HEADER\]/g) ?? []).length;
430
- const bulletGroups = (text.match(/\[BULLET\]/g) ?? []).length / 4;
431
- const wordBasedEstimate = Math.ceil(wordCount / 35);
432
- const estimate = Math.max(
433
- 5,
434
- // Minimum 5 slides
435
- Math.ceil((wordBasedEstimate + headerCount + bulletGroups) / 2)
436
- );
437
- return Math.min(estimate, 30);
438
- }
439
- // === Helper Methods ===
440
- containsSignals(text, signals) {
441
- const lowerText = text.toLowerCase();
442
- return signals.some((signal) => lowerText.includes(signal));
443
- }
444
- extractRelevantSentence(paragraph, signals) {
445
- const sentences = paragraph.split(/[.!?]+/);
804
+ extractFirstSentence(text) {
805
+ let cleaned = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
806
+ cleaned = cleaned.replace(/^(Overview|Introduction|The Problem|Our Solution|Next Steps|Conclusion|Summary|Background|Context|Recommendation)\s*:?\s*/i, "").trim();
807
+ const sentences = cleaned.match(/[^.!?]+[.!?]/g) || [];
446
808
  for (const sentence of sentences) {
447
- if (this.containsSignals(sentence.toLowerCase(), signals)) {
448
- return sentence.trim();
809
+ const trimmed = sentence.trim();
810
+ if (trimmed.length >= 20 && /\b(is|are|was|were|will|can|should|must|has|have|had|provides?|enables?|allows?|offers?|delivers?|creates?|includes?|involves?|requires?|experiencing)\b/i.test(trimmed) && !this.containsSignals(trimmed.toLowerCase(), this.ctaSignals)) {
811
+ return trimmed.slice(0, 150);
449
812
  }
450
813
  }
451
- return this.truncateToSentence(paragraph, 150);
452
- }
453
- truncateToSentence(text, maxLength) {
454
- if (text.length <= maxLength) {
455
- return text.trim();
456
- }
457
- const truncated = text.slice(0, maxLength);
458
- const lastPeriod = truncated.lastIndexOf(".");
459
- const lastQuestion = truncated.lastIndexOf("?");
460
- const lastExclaim = truncated.lastIndexOf("!");
461
- const lastBoundary = Math.max(lastPeriod, lastQuestion, lastExclaim);
462
- if (lastBoundary > maxLength * 0.5) {
463
- return text.slice(0, lastBoundary + 1).trim();
464
- }
465
- return truncated.trim() + "...";
466
- }
467
- truncateToWords(text, maxWords) {
468
- const words = text.split(/\s+/);
469
- if (words.length <= maxWords) {
470
- return text;
471
- }
472
- return words.slice(0, maxWords).join(" ");
473
- }
474
- capitalizeFirst(text) {
475
- if (!text) return "";
476
- return text.charAt(0).toUpperCase() + text.slice(1);
814
+ const fallback = cleaned.slice(0, 150);
815
+ return fallback.length >= 20 ? fallback : "";
477
816
  }
478
817
  };
479
818
 
480
819
  // src/core/SlideFactory.ts
481
820
  var SlideFactory = class {
482
821
  templates;
822
+ usedContent = /* @__PURE__ */ new Set();
483
823
  constructor() {
484
824
  this.templates = this.initializeTemplates();
485
825
  }
826
+ /**
827
+ * Check if content has already been used (deduplication)
828
+ */
829
+ isContentUsed(content) {
830
+ if (!content) return true;
831
+ const normalized = content.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 50);
832
+ if (this.usedContent.has(normalized)) {
833
+ return true;
834
+ }
835
+ this.usedContent.add(normalized);
836
+ return false;
837
+ }
486
838
  /**
487
839
  * Create slides from analyzed content.
840
+ *
841
+ * ARCHITECTURE (per KB expert methodologies):
842
+ * 1. Title slide - always first
843
+ * 2. Agenda slide - business mode only with 3+ sections
844
+ * 3. SCQA slides - Situation, Complication (per Minto)
845
+ * 4. Content slides - from sections with bullets/content
846
+ * 5. Metrics slide - if data points exist
847
+ * 6. Solution slide - SCQA answer
848
+ * 7. STAR moments - only if high-quality (per Duarte)
849
+ * 8. CTA slide - if call to action exists
850
+ * 9. Thank you slide - always last
488
851
  */
489
852
  async createSlides(analysis, mode) {
490
853
  const slides = [];
491
854
  let slideIndex = 0;
855
+ this.usedContent.clear();
492
856
  slides.push(this.createTitleSlide(slideIndex++, analysis));
493
- if (mode === "business" && analysis.keyMessages.length >= 2) {
857
+ this.isContentUsed(analysis.titles[0] ?? "");
858
+ const substantiveSections = analysis.sections.filter(
859
+ (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
860
+ );
861
+ if (mode === "business" && substantiveSections.length >= 3) {
494
862
  slides.push(this.createAgendaSlide(slideIndex++, analysis));
495
863
  }
496
- if (analysis.scqa.situation) {
864
+ if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
497
865
  slides.push(this.createContextSlide(slideIndex++, analysis, mode));
498
866
  }
499
- if (analysis.scqa.complication) {
867
+ if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
500
868
  slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
501
869
  }
870
+ for (const section of substantiveSections.slice(0, 4)) {
871
+ const headerUsed = this.isContentUsed(section.header);
872
+ const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
873
+ if (!headerUsed && !contentUsed) {
874
+ if (section.bullets.length > 0) {
875
+ slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
876
+ } else if (section.content.length > 50) {
877
+ slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
878
+ }
879
+ }
880
+ }
502
881
  for (const message of analysis.keyMessages) {
503
- slides.push(this.createMessageSlide(slideIndex++, message, mode));
882
+ const wordCount = message.split(/\s+/).length;
883
+ if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
884
+ slides.push(this.createMessageSlide(slideIndex++, message, mode));
885
+ }
504
886
  }
505
- for (const starMoment of analysis.starMoments.slice(0, 2)) {
506
- slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
887
+ if (analysis.dataPoints.length >= 2) {
888
+ slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
507
889
  }
508
- if (analysis.scqa.answer) {
890
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
509
891
  slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
510
892
  }
511
- if (analysis.sparkline.callToAdventure) {
893
+ const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
894
+ for (const starMoment of analysis.starMoments.slice(0, 2)) {
895
+ const wordCount = starMoment.split(/\s+/).length;
896
+ const hasVerb = verbPattern.test(starMoment);
897
+ if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
898
+ slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
899
+ }
900
+ }
901
+ if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
512
902
  slides.push(this.createCTASlide(slideIndex++, analysis, mode));
513
903
  }
514
904
  slides.push(this.createThankYouSlide(slideIndex++));
515
905
  return slides;
516
906
  }
907
+ /**
908
+ * Create a slide from a section with bullets
909
+ */
910
+ createSectionBulletSlide(index, section, mode) {
911
+ const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
912
+ return {
913
+ index,
914
+ type: "bullet-points",
915
+ data: {
916
+ title: this.truncate(section.header, 60),
917
+ bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
918
+ },
919
+ classes: ["slide-bullet-points"]
920
+ };
921
+ }
922
+ /**
923
+ * Create a slide from a section with body content
924
+ */
925
+ createSectionContentSlide(index, section, mode) {
926
+ if (mode === "keynote") {
927
+ return {
928
+ index,
929
+ type: "single-statement",
930
+ data: {
931
+ title: this.truncate(this.extractFirstSentence(section.content), 80),
932
+ keyMessage: section.header
933
+ },
934
+ classes: ["slide-single-statement"]
935
+ };
936
+ }
937
+ return {
938
+ index,
939
+ type: "two-column",
940
+ data: {
941
+ title: this.truncate(section.header, 60),
942
+ body: this.truncate(section.content, 200)
943
+ },
944
+ classes: ["slide-two-column"]
945
+ };
946
+ }
947
+ /**
948
+ * Extract first sentence from text
949
+ */
950
+ extractFirstSentence(text) {
951
+ const cleaned = this.cleanText(text);
952
+ const match = cleaned.match(/^[^.!?]+[.!?]/);
953
+ return match ? match[0].trim() : cleaned.slice(0, 100);
954
+ }
955
+ /**
956
+ * Create a metrics slide from data points
957
+ */
958
+ createMetricsSlide(index, dataPoints) {
959
+ const metrics = dataPoints.slice(0, 4).map((dp) => ({
960
+ value: dp.value,
961
+ label: this.cleanText(dp.label).slice(0, 40)
962
+ }));
963
+ return {
964
+ index,
965
+ type: "metrics-grid",
966
+ data: {
967
+ title: "Key Metrics",
968
+ metrics
969
+ },
970
+ classes: ["slide-metrics-grid"]
971
+ };
972
+ }
517
973
  /**
518
974
  * Create a title slide.
519
975
  */
520
976
  createTitleSlide(index, analysis) {
521
- const subtitle = analysis.keyMessages[0] ?? analysis.scqa.answer ?? "";
977
+ let subtitle = "";
978
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 10) {
979
+ subtitle = analysis.scqa.answer;
980
+ } else if (analysis.starMoments.length > 0 && analysis.starMoments[0]) {
981
+ subtitle = analysis.starMoments[0];
982
+ }
522
983
  return {
523
984
  index,
524
985
  type: "title",
525
986
  data: {
526
987
  title: analysis.titles[0] ?? "Presentation",
527
- subtitle: this.truncate(subtitle, 60),
988
+ subtitle: this.truncate(subtitle, 80),
528
989
  keyMessage: analysis.scqa.answer
529
990
  },
530
991
  classes: ["slide-title"]
@@ -805,23 +1266,43 @@ var SlideFactory = class {
805
1266
  }
806
1267
  // === Helper Methods ===
807
1268
  /**
808
- * Clean text by removing all content markers.
1269
+ * Clean text by removing all markdown and content markers.
1270
+ * CRITICAL: Must strip all formatting to prevent garbage in slides
809
1271
  */
810
1272
  cleanText(text) {
811
1273
  if (!text) return "";
812
- return text.replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\s+/g, " ").trim();
1274
+ return text.replace(/^#+\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\n{2,}/g, " ").replace(/\s+/g, " ").trim();
813
1275
  }
814
1276
  /**
815
- * Truncate text to max length at word boundary.
1277
+ * Truncate text to max length at sentence boundary when possible.
1278
+ * CRITICAL: Never cut mid-number (99.5% should not become 99.)
816
1279
  */
817
1280
  truncate(text, maxLength) {
818
1281
  const cleanedText = this.cleanText(text);
819
1282
  if (!cleanedText || cleanedText.length <= maxLength) {
820
1283
  return cleanedText;
821
1284
  }
1285
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1286
+ if (sentences) {
1287
+ let result = "";
1288
+ for (const sentence of sentences) {
1289
+ if ((result + sentence).length <= maxLength) {
1290
+ result += sentence;
1291
+ } else {
1292
+ break;
1293
+ }
1294
+ }
1295
+ if (result.length > maxLength * 0.5) {
1296
+ return result.trim();
1297
+ }
1298
+ }
822
1299
  const truncated = cleanedText.slice(0, maxLength);
823
- const lastSpace = truncated.lastIndexOf(" ");
824
- if (lastSpace > maxLength * 0.7) {
1300
+ let lastSpace = truncated.lastIndexOf(" ");
1301
+ const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1302
+ if (/^[\d.,%$]+/.test(afterCut)) {
1303
+ lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
1304
+ }
1305
+ if (lastSpace > maxLength * 0.5) {
825
1306
  return truncated.slice(0, lastSpace) + "...";
826
1307
  }
827
1308
  return truncated + "...";
@@ -852,10 +1333,12 @@ var SlideFactory = class {
852
1333
  return sentences.slice(0, 5).map((s) => s.trim());
853
1334
  }
854
1335
  /**
855
- * Remove a statistic from text.
1336
+ * Remove a statistic from text and clean thoroughly.
856
1337
  */
857
1338
  removeStatistic(text, stat) {
858
- return text.replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1339
+ const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1340
+ const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1341
+ return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
859
1342
  }
860
1343
  };
861
1344
 
@@ -1755,19 +2238,28 @@ var ScoreCalculator = class {
1755
2238
  import { chromium } from "playwright";
1756
2239
  var QAEngine = class {
1757
2240
  browser = null;
2241
+ kb;
1758
2242
  /**
1759
- * Validate a presentation.
2243
+ * Initialize KnowledgeGateway
2244
+ */
2245
+ async initialize() {
2246
+ this.kb = await getKnowledgeGateway();
2247
+ }
2248
+ /**
2249
+ * Validate a presentation using KB-driven quality metrics
1760
2250
  */
1761
2251
  async validate(presentation, options) {
2252
+ await this.initialize();
1762
2253
  const html = typeof presentation === "string" ? presentation : presentation.toString("utf-8");
1763
2254
  const mode = options?.mode ?? "keynote";
2255
+ const qualityMetrics = this.kb.getQualityMetrics();
1764
2256
  await this.initBrowser();
1765
2257
  try {
1766
2258
  const [visualResults, contentResults, expertResults, accessibilityResults] = await Promise.all([
1767
- this.runVisualTests(html, mode),
1768
- this.runContentTests(html, mode),
2259
+ this.runVisualTests(html, mode, qualityMetrics),
2260
+ this.runContentTests(html, mode, qualityMetrics),
1769
2261
  this.runExpertTests(html, mode),
1770
- this.runAccessibilityTests(html)
2262
+ this.runAccessibilityTests(html, qualityMetrics)
1771
2263
  ]);
1772
2264
  const issues = this.collectIssues(visualResults, contentResults, expertResults, accessibilityResults);
1773
2265
  const errorCount = issues.filter((i) => i.severity === "error").length;
@@ -1785,18 +2277,18 @@ var QAEngine = class {
1785
2277
  }
1786
2278
  }
1787
2279
  /**
1788
- * Calculate overall QA score.
2280
+ * Calculate overall QA score using KB-driven weights
1789
2281
  */
1790
2282
  calculateScore(results) {
1791
2283
  const weights = {
1792
2284
  visual: 0.35,
1793
- // 35%
2285
+ // 35% - Design quality
1794
2286
  content: 0.3,
1795
- // 30%
2287
+ // 30% - Content quality
1796
2288
  expert: 0.25,
1797
- // 25%
2289
+ // 25% - Expert methodology
1798
2290
  accessibility: 0.1
1799
- // 10%
2291
+ // 10% - Accessibility
1800
2292
  };
1801
2293
  const visualScore = this.calculateVisualScore(results.visual);
1802
2294
  const contentScore = this.calculateContentScore(results.content);
@@ -1843,13 +2335,15 @@ var QAEngine = class {
1843
2335
  };
1844
2336
  }
1845
2337
  // ===========================================================================
1846
- // VISUAL TESTS
2338
+ // VISUAL TESTS - KB-DRIVEN
1847
2339
  // ===========================================================================
1848
- async runVisualTests(html, mode) {
2340
+ async runVisualTests(html, mode, qualityMetrics) {
1849
2341
  const page = await this.browser.newPage();
1850
2342
  await page.setViewportSize({ width: 1280, height: 720 });
1851
2343
  await page.setContent(html);
1852
2344
  await page.waitForTimeout(1e3);
2345
+ const modeMetrics = mode === "keynote" ? qualityMetrics.keynote_mode : qualityMetrics.business_mode;
2346
+ const minWhitespace = this.kb.getWhitespaceRequirement(mode) * 100;
1853
2347
  const slideCount = await page.evaluate(() => {
1854
2348
  return window.Reveal?.getTotalSlides?.() ?? document.querySelectorAll(".slides > section").length;
1855
2349
  });
@@ -1915,9 +2409,8 @@ var QAEngine = class {
1915
2409
  }, { slideIndex: i });
1916
2410
  if (slideAnalysis) {
1917
2411
  const issues = [];
1918
- const minWhitespace = mode === "keynote" ? 40 : 25;
1919
2412
  if (slideAnalysis.whitespace < minWhitespace) {
1920
- issues.push(`Whitespace ${slideAnalysis.whitespace}% below ${minWhitespace}% minimum`);
2413
+ issues.push(`Whitespace ${slideAnalysis.whitespace}% below ${minWhitespace}% minimum (KB-defined)`);
1921
2414
  }
1922
2415
  if (slideAnalysis.whitespace > 80) {
1923
2416
  issues.push(`Whitespace ${slideAnalysis.whitespace}% - slide appears sparse`);
@@ -1972,34 +2465,35 @@ var QAEngine = class {
1972
2465
  };
1973
2466
  }
1974
2467
  // ===========================================================================
1975
- // CONTENT TESTS
2468
+ // CONTENT TESTS - KB-DRIVEN
1976
2469
  // ===========================================================================
1977
- async runContentTests(html, mode) {
2470
+ async runContentTests(html, mode, qualityMetrics) {
1978
2471
  const page = await this.browser.newPage();
1979
2472
  await page.setContent(html);
1980
2473
  await page.waitForTimeout(500);
1981
- const results = await page.evaluate((targetMode) => {
2474
+ const wordLimits = this.kb.getWordLimits(mode);
2475
+ const glanceTest = this.kb.getDuarteGlanceTest();
2476
+ const results = await page.evaluate((params) => {
2477
+ const { targetMode, maxWords, minWords, glanceWordLimit } = params;
1982
2478
  const slides = document.querySelectorAll(".slides > section");
1983
2479
  const perSlide = [];
1984
- const glanceTest = [];
2480
+ const glanceTestResults = [];
1985
2481
  const signalNoise = [];
1986
2482
  const oneIdea = [];
1987
2483
  slides.forEach((slide, index) => {
1988
2484
  const text = slide.innerText || "";
1989
2485
  const words = text.split(/\s+/).filter((w) => w.length > 0);
1990
2486
  const wordCount = words.length;
1991
- const maxWords = targetMode === "keynote" ? 25 : 80;
1992
- const minWords = targetMode === "business" ? 20 : 0;
1993
2487
  const withinLimit = wordCount <= maxWords && wordCount >= minWords;
1994
2488
  const title = slide.querySelector("h2")?.textContent || "";
1995
2489
  const hasVerb = /\b(is|are|was|were|has|have|had|will|can|could|should|would|may|might|must|exceeded|increased|decreased|grew|fell|drove|caused|enabled|prevented|achieved|failed|creates?|generates?|delivers?|provides?|shows?|demonstrates?)\b/i.test(title);
1996
2490
  const hasInsight = title.length > 30 && hasVerb;
1997
2491
  const issues = [];
1998
2492
  if (!withinLimit) {
1999
- issues.push(`Word count ${wordCount} outside ${minWords}-${maxWords} range`);
2493
+ issues.push(`Word count ${wordCount} outside ${minWords}-${maxWords} range (KB-defined)`);
2000
2494
  }
2001
2495
  if (targetMode === "business" && !hasInsight && index > 0) {
2002
- issues.push("Title is not action-oriented (missing insight)");
2496
+ issues.push("Title is not action-oriented (missing insight per Minto)");
2003
2497
  }
2004
2498
  perSlide.push({
2005
2499
  slideIndex: index,
@@ -2012,12 +2506,12 @@ var QAEngine = class {
2012
2506
  const keyMessage = prominentElement?.textContent?.trim() || "";
2013
2507
  const keyWordCount = keyMessage.split(/\s+/).filter((w) => w.length > 0).length;
2014
2508
  const readingTime = keyWordCount / 4.2;
2015
- glanceTest.push({
2509
+ glanceTestResults.push({
2016
2510
  slideIndex: index,
2017
2511
  keyMessage,
2018
2512
  wordCount: keyWordCount,
2019
2513
  readingTime: Math.round(readingTime * 10) / 10,
2020
- passed: readingTime <= 3 && keyWordCount <= 15,
2514
+ passed: readingTime <= 3 && keyWordCount <= glanceWordLimit,
2021
2515
  recommendation: readingTime > 3 ? `Shorten to ${Math.floor(3 * 4.2)} words or less` : void 0
2022
2516
  });
2023
2517
  const elements = slide.querySelectorAll("h1, h2, h3, p, li, img");
@@ -2044,35 +2538,143 @@ var QAEngine = class {
2044
2538
  conflictingIdeas: ideaCount > 2 ? ["Multiple competing ideas detected"] : void 0
2045
2539
  });
2046
2540
  });
2047
- return { perSlide, glanceTest, signalNoise, oneIdea };
2048
- }, mode);
2541
+ return { perSlide, glanceTest: glanceTestResults, signalNoise, oneIdea };
2542
+ }, {
2543
+ targetMode: mode,
2544
+ maxWords: wordLimits.max,
2545
+ minWords: mode === "business" ? 20 : 0,
2546
+ glanceWordLimit: glanceTest.wordLimit
2547
+ });
2049
2548
  await page.close();
2050
2549
  return results;
2051
2550
  }
2052
2551
  // ===========================================================================
2053
- // EXPERT TESTS
2552
+ // EXPERT TESTS - KB-DRIVEN
2054
2553
  // ===========================================================================
2055
2554
  async runExpertTests(html, mode) {
2555
+ const duartePrinciples = this.kb.getExpertPrinciples("nancy_duarte");
2556
+ const reynoldsPrinciples = this.kb.getExpertPrinciples("garr_reynolds");
2557
+ const galloPrinciples = this.kb.getExpertPrinciples("carmine_gallo");
2558
+ const andersonPrinciples = this.kb.getExpertPrinciples("chris_anderson");
2056
2559
  return {
2057
- duarte: this.createExpertResult("Nancy Duarte", ["Glance Test", "STAR Moment", "Sparkline"], 85),
2058
- reynolds: this.createExpertResult("Garr Reynolds", ["Signal-to-Noise", "Simplicity", "Picture Superiority"], 80),
2059
- gallo: this.createExpertResult("Carmine Gallo", ["Rule of Three", "Emotional Connection"], 85),
2060
- anderson: this.createExpertResult("Chris Anderson", ["One Idea", "Clarity"], 90)
2560
+ duarte: this.validateDuartePrinciples(html, duartePrinciples),
2561
+ reynolds: this.validateReynoldsPrinciples(html, reynoldsPrinciples),
2562
+ gallo: this.validateGalloPrinciples(html, galloPrinciples),
2563
+ anderson: this.validateAndersonPrinciples(html, andersonPrinciples)
2061
2564
  };
2062
2565
  }
2063
- createExpertResult(name, principles, score) {
2566
+ validateDuartePrinciples(html, principles) {
2567
+ const violations = [];
2568
+ let score = 100;
2569
+ const slideMatches = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
2570
+ slideMatches.forEach((slide, index) => {
2571
+ const h1Match = slide.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
2572
+ const h2Match = slide.match(/<h2[^>]*>([\s\S]*?)<\/h2>/i);
2573
+ const keyMessage = (h1Match?.[1] || h2Match?.[1] || "").replace(/<[^>]+>/g, "").trim();
2574
+ const wordCount = keyMessage.split(/\s+/).filter((w) => w.length > 0).length;
2575
+ if (wordCount > 25) {
2576
+ violations.push(`Slide ${index + 1}: Key message exceeds 25 words (Duarte Glance Test)`);
2577
+ score -= 5;
2578
+ }
2579
+ });
2580
+ const hasStarMoment = html.includes("callout") || html.includes("highlight") || html.includes("big-idea");
2581
+ if (!hasStarMoment && slideMatches.length > 5) {
2582
+ violations.push("Missing STAR moment (Something They'll Always Remember)");
2583
+ score -= 10;
2584
+ }
2064
2585
  return {
2065
- expertName: name,
2066
- principlesChecked: principles,
2586
+ expertName: "Nancy Duarte",
2587
+ principlesChecked: ["Glance Test", "STAR Moment", "Sparkline Structure"],
2067
2588
  passed: score >= 80,
2068
- score,
2069
- violations: score < 80 ? [`${name} principles not fully met`] : []
2589
+ score: Math.max(0, score),
2590
+ violations
2591
+ };
2592
+ }
2593
+ validateReynoldsPrinciples(html, principles) {
2594
+ const violations = [];
2595
+ let score = 100;
2596
+ const decorativeElements = (html.match(/class="[^"]*decorative[^"]*"/gi) || []).length;
2597
+ const randomImages = (html.match(/picsum\.photos/gi) || []).length;
2598
+ if (randomImages > 0) {
2599
+ violations.push(`Found ${randomImages} random stock images (Reynolds: only use purposeful images)`);
2600
+ score -= randomImages * 10;
2601
+ }
2602
+ if (decorativeElements > 3) {
2603
+ violations.push(`Too many decorative elements (${decorativeElements}) - reduces signal-to-noise ratio`);
2604
+ score -= 10;
2605
+ }
2606
+ const columnLayouts = (html.match(/class="[^"]*columns[^"]*"/gi) || []).length;
2607
+ const slideCount = (html.match(/<section/gi) || []).length;
2608
+ if (columnLayouts > slideCount * 0.5) {
2609
+ violations.push("Overuse of column layouts - simplify per Reynolds");
2610
+ score -= 10;
2611
+ }
2612
+ return {
2613
+ expertName: "Garr Reynolds",
2614
+ principlesChecked: ["Signal-to-Noise", "Simplicity", "Picture Superiority"],
2615
+ passed: score >= 80,
2616
+ score: Math.max(0, score),
2617
+ violations
2618
+ };
2619
+ }
2620
+ validateGalloPrinciples(html, principles) {
2621
+ const violations = [];
2622
+ let score = 100;
2623
+ const bulletLists = html.match(/<ul[^>]*>[\s\S]*?<\/ul>/gi) || [];
2624
+ bulletLists.forEach((list, index) => {
2625
+ const bullets = (list.match(/<li/gi) || []).length;
2626
+ if (bullets > 5) {
2627
+ violations.push(`Bullet list ${index + 1} has ${bullets} items (Gallo: max 3-5)`);
2628
+ score -= 5;
2629
+ }
2630
+ });
2631
+ const hasQuote = html.includes("blockquote") || html.includes("quote");
2632
+ const hasStory = html.toLowerCase().includes("story") || html.toLowerCase().includes("journey");
2633
+ if (!hasQuote && !hasStory) {
2634
+ violations.push("Missing emotional connection elements (quotes, stories)");
2635
+ score -= 10;
2636
+ }
2637
+ return {
2638
+ expertName: "Carmine Gallo",
2639
+ principlesChecked: ["Rule of Three", "Emotional Connection", "Headline Power"],
2640
+ passed: score >= 80,
2641
+ score: Math.max(0, score),
2642
+ violations
2643
+ };
2644
+ }
2645
+ validateAndersonPrinciples(html, principles) {
2646
+ const violations = [];
2647
+ let score = 100;
2648
+ const slideMatches = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
2649
+ slideMatches.forEach((slide, index) => {
2650
+ const headings = (slide.match(/<h[12][^>]*>/gi) || []).length;
2651
+ if (headings > 2) {
2652
+ violations.push(`Slide ${index + 1}: Multiple ideas detected (Anderson: one idea per slide)`);
2653
+ score -= 5;
2654
+ }
2655
+ });
2656
+ const jargonPatterns = ["synergy", "leverage", "paradigm", "holistic", "streamline"];
2657
+ let jargonCount = 0;
2658
+ jargonPatterns.forEach((pattern) => {
2659
+ const matches = html.toLowerCase().match(new RegExp(pattern, "gi")) || [];
2660
+ jargonCount += matches.length;
2661
+ });
2662
+ if (jargonCount > 5) {
2663
+ violations.push(`Excessive jargon detected (${jargonCount} instances) - simplify language`);
2664
+ score -= 10;
2665
+ }
2666
+ return {
2667
+ expertName: "Chris Anderson",
2668
+ principlesChecked: ["One Idea", "Clarity", "Throughline"],
2669
+ passed: score >= 80,
2670
+ score: Math.max(0, score),
2671
+ violations
2070
2672
  };
2071
2673
  }
2072
2674
  // ===========================================================================
2073
- // ACCESSIBILITY TESTS
2675
+ // ACCESSIBILITY TESTS - KB-DRIVEN
2074
2676
  // ===========================================================================
2075
- async runAccessibilityTests(html) {
2677
+ async runAccessibilityTests(html, qualityMetrics) {
2076
2678
  const page = await this.browser.newPage();
2077
2679
  await page.setContent(html);
2078
2680
  await page.waitForTimeout(500);
@@ -2122,7 +2724,7 @@ var QAEngine = class {
2122
2724
  };
2123
2725
  }
2124
2726
  // ===========================================================================
2125
- // SCORING
2727
+ // SCORING - KB-DRIVEN
2126
2728
  // ===========================================================================
2127
2729
  calculateVisualScore(results) {
2128
2730
  let score = 100;
@@ -2246,6 +2848,7 @@ var QAEngine = class {
2246
2848
  // src/generators/html/RevealJsGenerator.ts
2247
2849
  var RevealJsGenerator = class {
2248
2850
  templateEngine;
2851
+ kb;
2249
2852
  defaultRevealConfig = {
2250
2853
  revealVersion: "5.0.4",
2251
2854
  hash: true,
@@ -2264,20 +2867,32 @@ var RevealJsGenerator = class {
2264
2867
  constructor() {
2265
2868
  this.templateEngine = new TemplateEngine();
2266
2869
  }
2870
+ /**
2871
+ * Initialize KnowledgeGateway
2872
+ */
2873
+ async initialize() {
2874
+ this.kb = await getKnowledgeGateway();
2875
+ }
2267
2876
  /**
2268
2877
  * Generate complete Reveal.js HTML presentation.
2269
2878
  */
2270
2879
  async generate(slides, config) {
2880
+ await this.initialize();
2271
2881
  const templateConfig = {};
2272
2882
  if (config.theme) templateConfig.theme = config.theme;
2273
2883
  if (config.customTemplates) templateConfig.customTemplates = config.customTemplates;
2274
2884
  const slideHtml = this.templateEngine.renderAll(slides, templateConfig);
2885
+ const presentationType = this.detectPresentationType(config);
2886
+ const palette = this.kb.getRecommendedPalette(presentationType);
2887
+ const typography = this.kb.getTypography(config.mode);
2275
2888
  const docConfig = {
2276
2889
  title: config.title,
2277
2890
  slides: slideHtml.join("\n"),
2278
2891
  theme: config.theme ?? "default",
2279
2892
  revealConfig: this.defaultRevealConfig,
2280
- mode: config.mode
2893
+ mode: config.mode,
2894
+ palette,
2895
+ typography
2281
2896
  };
2282
2897
  if (config.author) docConfig.author = config.author;
2283
2898
  if (config.subject) docConfig.subject = config.subject;
@@ -2288,11 +2903,23 @@ var RevealJsGenerator = class {
2288
2903
  }
2289
2904
  return html;
2290
2905
  }
2906
+ /**
2907
+ * Detect presentation type from config
2908
+ */
2909
+ detectPresentationType(config) {
2910
+ if (config.presentationType) {
2911
+ return config.presentationType;
2912
+ }
2913
+ if (config.mode === "keynote") {
2914
+ return "ted_keynote";
2915
+ }
2916
+ return "consulting_deck";
2917
+ }
2291
2918
  /**
2292
2919
  * Build the complete HTML document.
2293
2920
  */
2294
2921
  buildDocument(options) {
2295
- const { title, author, subject, slides, theme, customCSS, revealConfig, mode } = options;
2922
+ const { title, author, subject, slides, theme, customCSS, revealConfig, mode, palette, typography } = options;
2296
2923
  return `<!DOCTYPE html>
2297
2924
  <html lang="en">
2298
2925
  <head>
@@ -2300,7 +2927,7 @@ var RevealJsGenerator = class {
2300
2927
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2301
2928
  <meta name="author" content="${this.escapeHtml(author ?? "Claude Presentation Master")}">
2302
2929
  <meta name="description" content="${this.escapeHtml(subject ?? "")}">
2303
- <meta name="generator" content="Claude Presentation Master v1.0.0">
2930
+ <meta name="generator" content="Claude Presentation Master v6.0.0">
2304
2931
  <title>${this.escapeHtml(title)}</title>
2305
2932
 
2306
2933
  <!-- Reveal.js CSS -->
@@ -2314,10 +2941,10 @@ var RevealJsGenerator = class {
2314
2941
  <!-- Mermaid for diagrams -->
2315
2942
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
2316
2943
 
2317
- <!-- Presentation Engine CSS -->
2944
+ <!-- Presentation Engine CSS - KB-Driven Design System -->
2318
2945
  <style>
2319
- ${this.getBaseStyles(mode)}
2320
- ${this.getThemeStyles(theme)}
2946
+ ${this.getBaseStyles(mode, palette, typography)}
2947
+ ${this.getThemeStyles(theme, palette)}
2321
2948
  ${this.getAnimationStyles()}
2322
2949
  ${customCSS ?? ""}
2323
2950
  </style>
@@ -2369,26 +2996,35 @@ ${slides}
2369
2996
  </html>`;
2370
2997
  }
2371
2998
  /**
2372
- * Get base styles for slides.
2999
+ * Get base styles for slides - KB-DRIVEN
2373
3000
  */
2374
- getBaseStyles(mode) {
3001
+ getBaseStyles(mode, palette, typography) {
2375
3002
  const fontSize = mode === "keynote" ? "2.5em" : "1.8em";
2376
3003
  const lineHeight = mode === "keynote" ? "1.4" : "1.5";
3004
+ const primary = palette.primary || "#1a1a2e";
3005
+ const secondary = palette.secondary || "#16213e";
3006
+ const accent = palette.accent || "#0f3460";
3007
+ const highlight = palette.accent || "#e94560";
3008
+ const text = palette.text || "#1a1a2e";
3009
+ const background = palette.background || "#ffffff";
2377
3010
  return `
2378
- /* Base Styles */
3011
+ /* Base Styles - KB-Driven Design System */
2379
3012
  :root {
3013
+ /* Typography from KB */
2380
3014
  --font-heading: 'Source Sans Pro', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, sans-serif;
2381
3015
  --font-body: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2382
3016
  --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
2383
3017
 
2384
- --color-primary: #1a1a2e;
2385
- --color-secondary: #16213e;
2386
- --color-accent: #0f3460;
2387
- --color-highlight: #e94560;
2388
- --color-text: #1a1a2e;
2389
- --color-text-light: #4a4a68;
2390
- --color-background: #ffffff;
3018
+ /* Colors from KB Palette: ${palette.name} */
3019
+ --color-primary: ${primary};
3020
+ --color-secondary: ${secondary};
3021
+ --color-accent: ${accent};
3022
+ --color-highlight: ${highlight};
3023
+ --color-text: ${text};
3024
+ --color-text-light: ${this.lightenColor(text, 30)};
3025
+ --color-background: ${background};
2391
3026
 
3027
+ /* Layout */
2392
3028
  --slide-padding: 60px;
2393
3029
  --content-max-width: 1200px;
2394
3030
  }
@@ -2504,7 +3140,7 @@ ${slides}
2504
3140
  font-size: 0.8em;
2505
3141
  }
2506
3142
 
2507
- /* Images */
3143
+ /* Images - NO RANDOM PICSUM - Only user-provided or none */
2508
3144
  .reveal img {
2509
3145
  max-width: 100%;
2510
3146
  height: auto;
@@ -2610,9 +3246,25 @@ ${slides}
2610
3246
  `;
2611
3247
  }
2612
3248
  /**
2613
- * Get theme-specific styles.
3249
+ * Lighten a hex color
3250
+ */
3251
+ lightenColor(hex, percent) {
3252
+ hex = hex.replace("#", "");
3253
+ let r = parseInt(hex.substring(0, 2), 16);
3254
+ let g = parseInt(hex.substring(2, 4), 16);
3255
+ let b = parseInt(hex.substring(4, 6), 16);
3256
+ r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));
3257
+ g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));
3258
+ b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
3259
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
3260
+ }
3261
+ /**
3262
+ * Get theme-specific styles - KB-DRIVEN
2614
3263
  */
2615
- getThemeStyles(theme) {
3264
+ getThemeStyles(theme, palette) {
3265
+ if (theme === "default") {
3266
+ return "";
3267
+ }
2616
3268
  const themes = {
2617
3269
  "default": "",
2618
3270
  "light-corporate": `
@@ -2626,10 +3278,10 @@ ${slides}
2626
3278
  `,
2627
3279
  "modern-tech": `
2628
3280
  :root {
2629
- --color-primary: #1a1a2e;
2630
- --color-secondary: #16213e;
2631
- --color-accent: #0f3460;
2632
- --color-highlight: #e94560;
3281
+ --color-primary: ${palette.primary || "#1a1a2e"};
3282
+ --color-secondary: ${palette.secondary || "#16213e"};
3283
+ --color-accent: ${palette.accent || "#0f3460"};
3284
+ --color-highlight: ${palette.accent || "#e94560"};
2633
3285
  --color-background: #f8f9fa;
2634
3286
  }
2635
3287
  `,
@@ -3875,239 +4527,6 @@ function createDefaultImageProvider(options) {
3875
4527
  return new CompositeImageProvider(providers);
3876
4528
  }
3877
4529
 
3878
- // src/knowledge/KnowledgeBase.ts
3879
- import { readFileSync } from "fs";
3880
- import { join, dirname } from "path";
3881
- import { fileURLToPath } from "url";
3882
- import * as yaml from "yaml";
3883
- function getModuleDir() {
3884
- if (typeof import.meta !== "undefined" && import.meta.url) {
3885
- return dirname(fileURLToPath(import.meta.url));
3886
- }
3887
- if (typeof __dirname !== "undefined") {
3888
- return __dirname;
3889
- }
3890
- return process.cwd();
3891
- }
3892
- var moduleDir = getModuleDir();
3893
- var KnowledgeBase = class {
3894
- data = null;
3895
- loaded = false;
3896
- /**
3897
- * Load the knowledge base from the bundled YAML file.
3898
- */
3899
- async load() {
3900
- if (this.loaded) return;
3901
- try {
3902
- const possiblePaths = [
3903
- // From dist/: go up to package root
3904
- join(moduleDir, "../../assets/presentation-knowledge.yaml"),
3905
- join(moduleDir, "../assets/presentation-knowledge.yaml"),
3906
- join(moduleDir, "../../../assets/presentation-knowledge.yaml"),
3907
- // From bundle in dist/index.js: go up one level
3908
- join(moduleDir, "assets/presentation-knowledge.yaml"),
3909
- // CWD-based paths for development or local installs
3910
- join(process.cwd(), "assets/presentation-knowledge.yaml"),
3911
- join(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
3912
- ];
3913
- let assetPath = "";
3914
- for (const p of possiblePaths) {
3915
- try {
3916
- readFileSync(p);
3917
- assetPath = p;
3918
- break;
3919
- } catch {
3920
- continue;
3921
- }
3922
- }
3923
- if (!assetPath) {
3924
- throw new Error("Could not locate knowledge base file");
3925
- }
3926
- const content = readFileSync(assetPath, "utf-8");
3927
- this.data = yaml.parse(content);
3928
- this.loaded = true;
3929
- console.log(`\u{1F4DA} Knowledge base loaded: v${this.data.version}`);
3930
- } catch (error) {
3931
- console.warn("\u26A0\uFE0F Could not load knowledge base, using defaults");
3932
- this.data = this.getDefaultData();
3933
- this.loaded = true;
3934
- }
3935
- }
3936
- /**
3937
- * Get expert methodology by name.
3938
- */
3939
- getExpert(name) {
3940
- this.ensureLoaded();
3941
- return this.data?.experts?.[name];
3942
- }
3943
- /**
3944
- * Get all expert names.
3945
- */
3946
- getExpertNames() {
3947
- this.ensureLoaded();
3948
- return Object.keys(this.data?.experts ?? {});
3949
- }
3950
- /**
3951
- * Get framework recommendation for audience.
3952
- */
3953
- getFrameworkForAudience(audience) {
3954
- this.ensureLoaded();
3955
- return this.data?.frameworkSelector?.byAudience?.[audience];
3956
- }
3957
- /**
3958
- * Get framework recommendation for goal.
3959
- */
3960
- getFrameworkForGoal(goal) {
3961
- this.ensureLoaded();
3962
- return this.data?.frameworkSelector?.byGoal?.[goal];
3963
- }
3964
- /**
3965
- * Get QA scoring rubric.
3966
- */
3967
- getScoringRubric() {
3968
- this.ensureLoaded();
3969
- return this.data?.automatedQA?.scoringRubric;
3970
- }
3971
- /**
3972
- * Get mode configuration (keynote or business).
3973
- */
3974
- getModeConfig(mode) {
3975
- this.ensureLoaded();
3976
- return this.data?.modes?.[mode];
3977
- }
3978
- /**
3979
- * Get slide type configuration.
3980
- */
3981
- getSlideType(type) {
3982
- this.ensureLoaded();
3983
- return this.data?.slideTypes?.[type];
3984
- }
3985
- /**
3986
- * Get the knowledge base version.
3987
- */
3988
- getVersion() {
3989
- this.ensureLoaded();
3990
- return this.data?.version ?? "unknown";
3991
- }
3992
- /**
3993
- * Validate a slide against expert principles.
3994
- */
3995
- validateAgainstExpert(expertName, slideData) {
3996
- const expert = this.getExpert(expertName);
3997
- if (!expert) {
3998
- return { passed: true, violations: [] };
3999
- }
4000
- const violations = [];
4001
- if (expert.wordLimits) {
4002
- if (expert.wordLimits.max && slideData.wordCount > expert.wordLimits.max) {
4003
- violations.push(`Exceeds ${expertName} word limit of ${expert.wordLimits.max}`);
4004
- }
4005
- if (expert.wordLimits.min && slideData.wordCount < expert.wordLimits.min) {
4006
- violations.push(`Below ${expertName} minimum of ${expert.wordLimits.min} words`);
4007
- }
4008
- }
4009
- return {
4010
- passed: violations.length === 0,
4011
- violations
4012
- };
4013
- }
4014
- /**
4015
- * Ensure knowledge base is loaded.
4016
- */
4017
- ensureLoaded() {
4018
- if (!this.loaded) {
4019
- this.data = this.getDefaultData();
4020
- this.loaded = true;
4021
- }
4022
- }
4023
- /**
4024
- * Get default data if YAML can't be loaded.
4025
- */
4026
- getDefaultData() {
4027
- return {
4028
- version: "1.0.0-fallback",
4029
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4030
- experts: {
4031
- "Nancy Duarte": {
4032
- name: "Nancy Duarte",
4033
- principles: [
4034
- { name: "Glance Test", description: "Message clear in 3 seconds" },
4035
- { name: "STAR Moment", description: "Something They'll Always Remember" },
4036
- { name: "Sparkline", description: "Contrast What Is vs What Could Be" }
4037
- ]
4038
- },
4039
- "Garr Reynolds": {
4040
- name: "Garr Reynolds",
4041
- principles: [
4042
- { name: "Signal-to-Noise", description: "Maximize signal, minimize noise" },
4043
- { name: "Simplicity", description: "Amplify through simplification" }
4044
- ]
4045
- },
4046
- "Carmine Gallo": {
4047
- name: "Carmine Gallo",
4048
- principles: [
4049
- { name: "Rule of Three", description: "Maximum 3 key messages" },
4050
- { name: "Emotional Connection", description: "Connect emotionally first" }
4051
- ]
4052
- },
4053
- "Chris Anderson": {
4054
- name: "Chris Anderson",
4055
- principles: [
4056
- { name: "One Idea", description: "One powerful idea per talk" },
4057
- { name: "Dead Laptop Test", description: "Present without slides" }
4058
- ]
4059
- }
4060
- },
4061
- frameworkSelector: {
4062
- byAudience: {
4063
- "board": {
4064
- primaryFramework: "Barbara Minto",
4065
- slideTypes: ["executive_summary", "data_insight"]
4066
- },
4067
- "sales": {
4068
- primaryFramework: "Nancy Duarte",
4069
- slideTypes: ["big_idea", "social_proof"]
4070
- }
4071
- },
4072
- byGoal: {
4073
- "persuade": {
4074
- primaryFramework: "Nancy Duarte",
4075
- slideTypes: ["big_idea", "star_moment"]
4076
- },
4077
- "inform": {
4078
- primaryFramework: "Barbara Minto",
4079
- slideTypes: ["bullet_points", "data_insight"]
4080
- }
4081
- }
4082
- },
4083
- automatedQA: {
4084
- scoringRubric: {
4085
- totalPoints: 100,
4086
- passingThreshold: 95,
4087
- categories: {
4088
- visual: { weight: 35, checks: {} },
4089
- content: { weight: 30, checks: {} },
4090
- expert: { weight: 25, checks: {} },
4091
- accessibility: { weight: 10, checks: {} }
4092
- }
4093
- }
4094
- },
4095
- slideTypes: {},
4096
- modes: {
4097
- keynote: { maxWords: 25, minWhitespace: 35 },
4098
- business: { maxWords: 80, minWhitespace: 25 }
4099
- }
4100
- };
4101
- }
4102
- };
4103
- var knowledgeBaseInstance = null;
4104
- function getKnowledgeBase() {
4105
- if (!knowledgeBaseInstance) {
4106
- knowledgeBaseInstance = new KnowledgeBase();
4107
- }
4108
- return knowledgeBaseInstance;
4109
- }
4110
-
4111
4530
  // src/index.ts
4112
4531
  async function generate(config) {
4113
4532
  const engine = new PresentationEngine();
@@ -4122,7 +4541,7 @@ async function validate(presentation, options) {
4122
4541
  score
4123
4542
  };
4124
4543
  }
4125
- var VERSION = "1.0.0";
4544
+ var VERSION = "6.0.0";
4126
4545
  var index_default = {
4127
4546
  generate,
4128
4547
  validate,
@@ -4135,7 +4554,7 @@ export {
4135
4554
  CompositeChartProvider,
4136
4555
  CompositeImageProvider,
4137
4556
  ContentAnalyzer,
4138
- KnowledgeBase,
4557
+ KnowledgeGateway,
4139
4558
  LocalImageProvider,
4140
4559
  MermaidProvider,
4141
4560
  PlaceholderImageProvider,
@@ -4156,7 +4575,7 @@ export {
4156
4575
  createDefaultImageProvider,
4157
4576
  index_default as default,
4158
4577
  generate,
4159
- getKnowledgeBase,
4578
+ getKnowledgeGateway,
4160
4579
  validate
4161
4580
  };
4162
4581
  /**