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.js CHANGED
@@ -34,7 +34,7 @@ __export(index_exports, {
34
34
  CompositeChartProvider: () => CompositeChartProvider,
35
35
  CompositeImageProvider: () => CompositeImageProvider,
36
36
  ContentAnalyzer: () => ContentAnalyzer,
37
- KnowledgeBase: () => KnowledgeBase,
37
+ KnowledgeGateway: () => KnowledgeGateway,
38
38
  LocalImageProvider: () => LocalImageProvider,
39
39
  MermaidProvider: () => MermaidProvider,
40
40
  PlaceholderImageProvider: () => PlaceholderImageProvider,
@@ -55,7 +55,7 @@ __export(index_exports, {
55
55
  createDefaultImageProvider: () => createDefaultImageProvider,
56
56
  default: () => index_default,
57
57
  generate: () => generate,
58
- getKnowledgeBase: () => getKnowledgeBase,
58
+ getKnowledgeGateway: () => getKnowledgeGateway,
59
59
  validate: () => validate
60
60
  });
61
61
  module.exports = __toCommonJS(index_exports);
@@ -88,8 +88,281 @@ var TemplateNotFoundError = class extends Error {
88
88
  }
89
89
  };
90
90
 
91
+ // src/kb/KnowledgeGateway.ts
92
+ var import_fs = require("fs");
93
+ var import_path = require("path");
94
+ var import_url = require("url");
95
+ var yaml = __toESM(require("yaml"));
96
+ var import_meta = {};
97
+ function getModuleDir() {
98
+ if (typeof import_meta !== "undefined" && import_meta.url) {
99
+ return (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
100
+ }
101
+ if (typeof __dirname !== "undefined") {
102
+ return __dirname;
103
+ }
104
+ return process.cwd();
105
+ }
106
+ var KnowledgeGateway = class {
107
+ kb;
108
+ loaded = false;
109
+ constructor() {
110
+ }
111
+ /**
112
+ * Load the knowledge base from YAML file
113
+ */
114
+ async load() {
115
+ if (this.loaded) return;
116
+ const moduleDir = getModuleDir();
117
+ const possiblePaths = [
118
+ (0, import_path.join)(moduleDir, "../../assets/presentation-knowledge.yaml"),
119
+ (0, import_path.join)(moduleDir, "../assets/presentation-knowledge.yaml"),
120
+ (0, import_path.join)(moduleDir, "../../../assets/presentation-knowledge.yaml"),
121
+ (0, import_path.join)(moduleDir, "assets/presentation-knowledge.yaml"),
122
+ (0, import_path.join)(process.cwd(), "assets/presentation-knowledge.yaml"),
123
+ (0, import_path.join)(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
124
+ ];
125
+ for (const path of possiblePaths) {
126
+ try {
127
+ const content = (0, import_fs.readFileSync)(path, "utf-8");
128
+ this.kb = yaml.parse(content);
129
+ this.loaded = true;
130
+ console.log(` \u2713 Knowledge base loaded from ${path}`);
131
+ return;
132
+ } catch {
133
+ }
134
+ }
135
+ throw new Error(`Knowledge base not found. Tried: ${possiblePaths.join(", ")}`);
136
+ }
137
+ /**
138
+ * Get presentation mode configuration (keynote or business)
139
+ */
140
+ getMode(mode) {
141
+ this.ensureLoaded();
142
+ return this.kb.presentation_modes[mode];
143
+ }
144
+ /**
145
+ * Determine which mode is best for a given presentation type
146
+ */
147
+ getModeForType(type) {
148
+ const keynoteTypes = ["ted_keynote", "sales_pitch", "all_hands"];
149
+ const businessTypes = ["consulting_deck", "investment_banking", "investor_pitch", "technical_presentation"];
150
+ if (keynoteTypes.includes(type)) return "keynote";
151
+ if (businessTypes.includes(type)) return "business";
152
+ return "keynote";
153
+ }
154
+ /**
155
+ * Get word limits for the specified mode
156
+ */
157
+ getWordLimits(mode) {
158
+ const modeConfig = this.getMode(mode);
159
+ const range = modeConfig.characteristics.words_per_slide;
160
+ if (mode === "keynote") {
161
+ return { min: 6, max: 15, ideal: 10 };
162
+ } else {
163
+ return { min: 40, max: 80, ideal: 60 };
164
+ }
165
+ }
166
+ /**
167
+ * Get bullet point limits
168
+ */
169
+ getBulletLimits(mode) {
170
+ if (mode === "keynote") {
171
+ return { maxBullets: 3, maxWordsPerBullet: 8 };
172
+ } else {
173
+ return { maxBullets: 5, maxWordsPerBullet: 20 };
174
+ }
175
+ }
176
+ /**
177
+ * Get whitespace percentage requirement
178
+ */
179
+ getWhitespaceRequirement(mode) {
180
+ if (mode === "keynote") return 0.4;
181
+ return 0.25;
182
+ }
183
+ /**
184
+ * Get slide templates for the specified mode
185
+ */
186
+ getSlideTemplates(mode) {
187
+ this.ensureLoaded();
188
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
189
+ return this.kb.slide_templates[key] || [];
190
+ }
191
+ /**
192
+ * Get a specific slide template by name
193
+ */
194
+ getSlideTemplate(mode, templateName) {
195
+ const templates = this.getSlideTemplates(mode);
196
+ return templates.find((t) => t.name.toLowerCase().includes(templateName.toLowerCase()));
197
+ }
198
+ /**
199
+ * Get quality metrics for grading
200
+ */
201
+ getQualityMetrics() {
202
+ this.ensureLoaded();
203
+ return this.kb.quality_metrics;
204
+ }
205
+ /**
206
+ * Get anti-patterns to avoid
207
+ */
208
+ getAntiPatterns(mode) {
209
+ this.ensureLoaded();
210
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
211
+ return this.kb.anti_patterns[key] || [];
212
+ }
213
+ /**
214
+ * Get data visualization anti-patterns
215
+ */
216
+ getDataVizAntiPatterns() {
217
+ this.ensureLoaded();
218
+ return this.kb.anti_patterns.data_visualization || [];
219
+ }
220
+ /**
221
+ * Get accessibility anti-patterns
222
+ */
223
+ getAccessibilityAntiPatterns() {
224
+ this.ensureLoaded();
225
+ return this.kb.anti_patterns.accessibility || [];
226
+ }
227
+ /**
228
+ * Get color palette by name
229
+ */
230
+ getColorPalette(name) {
231
+ this.ensureLoaded();
232
+ const palettes = this.kb.design_system.color_palettes;
233
+ return palettes[name];
234
+ }
235
+ /**
236
+ * Get recommended color palette for presentation type
237
+ */
238
+ getRecommendedPalette(type) {
239
+ this.ensureLoaded();
240
+ const palettes = this.kb.design_system.color_palettes;
241
+ switch (type) {
242
+ case "consulting_deck":
243
+ return palettes.consulting_classic;
244
+ case "investment_banking":
245
+ case "investor_pitch":
246
+ return palettes.executive_professional;
247
+ case "ted_keynote":
248
+ return palettes.dark_executive;
249
+ case "sales_pitch":
250
+ return palettes.modern_business;
251
+ case "technical_presentation":
252
+ return palettes.modern_business;
253
+ case "all_hands":
254
+ return palettes.strategy_growth;
255
+ default:
256
+ return palettes.consulting_classic;
257
+ }
258
+ }
259
+ /**
260
+ * Get typography guidelines for mode
261
+ */
262
+ getTypography(mode) {
263
+ this.ensureLoaded();
264
+ const key = mode === "keynote" ? "keynote_mode" : "business_mode";
265
+ return this.kb.design_system.typography[key];
266
+ }
267
+ /**
268
+ * Get story framework by name
269
+ */
270
+ getStoryFramework(name) {
271
+ this.ensureLoaded();
272
+ return this.kb.story_frameworks[name];
273
+ }
274
+ /**
275
+ * Get expert principles by name
276
+ */
277
+ getExpertPrinciples(expertName) {
278
+ this.ensureLoaded();
279
+ if (this.kb.experts[expertName]) {
280
+ return this.kb.experts[expertName];
281
+ }
282
+ if (this.kb.data_visualization_experts) {
283
+ for (const key of Object.keys(this.kb.data_visualization_experts)) {
284
+ if (key.includes(expertName.toLowerCase())) {
285
+ return this.kb.data_visualization_experts[key];
286
+ }
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+ /**
292
+ * Get Duarte's glance test requirements
293
+ */
294
+ getDuarteGlanceTest() {
295
+ this.ensureLoaded();
296
+ const duarte = this.kb.experts.nancy_duarte;
297
+ return {
298
+ wordLimit: duarte.core_principles.glance_test.word_limit || 25,
299
+ timeSeconds: 3
300
+ };
301
+ }
302
+ /**
303
+ * Get Miller's Law constraints
304
+ */
305
+ getMillersLaw() {
306
+ this.ensureLoaded();
307
+ return { minItems: 5, maxItems: 9 };
308
+ }
309
+ /**
310
+ * Get cognitive load constraints
311
+ */
312
+ getCognitiveLoadLimits() {
313
+ this.ensureLoaded();
314
+ return { maxItemsPerChunk: 7 };
315
+ }
316
+ /**
317
+ * Get chart selection guidance
318
+ */
319
+ getChartGuidance(purpose) {
320
+ this.ensureLoaded();
321
+ return this.kb.chart_selection_guide.by_purpose[purpose];
322
+ }
323
+ /**
324
+ * Get charts to avoid
325
+ */
326
+ getChartsToAvoid() {
327
+ this.ensureLoaded();
328
+ const knaflic = this.kb.data_visualization_experts.cole_nussbaumer_knaflic;
329
+ return knaflic.chart_guidance.avoid_these || [];
330
+ }
331
+ /**
332
+ * Get investment banking pitch book structure
333
+ */
334
+ getIBPitchBookStructure(type) {
335
+ this.ensureLoaded();
336
+ return this.kb.investment_banking?.pitch_book_types[type];
337
+ }
338
+ /**
339
+ * Check if knowledge base is loaded
340
+ */
341
+ ensureLoaded() {
342
+ if (!this.loaded) {
343
+ throw new Error("Knowledge base not loaded. Call load() first.");
344
+ }
345
+ }
346
+ /**
347
+ * Get the full knowledge base (for advanced queries)
348
+ */
349
+ getRawKB() {
350
+ this.ensureLoaded();
351
+ return this.kb;
352
+ }
353
+ };
354
+ var gatewayInstance = null;
355
+ async function getKnowledgeGateway() {
356
+ if (!gatewayInstance) {
357
+ gatewayInstance = new KnowledgeGateway();
358
+ await gatewayInstance.load();
359
+ }
360
+ return gatewayInstance;
361
+ }
362
+
91
363
  // src/core/ContentAnalyzer.ts
92
364
  var ContentAnalyzer = class {
365
+ kb;
93
366
  // Signal words for SCQA detection
94
367
  situationSignals = [
95
368
  "currently",
@@ -101,7 +374,8 @@ var ContentAnalyzer = class {
101
374
  "our",
102
375
  "the market",
103
376
  "industry",
104
- "context"
377
+ "context",
378
+ "background"
105
379
  ];
106
380
  complicationSignals = [
107
381
  "however",
@@ -119,20 +393,10 @@ var ContentAnalyzer = class {
119
393
  "yet",
120
394
  "although",
121
395
  "despite",
122
- "while"
123
- ];
124
- questionSignals = [
125
- "how",
126
- "what",
127
- "why",
128
- "when",
129
- "where",
130
- "which",
131
- "should",
132
- "could",
133
- "can we",
134
- "is it possible",
135
- "?"
396
+ "while",
397
+ "crisis",
398
+ "gap",
399
+ "broken"
136
400
  ];
137
401
  answerSignals = [
138
402
  "therefore",
@@ -148,249 +412,389 @@ var ContentAnalyzer = class {
148
412
  "we should",
149
413
  "must",
150
414
  "need to",
151
- "the answer"
152
- ];
153
- // Sparkline detection
154
- whatIsSignals = [
155
- "currently",
156
- "today",
157
- "status quo",
158
- "reality",
159
- "actual",
160
- "now",
161
- "existing",
162
- "present state",
163
- "as-is",
164
- "problem"
415
+ "the answer",
416
+ "introducing",
417
+ "presenting",
418
+ "platform"
165
419
  ];
166
- whatCouldBeSignals = [
167
- "imagine",
168
- "vision",
169
- "future",
170
- "could be",
171
- "what if",
172
- "possibility",
173
- "potential",
174
- "opportunity",
175
- "transform",
176
- "envision",
177
- "ideal",
178
- "dream",
179
- "goal",
180
- "aspiration"
420
+ // CTA signals - used to filter these OUT of answer detection
421
+ ctaSignals = [
422
+ "contact",
423
+ "call us",
424
+ "email",
425
+ "schedule",
426
+ "sign up",
427
+ "register",
428
+ "next steps",
429
+ "get started",
430
+ "reach out",
431
+ "visit",
432
+ "follow"
181
433
  ];
434
+ // Type detection signals
435
+ typeSignals = {
436
+ ted_keynote: ["inspire", "vision", "imagine", "dream", "transform", "story", "journey"],
437
+ sales_pitch: ["buy", "purchase", "pricing", "offer", "deal", "discount", "roi", "value"],
438
+ consulting_deck: ["recommend", "analysis", "assessment", "strategy", "findings", "options", "mece"],
439
+ investment_banking: ["valuation", "dcf", "multiple", "enterprise value", "ebitda", "ipo", "m&a"],
440
+ investor_pitch: ["investment", "funding", "series", "traction", "market size", "tam", "runway"],
441
+ technical_presentation: ["architecture", "api", "system", "implementation", "code", "database", "deploy"],
442
+ all_hands: ["team", "quarter", "update", "progress", "celebrate", "recognize", "company"]
443
+ };
444
+ async initialize() {
445
+ this.kb = await getKnowledgeGateway();
446
+ }
182
447
  /**
183
448
  * Analyze content and extract structural elements.
184
449
  */
185
450
  async analyze(content, contentType) {
451
+ await this.initialize();
186
452
  const text = this.parseContent(content, contentType);
187
- const paragraphs = this.splitIntoParagraphs(text);
188
- const sentences = this.splitIntoSentences(text);
189
- const scqa = this.extractSCQA(paragraphs, sentences);
190
- const sparkline = this.extractSparkline(paragraphs);
191
- const keyMessages = this.extractKeyMessages(text, sentences);
192
- const titles = this.generateActionTitles(keyMessages, paragraphs);
193
- const starMoments = this.identifyStarMoments(paragraphs);
194
- const estimatedSlideCount = this.estimateSlideCount(text, paragraphs);
453
+ const title = this.extractTitle(text);
454
+ const detectedType = this.detectPresentationType(text);
455
+ console.log(` \u2713 Detected type: ${detectedType}`);
456
+ const sections = this.extractSections(text);
457
+ console.log(` \u2713 Found ${sections.length} sections`);
458
+ const scqa = this.extractSCQA(text);
459
+ const sparkline = this.extractSparkline(text);
460
+ const keyMessages = this.extractKeyMessages(text);
461
+ const dataPoints = this.extractDataPoints(text);
462
+ console.log(` \u2713 Found ${dataPoints.length} data points`);
463
+ const titles = sections.map((s) => s.header).filter((h) => h.length > 0);
464
+ const starMoments = this.extractStarMoments(text);
465
+ const estimatedSlideCount = Math.max(5, sections.length + 3);
195
466
  return {
196
- scqa,
197
- sparkline,
467
+ detectedType,
468
+ title,
469
+ sections,
198
470
  keyMessages,
471
+ dataPoints,
472
+ scqa: {
473
+ situation: scqa.situation || "",
474
+ complication: scqa.complication || "",
475
+ question: scqa.question || "",
476
+ answer: scqa.answer || ""
477
+ },
478
+ sparkline: {
479
+ whatIs: sparkline.callToAction ? [sparkline.callToAction] : [],
480
+ whatCouldBe: keyMessages,
481
+ callToAdventure: sparkline.callToAction || ""
482
+ },
199
483
  titles,
200
484
  starMoments,
201
485
  estimatedSlideCount
202
486
  };
203
487
  }
204
488
  /**
205
- * Parse content based on its type.
489
+ * Parse content based on type
206
490
  */
207
491
  parseContent(content, contentType) {
208
- switch (contentType) {
209
- case "markdown":
210
- return this.parseMarkdown(content);
211
- case "json":
212
- return this.parseJSON(content);
213
- case "yaml":
214
- return this.parseYAML(content);
215
- case "text":
216
- default:
217
- return content;
492
+ if (contentType === "markdown" || content.includes("#") || content.includes("- ")) {
493
+ return content;
218
494
  }
495
+ return content;
219
496
  }
220
497
  /**
221
- * Parse markdown content to plain text (preserving structure hints).
222
- */
223
- parseMarkdown(content) {
224
- let text = content;
225
- text = text.replace(/^#{1,6}\s+(.+)$/gm, "\n[HEADER] $1\n");
226
- text = text.replace(/^[-*+]\s+(.+)$/gm, "[BULLET] $1");
227
- text = text.replace(/^\d+\.\s+(.+)$/gm, "[NUMBERED] $1");
228
- text = text.replace(/\*\*(.+?)\*\*/g, "[EMPHASIS] $1 [/EMPHASIS]");
229
- text = text.replace(/\*(.+?)\*/g, "$1");
230
- text = text.replace(/```[\s\S]*?```/g, "[CODE BLOCK]");
231
- text = text.replace(/`(.+?)`/g, "$1");
232
- text = text.replace(/\[(.+?)\]\(.+?\)/g, "$1");
233
- text = text.replace(/!\[.*?\]\(.+?\)/g, "[IMAGE]");
234
- return text.trim();
235
- }
236
- /**
237
- * Parse JSON content.
498
+ * Extract the main title from content
238
499
  */
239
- parseJSON(content) {
240
- try {
241
- const data = JSON.parse(content);
242
- return this.flattenObject(data);
243
- } catch {
244
- return content;
500
+ extractTitle(text) {
501
+ const h1Match = text.match(/^#\s+(.+)$/m);
502
+ if (h1Match && h1Match[1]) {
503
+ return h1Match[1].replace(/\*\*/g, "").trim();
504
+ }
505
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
506
+ if (lines.length > 0 && lines[0]) {
507
+ return lines[0].replace(/^#+\s*/, "").replace(/\*\*/g, "").trim().slice(0, 80);
245
508
  }
509
+ return "Presentation";
246
510
  }
247
511
  /**
248
- * Parse YAML content.
512
+ * Detect presentation type from content signals
249
513
  */
250
- parseYAML(content) {
251
- const lines = content.split("\n");
252
- const values = [];
253
- for (const line of lines) {
254
- const match = line.match(/^[\s-]*(?:\w+:\s*)?(.+)$/);
255
- if (match?.[1] && !match[1].includes(":")) {
256
- values.push(match[1].trim());
514
+ detectPresentationType(text) {
515
+ const lowerText = text.toLowerCase();
516
+ const scores = {
517
+ ted_keynote: 0,
518
+ sales_pitch: 0,
519
+ consulting_deck: 0,
520
+ investment_banking: 0,
521
+ investor_pitch: 0,
522
+ technical_presentation: 0,
523
+ all_hands: 0
524
+ };
525
+ for (const [type, signals] of Object.entries(this.typeSignals)) {
526
+ for (const signal of signals) {
527
+ if (lowerText.includes(signal)) {
528
+ scores[type]++;
529
+ }
257
530
  }
258
531
  }
259
- return values.join("\n");
532
+ let maxScore = 0;
533
+ let detectedType = "consulting_deck";
534
+ for (const [type, score] of Object.entries(scores)) {
535
+ if (score > maxScore) {
536
+ maxScore = score;
537
+ detectedType = type;
538
+ }
539
+ }
540
+ return detectedType;
260
541
  }
261
542
  /**
262
- * Flatten object to text.
543
+ * Extract sections from content (headers, bullets, content)
263
544
  */
264
- flattenObject(obj, prefix = "") {
265
- const parts = [];
266
- if (typeof obj === "string") {
267
- return obj;
268
- }
269
- if (Array.isArray(obj)) {
270
- for (const item of obj) {
271
- parts.push(this.flattenObject(item, prefix));
545
+ extractSections(text) {
546
+ const sections = [];
547
+ const lines = text.split("\n");
548
+ let currentSection = null;
549
+ let contentLines = [];
550
+ for (const line of lines) {
551
+ const trimmedLine = line.trim();
552
+ const headerMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/);
553
+ if (headerMatch) {
554
+ if (currentSection) {
555
+ currentSection.content = contentLines.join("\n").trim();
556
+ if (currentSection.header || currentSection.content || currentSection.bullets.length > 0) {
557
+ sections.push(currentSection);
558
+ }
559
+ }
560
+ currentSection = {
561
+ header: (headerMatch[2] || "").replace(/\*\*/g, "").trim(),
562
+ level: (headerMatch[1] || "").length,
563
+ content: "",
564
+ bullets: [],
565
+ metrics: []
566
+ };
567
+ contentLines = [];
568
+ continue;
569
+ }
570
+ const bulletMatch = trimmedLine.match(/^[-*+]\s+(.+)$/);
571
+ if (bulletMatch && bulletMatch[1] && currentSection) {
572
+ currentSection.bullets.push(bulletMatch[1]);
573
+ continue;
574
+ }
575
+ const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/);
576
+ if (numberedMatch && numberedMatch[1] && currentSection) {
577
+ currentSection.bullets.push(numberedMatch[1]);
578
+ continue;
579
+ }
580
+ const metricMatch = trimmedLine.match(/\$?([\d,]+\.?\d*)[%]?\s*[-–—:]\s*(.+)/);
581
+ if (metricMatch && metricMatch[1] && metricMatch[2] && currentSection) {
582
+ currentSection.metrics.push({
583
+ value: metricMatch[1],
584
+ label: metricMatch[2].slice(0, 50)
585
+ });
586
+ continue;
272
587
  }
273
- } else if (typeof obj === "object" && obj !== null) {
274
- for (const [key, value] of Object.entries(obj)) {
275
- const newPrefix = prefix ? `${prefix}.${key}` : key;
276
- parts.push(this.flattenObject(value, newPrefix));
588
+ if (trimmedLine.includes("|") && currentSection) {
589
+ const cells = trimmedLine.split("|").map((c) => c.trim()).filter((c) => c && !c.match(/^-+$/));
590
+ if (cells.length >= 2) {
591
+ const value = cells.find((c) => c.match(/^\$?[\d,]+\.?\d*[%]?$/));
592
+ const label = cells.find((c) => !c.match(/^\$?[\d,]+\.?\d*[%]?$/) && c.length > 2);
593
+ if (value && label) {
594
+ currentSection.metrics.push({ value, label: label.slice(0, 50) });
595
+ }
596
+ }
597
+ continue;
598
+ }
599
+ if (trimmedLine && currentSection) {
600
+ contentLines.push(trimmedLine);
277
601
  }
278
- } else if (obj !== null && obj !== void 0) {
279
- parts.push(String(obj));
280
602
  }
281
- return parts.join("\n");
282
- }
283
- /**
284
- * Split text into paragraphs.
285
- */
286
- splitIntoParagraphs(text) {
287
- return text.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
288
- }
289
- /**
290
- * Split text into sentences.
291
- */
292
- splitIntoSentences(text) {
293
- 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");
294
- return cleaned.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 10);
603
+ if (currentSection) {
604
+ currentSection.content = contentLines.join("\n").trim();
605
+ if (currentSection.header || currentSection.content || currentSection.bullets.length > 0) {
606
+ sections.push(currentSection);
607
+ }
608
+ }
609
+ return sections;
295
610
  }
296
611
  /**
297
- * Extract SCQA structure (Barbara Minto's Pyramid Principle).
612
+ * Extract SCQA structure (Barbara Minto)
298
613
  */
299
- extractSCQA(paragraphs, sentences) {
614
+ extractSCQA(text) {
615
+ const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
300
616
  let situation = "";
301
617
  let complication = "";
302
618
  let question = "";
303
619
  let answer = "";
304
620
  for (const para of paragraphs.slice(0, 3)) {
305
- if (this.containsSignals(para, this.situationSignals)) {
306
- situation = this.extractRelevantSentence(para, this.situationSignals);
621
+ if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
622
+ situation = this.extractFirstSentence(para);
307
623
  break;
308
624
  }
309
625
  }
310
626
  for (const para of paragraphs) {
311
- if (this.containsSignals(para, this.complicationSignals)) {
312
- complication = this.extractRelevantSentence(para, this.complicationSignals);
627
+ if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
628
+ complication = this.extractFirstSentence(para);
313
629
  break;
314
630
  }
315
631
  }
316
- for (const sentence of sentences) {
317
- if (sentence.includes("?") || this.containsSignals(sentence, this.questionSignals)) {
318
- question = sentence;
632
+ const middleStart = Math.floor(paragraphs.length * 0.2);
633
+ const middleEnd = Math.floor(paragraphs.length * 0.8);
634
+ for (const para of paragraphs.slice(middleStart, middleEnd)) {
635
+ const lowerPara = para.toLowerCase();
636
+ if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
637
+ answer = this.extractFirstSentence(para);
319
638
  break;
320
639
  }
321
640
  }
322
- for (const para of paragraphs.slice(-3)) {
323
- if (this.containsSignals(para, this.answerSignals)) {
324
- answer = this.extractRelevantSentence(para, this.answerSignals);
325
- break;
326
- }
641
+ if (!situation && paragraphs.length > 0 && paragraphs[0]) {
642
+ situation = this.extractFirstSentence(paragraphs[0]);
327
643
  }
328
- if (!situation && paragraphs.length > 0) {
329
- situation = this.truncateToSentence(paragraphs[0] ?? "", 150);
644
+ if (!answer && paragraphs.length > 2) {
645
+ for (const para of paragraphs.slice(1, Math.floor(paragraphs.length * 0.5))) {
646
+ const lowerPara = para.toLowerCase();
647
+ if (lowerPara.includes("recommend") || lowerPara.includes("strategy") || lowerPara.includes("solution") || lowerPara.includes("approach")) {
648
+ answer = this.extractFirstSentence(para);
649
+ break;
650
+ }
651
+ }
330
652
  }
331
- if (!answer && paragraphs.length > 1) {
332
- const lastPara = paragraphs[paragraphs.length - 1];
333
- answer = lastPara ? this.truncateToSentence(lastPara, 150) : "";
653
+ if (complication && !question) {
654
+ question = `How do we address: ${complication.slice(0, 80)}?`;
334
655
  }
335
656
  return { situation, complication, question, answer };
336
657
  }
337
658
  /**
338
- * Extract Sparkline structure (Nancy Duarte).
659
+ * Extract STAR moments (Something They'll Always Remember)
660
+ * Per Nancy Duarte: These should be memorable, COMPLETE thoughts with impact
661
+ *
662
+ * CRITICAL REQUIREMENTS:
663
+ * - Must be complete sentences with subject + verb structure
664
+ * - Must have 5+ words minimum (no fragments!)
665
+ * - Must be 30+ characters
666
+ * - Must contain a verb
667
+ * - NOT fragments like "significant growth" or "cloud-first strategy"
668
+ * - NOT headers or topic labels
339
669
  */
340
- extractSparkline(paragraphs) {
341
- const whatIs = [];
342
- const whatCouldBe = [];
343
- let callToAdventure = "";
344
- for (const para of paragraphs) {
345
- const lowerPara = para.toLowerCase();
346
- if (this.containsSignals(lowerPara, this.whatIsSignals)) {
347
- whatIs.push(this.truncateToSentence(para, 100));
670
+ extractStarMoments(text) {
671
+ const starMoments = [];
672
+ const usedPhrases = /* @__PURE__ */ new Set();
673
+ const fragmentPatterns = [
674
+ /^[a-z]+-[a-z]+\s+(strategy|approach|solution|platform|architecture)$/i,
675
+ // "cloud-first strategy"
676
+ /^(significant|substantial|dramatic|rapid|strong)\s+(growth|increase|decline|change)$/i,
677
+ // "significant growth"
678
+ /^(digital|cloud|data|ai)\s+(transformation|strategy|platform|solution)$/i,
679
+ // "digital transformation"
680
+ /^(our|your|the|a)\s+\w+\s*$/i,
681
+ // "our solution", "the platform"
682
+ /^[a-z]+\s+[a-z]+$/i
683
+ // Any two-word phrase
684
+ ];
685
+ 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;
686
+ const boldMatches = text.match(/\*\*([^*]+)\*\*/g);
687
+ if (boldMatches) {
688
+ for (const match of boldMatches) {
689
+ const cleaned = match.replace(/\*\*/g, "").trim();
690
+ const lowerCleaned = cleaned.toLowerCase();
691
+ const wordCount = cleaned.split(/\s+/).length;
692
+ const normalized = cleaned.toLowerCase().slice(0, 30);
693
+ const hasVerb = verbPatterns.test(cleaned);
694
+ const isLongEnough = wordCount >= 5 && cleaned.length >= 30;
695
+ const isNotFragment = !fragmentPatterns.some((p) => p.test(cleaned));
696
+ const isNotHeader = !/^(The |Our |Your |Next |Key |Overview|Introduction|Conclusion|Summary|Background)/i.test(cleaned);
697
+ const isNotCTA = !this.containsSignals(lowerCleaned, this.ctaSignals);
698
+ const hasMetricContext = /\d+[%xX]|\$[\d,]+/.test(cleaned) && wordCount >= 5;
699
+ if ((hasVerb || hasMetricContext) && isLongEnough && isNotFragment && isNotHeader && isNotCTA && wordCount <= 25 && !usedPhrases.has(normalized)) {
700
+ starMoments.push(cleaned);
701
+ usedPhrases.add(normalized);
702
+ if (starMoments.length >= 3) break;
703
+ }
348
704
  }
349
- if (this.containsSignals(lowerPara, this.whatCouldBeSignals)) {
350
- whatCouldBe.push(this.truncateToSentence(para, 100));
705
+ }
706
+ if (starMoments.length < 3) {
707
+ const cleanedText = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
708
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g) || [];
709
+ for (const sentence of sentences) {
710
+ const cleaned = sentence.trim();
711
+ const normalized = cleaned.toLowerCase().slice(0, 30);
712
+ const wordCount = cleaned.split(/\s+/).length;
713
+ const hasHighImpact = /(\d{2,3}%|\d+x|billion|million|\$[\d,]+\s*(million|billion)?)/i.test(cleaned);
714
+ const hasVerb = verbPatterns.test(cleaned);
715
+ if (hasHighImpact && hasVerb && wordCount >= 6 && wordCount <= 25 && cleaned.length >= 40 && cleaned.length <= 200 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
716
+ starMoments.push(cleaned);
717
+ usedPhrases.add(normalized);
718
+ if (starMoments.length >= 3) break;
719
+ }
351
720
  }
352
721
  }
353
- for (const para of paragraphs.slice(-2)) {
354
- if (this.containsSignals(para.toLowerCase(), ["join", "together", "action", "start", "begin", "now"])) {
355
- callToAdventure = this.truncateToSentence(para, 150);
722
+ return starMoments.slice(0, 3);
723
+ }
724
+ /**
725
+ * Extract Sparkline elements (Nancy Duarte)
726
+ */
727
+ extractSparkline(text) {
728
+ const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
729
+ let callToAction = "";
730
+ const ctaSignals = ["next steps", "call to action", "take action", "start now", "join us", "contact"];
731
+ for (const para of paragraphs.slice(-3)) {
732
+ if (this.containsSignals(para.toLowerCase(), ctaSignals)) {
733
+ callToAction = this.extractFirstSentence(para);
356
734
  break;
357
735
  }
358
736
  }
359
- return { whatIs, whatCouldBe, callToAdventure };
737
+ return { callToAction };
360
738
  }
361
739
  /**
362
- * Extract key messages (max 3 - Rule of Three).
740
+ * Extract key messages - the actual insights from the content
741
+ * Per Carmine Gallo: Rule of Three - max 3 key messages
742
+ * Per Barbara Minto: Messages should be actionable conclusions, not topics
363
743
  */
364
- extractKeyMessages(text, sentences) {
744
+ extractKeyMessages(text) {
365
745
  const messages = [];
366
- const emphasisMatches = text.match(/\[EMPHASIS\](.+?)\[\/EMPHASIS\]/g);
367
- if (emphasisMatches) {
368
- for (const match of emphasisMatches.slice(0, 3)) {
369
- const content = match.replace(/\[EMPHASIS\]|\[\/EMPHASIS\]/g, "").trim();
370
- if (content.length > 10 && content.length < 100) {
371
- messages.push(content);
372
- }
373
- }
374
- }
375
- const headerMatches = text.match(/\[HEADER\](.+)/g);
376
- if (headerMatches && messages.length < 3) {
377
- for (const match of headerMatches.slice(0, 3 - messages.length)) {
378
- const content = match.replace("[HEADER]", "").trim();
379
- if (content.length > 5 && content.length < 80) {
380
- messages.push(content);
746
+ const usedPhrases = /* @__PURE__ */ new Set();
747
+ let cleanText = text.replace(/^#+\s+.+$/gm, "").replace(/\[.+?\]/g, "").replace(/\*\*/g, "");
748
+ cleanText = cleanText.replace(/(\d)\.(\d)/g, "$1<DECIMAL>$2");
749
+ const rawSentences = cleanText.match(/[^.!?]+[.!?]/g) || [];
750
+ const sentences = rawSentences.map((s) => s.replace(/<DECIMAL>/g, "."));
751
+ const insightSignals = [
752
+ "we recommend",
753
+ "we suggest",
754
+ "our strategy",
755
+ "the solution",
756
+ "key finding",
757
+ "result",
758
+ "achieve",
759
+ "improve",
760
+ "increase",
761
+ "decrease",
762
+ "expect",
763
+ "roi",
764
+ "benefit",
765
+ "opportunity",
766
+ "transform"
767
+ ];
768
+ for (const sentence of sentences) {
769
+ const cleaned = sentence.trim();
770
+ const normalized = cleaned.toLowerCase().slice(0, 30);
771
+ if (cleaned.length > 30 && cleaned.length < 150 && !usedPhrases.has(normalized) && !this.containsSignals(cleaned.toLowerCase(), this.ctaSignals)) {
772
+ if (this.containsSignals(cleaned.toLowerCase(), insightSignals)) {
773
+ messages.push(cleaned);
774
+ usedPhrases.add(normalized);
775
+ if (messages.length >= 3) break;
381
776
  }
382
777
  }
383
778
  }
384
779
  if (messages.length < 3) {
385
- const strongSentences = sentences.filter((s) => s.length > 20 && s.length < 100).filter((s) => this.containsSignals(s.toLowerCase(), ["key", "important", "critical", "essential", "must", "need"]));
386
- for (const sentence of strongSentences.slice(0, 3 - messages.length)) {
387
- messages.push(sentence);
780
+ for (const sentence of sentences) {
781
+ const cleaned = sentence.trim();
782
+ const normalized = cleaned.toLowerCase().slice(0, 30);
783
+ if (cleaned.length > 25 && cleaned.length < 150 && !usedPhrases.has(normalized) && /\d+[%xX]|\$[\d,]+/.test(cleaned)) {
784
+ messages.push(cleaned);
785
+ usedPhrases.add(normalized);
786
+ if (messages.length >= 3) break;
787
+ }
388
788
  }
389
789
  }
390
- if (messages.length < 3) {
790
+ if (messages.length < 2) {
391
791
  for (const sentence of sentences) {
392
- if (sentence.length > 30 && sentence.length < 100 && !messages.includes(sentence)) {
393
- messages.push(sentence);
792
+ const cleaned = sentence.trim();
793
+ const wordCount = cleaned.split(/\s+/).length;
794
+ const normalized = cleaned.toLowerCase().slice(0, 30);
795
+ if (wordCount >= 6 && wordCount <= 25 && !usedPhrases.has(normalized) && /\b(is|are|will|can|should|must|has|have|provides?|enables?|allows?)\b/.test(cleaned.toLowerCase())) {
796
+ messages.push(cleaned);
797
+ usedPhrases.add(normalized);
394
798
  if (messages.length >= 3) break;
395
799
  }
396
800
  }
@@ -398,195 +802,253 @@ var ContentAnalyzer = class {
398
802
  return messages.slice(0, 3);
399
803
  }
400
804
  /**
401
- * Generate action titles (McKinsey-style).
805
+ * Extract data points (metrics with values)
806
+ * IMPROVED: Smarter label extraction that understands markdown tables
402
807
  */
403
- generateActionTitles(keyMessages, paragraphs) {
404
- const titles = [];
405
- for (const message of keyMessages) {
406
- const actionTitle = this.transformToActionTitle(message);
407
- if (actionTitle) {
408
- titles.push(actionTitle);
808
+ extractDataPoints(text) {
809
+ const dataPoints = [];
810
+ const usedValues = /* @__PURE__ */ new Set();
811
+ const tableLines = text.split("\n").filter((line) => line.includes("|"));
812
+ if (tableLines.length >= 3) {
813
+ const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
814
+ if (dataRows.length >= 2) {
815
+ for (const row of dataRows.slice(1)) {
816
+ const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
817
+ if (cells.length >= 2) {
818
+ const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
819
+ const labelCell = cells[0];
820
+ if (valueCell && labelCell && !usedValues.has(valueCell)) {
821
+ usedValues.add(valueCell);
822
+ dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
823
+ }
824
+ }
825
+ }
409
826
  }
410
827
  }
411
- for (const para of paragraphs) {
412
- if (titles.length >= 10) break;
413
- const headerMatch = para.match(/\[HEADER\]\s*(.+)/);
414
- if (headerMatch?.[1]) {
415
- const title = this.transformToActionTitle(headerMatch[1]);
416
- if (title && !titles.includes(title)) {
417
- titles.push(title);
828
+ const lines = text.split("\n");
829
+ for (const line of lines) {
830
+ if (line.includes("|")) continue;
831
+ const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
832
+ if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
833
+ usedValues.add(percentMatch[2]);
834
+ dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
835
+ }
836
+ const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
837
+ if (dollarMatch && dataPoints.length < 6) {
838
+ const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
839
+ if (!usedValues.has(fullValue)) {
840
+ usedValues.add(fullValue);
841
+ const label = this.extractLabelFromLine(line, fullValue);
842
+ dataPoints.push({ value: fullValue, label });
418
843
  }
419
844
  }
420
845
  }
421
- return titles;
846
+ return dataPoints.slice(0, 4);
422
847
  }
423
848
  /**
424
- * Transform a statement into an action title.
849
+ * Extract a meaningful label from a line containing a metric
425
850
  */
426
- transformToActionTitle(statement) {
427
- let title = statement.replace(/\[(HEADER|EMPHASIS|BULLET|NUMBERED)\]/g, "").trim();
428
- const actionVerbs = ["increase", "decrease", "improve", "reduce", "achieve", "deliver", "create", "build", "launch", "transform", "enable", "drive"];
429
- const firstWord = title.split(" ")[0]?.toLowerCase();
430
- if (firstWord && actionVerbs.includes(firstWord)) {
431
- return this.capitalizeFirst(title);
432
- }
433
- if (title.toLowerCase().includes("should")) {
434
- title = title.replace(/we should|you should|should/gi, "").trim();
435
- return this.capitalizeFirst(title);
436
- }
437
- if (title.toLowerCase().includes("need to")) {
438
- title = title.replace(/we need to|you need to|need to/gi, "").trim();
439
- return this.capitalizeFirst(title);
440
- }
441
- if (title.length < 50) {
442
- return title;
443
- }
444
- return this.truncateToWords(title, 8);
851
+ extractLabelFromLine(line, value) {
852
+ const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
853
+ const beforeValue = cleaned.split(value)[0] || "";
854
+ const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
855
+ return words.slice(-4).join(" ").slice(0, 40) || "Value";
445
856
  }
446
857
  /**
447
- * Identify STAR moments (Something They'll Always Remember).
858
+ * Check if text contains any of the signals
448
859
  */
449
- identifyStarMoments(paragraphs) {
450
- const starMoments = [];
451
- const starSignals = [
452
- "surprising",
453
- "amazing",
454
- "incredible",
455
- "remarkable",
456
- "stunning",
457
- "imagine",
458
- "what if",
459
- "breakthrough",
460
- "revolutionary",
461
- "never before",
462
- "first time",
463
- "unprecedented",
464
- "game-changing",
465
- "dramatic"
466
- ];
467
- for (const para of paragraphs) {
468
- if (this.containsSignals(para.toLowerCase(), starSignals)) {
469
- const moment = this.truncateToSentence(para, 120);
470
- if (moment.length > 20) {
471
- starMoments.push(moment);
472
- }
473
- }
474
- }
475
- const statPattern = /\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion|trillion))?|\d+(?:\s*(?:million|billion|trillion))/g;
476
- for (const para of paragraphs) {
477
- if (statPattern.test(para) && starMoments.length < 5) {
478
- const moment = this.truncateToSentence(para, 100);
479
- if (!starMoments.includes(moment)) {
480
- starMoments.push(moment);
481
- }
482
- }
483
- }
484
- return starMoments.slice(0, 5);
860
+ containsSignals(text, signals) {
861
+ return signals.some((signal) => text.includes(signal));
485
862
  }
486
863
  /**
487
- * Estimate slide count based on content.
864
+ * Extract first meaningful sentence from text
865
+ * CRITICAL: Must strip headers, CTA text, and fragments
488
866
  */
489
- estimateSlideCount(text, paragraphs) {
490
- const wordCount = text.split(/\s+/).length;
491
- const headerCount = (text.match(/\[HEADER\]/g) ?? []).length;
492
- const bulletGroups = (text.match(/\[BULLET\]/g) ?? []).length / 4;
493
- const wordBasedEstimate = Math.ceil(wordCount / 35);
494
- const estimate = Math.max(
495
- 5,
496
- // Minimum 5 slides
497
- Math.ceil((wordBasedEstimate + headerCount + bulletGroups) / 2)
498
- );
499
- return Math.min(estimate, 30);
500
- }
501
- // === Helper Methods ===
502
- containsSignals(text, signals) {
503
- const lowerText = text.toLowerCase();
504
- return signals.some((signal) => lowerText.includes(signal));
505
- }
506
- extractRelevantSentence(paragraph, signals) {
507
- const sentences = paragraph.split(/[.!?]+/);
867
+ extractFirstSentence(text) {
868
+ let cleaned = text.replace(/^#+\s+.+$/gm, "").replace(/\*\*/g, "").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\n{2,}/g, "\n").trim();
869
+ cleaned = cleaned.replace(/^(Overview|Introduction|The Problem|Our Solution|Next Steps|Conclusion|Summary|Background|Context|Recommendation)\s*:?\s*/i, "").trim();
870
+ const sentences = cleaned.match(/[^.!?]+[.!?]/g) || [];
508
871
  for (const sentence of sentences) {
509
- if (this.containsSignals(sentence.toLowerCase(), signals)) {
510
- return sentence.trim();
872
+ const trimmed = sentence.trim();
873
+ 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)) {
874
+ return trimmed.slice(0, 150);
511
875
  }
512
876
  }
513
- return this.truncateToSentence(paragraph, 150);
514
- }
515
- truncateToSentence(text, maxLength) {
516
- if (text.length <= maxLength) {
517
- return text.trim();
518
- }
519
- const truncated = text.slice(0, maxLength);
520
- const lastPeriod = truncated.lastIndexOf(".");
521
- const lastQuestion = truncated.lastIndexOf("?");
522
- const lastExclaim = truncated.lastIndexOf("!");
523
- const lastBoundary = Math.max(lastPeriod, lastQuestion, lastExclaim);
524
- if (lastBoundary > maxLength * 0.5) {
525
- return text.slice(0, lastBoundary + 1).trim();
526
- }
527
- return truncated.trim() + "...";
528
- }
529
- truncateToWords(text, maxWords) {
530
- const words = text.split(/\s+/);
531
- if (words.length <= maxWords) {
532
- return text;
533
- }
534
- return words.slice(0, maxWords).join(" ");
535
- }
536
- capitalizeFirst(text) {
537
- if (!text) return "";
538
- return text.charAt(0).toUpperCase() + text.slice(1);
877
+ const fallback = cleaned.slice(0, 150);
878
+ return fallback.length >= 20 ? fallback : "";
539
879
  }
540
880
  };
541
881
 
542
882
  // src/core/SlideFactory.ts
543
883
  var SlideFactory = class {
544
884
  templates;
885
+ usedContent = /* @__PURE__ */ new Set();
545
886
  constructor() {
546
887
  this.templates = this.initializeTemplates();
547
888
  }
889
+ /**
890
+ * Check if content has already been used (deduplication)
891
+ */
892
+ isContentUsed(content) {
893
+ if (!content) return true;
894
+ const normalized = content.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 50);
895
+ if (this.usedContent.has(normalized)) {
896
+ return true;
897
+ }
898
+ this.usedContent.add(normalized);
899
+ return false;
900
+ }
548
901
  /**
549
902
  * Create slides from analyzed content.
903
+ *
904
+ * ARCHITECTURE (per KB expert methodologies):
905
+ * 1. Title slide - always first
906
+ * 2. Agenda slide - business mode only with 3+ sections
907
+ * 3. SCQA slides - Situation, Complication (per Minto)
908
+ * 4. Content slides - from sections with bullets/content
909
+ * 5. Metrics slide - if data points exist
910
+ * 6. Solution slide - SCQA answer
911
+ * 7. STAR moments - only if high-quality (per Duarte)
912
+ * 8. CTA slide - if call to action exists
913
+ * 9. Thank you slide - always last
550
914
  */
551
915
  async createSlides(analysis, mode) {
552
916
  const slides = [];
553
917
  let slideIndex = 0;
918
+ this.usedContent.clear();
554
919
  slides.push(this.createTitleSlide(slideIndex++, analysis));
555
- if (mode === "business" && analysis.keyMessages.length >= 2) {
920
+ this.isContentUsed(analysis.titles[0] ?? "");
921
+ const substantiveSections = analysis.sections.filter(
922
+ (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
923
+ );
924
+ if (mode === "business" && substantiveSections.length >= 3) {
556
925
  slides.push(this.createAgendaSlide(slideIndex++, analysis));
557
926
  }
558
- if (analysis.scqa.situation) {
927
+ if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
559
928
  slides.push(this.createContextSlide(slideIndex++, analysis, mode));
560
929
  }
561
- if (analysis.scqa.complication) {
930
+ if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
562
931
  slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
563
932
  }
933
+ for (const section of substantiveSections.slice(0, 4)) {
934
+ const headerUsed = this.isContentUsed(section.header);
935
+ const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
936
+ if (!headerUsed && !contentUsed) {
937
+ if (section.bullets.length > 0) {
938
+ slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
939
+ } else if (section.content.length > 50) {
940
+ slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
941
+ }
942
+ }
943
+ }
564
944
  for (const message of analysis.keyMessages) {
565
- slides.push(this.createMessageSlide(slideIndex++, message, mode));
945
+ const wordCount = message.split(/\s+/).length;
946
+ if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
947
+ slides.push(this.createMessageSlide(slideIndex++, message, mode));
948
+ }
566
949
  }
567
- for (const starMoment of analysis.starMoments.slice(0, 2)) {
568
- slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
950
+ if (analysis.dataPoints.length >= 2) {
951
+ slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
569
952
  }
570
- if (analysis.scqa.answer) {
953
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
571
954
  slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
572
955
  }
573
- if (analysis.sparkline.callToAdventure) {
956
+ const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
957
+ for (const starMoment of analysis.starMoments.slice(0, 2)) {
958
+ const wordCount = starMoment.split(/\s+/).length;
959
+ const hasVerb = verbPattern.test(starMoment);
960
+ if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
961
+ slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
962
+ }
963
+ }
964
+ if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
574
965
  slides.push(this.createCTASlide(slideIndex++, analysis, mode));
575
966
  }
576
967
  slides.push(this.createThankYouSlide(slideIndex++));
577
968
  return slides;
578
969
  }
970
+ /**
971
+ * Create a slide from a section with bullets
972
+ */
973
+ createSectionBulletSlide(index, section, mode) {
974
+ const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
975
+ return {
976
+ index,
977
+ type: "bullet-points",
978
+ data: {
979
+ title: this.truncate(section.header, 60),
980
+ bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
981
+ },
982
+ classes: ["slide-bullet-points"]
983
+ };
984
+ }
985
+ /**
986
+ * Create a slide from a section with body content
987
+ */
988
+ createSectionContentSlide(index, section, mode) {
989
+ if (mode === "keynote") {
990
+ return {
991
+ index,
992
+ type: "single-statement",
993
+ data: {
994
+ title: this.truncate(this.extractFirstSentence(section.content), 80),
995
+ keyMessage: section.header
996
+ },
997
+ classes: ["slide-single-statement"]
998
+ };
999
+ }
1000
+ return {
1001
+ index,
1002
+ type: "two-column",
1003
+ data: {
1004
+ title: this.truncate(section.header, 60),
1005
+ body: this.truncate(section.content, 200)
1006
+ },
1007
+ classes: ["slide-two-column"]
1008
+ };
1009
+ }
1010
+ /**
1011
+ * Extract first sentence from text
1012
+ */
1013
+ extractFirstSentence(text) {
1014
+ const cleaned = this.cleanText(text);
1015
+ const match = cleaned.match(/^[^.!?]+[.!?]/);
1016
+ return match ? match[0].trim() : cleaned.slice(0, 100);
1017
+ }
1018
+ /**
1019
+ * Create a metrics slide from data points
1020
+ */
1021
+ createMetricsSlide(index, dataPoints) {
1022
+ const metrics = dataPoints.slice(0, 4).map((dp) => ({
1023
+ value: dp.value,
1024
+ label: this.cleanText(dp.label).slice(0, 40)
1025
+ }));
1026
+ return {
1027
+ index,
1028
+ type: "metrics-grid",
1029
+ data: {
1030
+ title: "Key Metrics",
1031
+ metrics
1032
+ },
1033
+ classes: ["slide-metrics-grid"]
1034
+ };
1035
+ }
579
1036
  /**
580
1037
  * Create a title slide.
581
1038
  */
582
1039
  createTitleSlide(index, analysis) {
583
- const subtitle = analysis.keyMessages[0] ?? analysis.scqa.answer ?? "";
1040
+ let subtitle = "";
1041
+ if (analysis.scqa.answer && analysis.scqa.answer.length > 10) {
1042
+ subtitle = analysis.scqa.answer;
1043
+ } else if (analysis.starMoments.length > 0 && analysis.starMoments[0]) {
1044
+ subtitle = analysis.starMoments[0];
1045
+ }
584
1046
  return {
585
1047
  index,
586
1048
  type: "title",
587
1049
  data: {
588
1050
  title: analysis.titles[0] ?? "Presentation",
589
- subtitle: this.truncate(subtitle, 60),
1051
+ subtitle: this.truncate(subtitle, 80),
590
1052
  keyMessage: analysis.scqa.answer
591
1053
  },
592
1054
  classes: ["slide-title"]
@@ -867,23 +1329,43 @@ var SlideFactory = class {
867
1329
  }
868
1330
  // === Helper Methods ===
869
1331
  /**
870
- * Clean text by removing all content markers.
1332
+ * Clean text by removing all markdown and content markers.
1333
+ * CRITICAL: Must strip all formatting to prevent garbage in slides
871
1334
  */
872
1335
  cleanText(text) {
873
1336
  if (!text) return "";
874
- 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();
1337
+ 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();
875
1338
  }
876
1339
  /**
877
- * Truncate text to max length at word boundary.
1340
+ * Truncate text to max length at sentence boundary when possible.
1341
+ * CRITICAL: Never cut mid-number (99.5% should not become 99.)
878
1342
  */
879
1343
  truncate(text, maxLength) {
880
1344
  const cleanedText = this.cleanText(text);
881
1345
  if (!cleanedText || cleanedText.length <= maxLength) {
882
1346
  return cleanedText;
883
1347
  }
1348
+ const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1349
+ if (sentences) {
1350
+ let result = "";
1351
+ for (const sentence of sentences) {
1352
+ if ((result + sentence).length <= maxLength) {
1353
+ result += sentence;
1354
+ } else {
1355
+ break;
1356
+ }
1357
+ }
1358
+ if (result.length > maxLength * 0.5) {
1359
+ return result.trim();
1360
+ }
1361
+ }
884
1362
  const truncated = cleanedText.slice(0, maxLength);
885
- const lastSpace = truncated.lastIndexOf(" ");
886
- if (lastSpace > maxLength * 0.7) {
1363
+ let lastSpace = truncated.lastIndexOf(" ");
1364
+ const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1365
+ if (/^[\d.,%$]+/.test(afterCut)) {
1366
+ lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
1367
+ }
1368
+ if (lastSpace > maxLength * 0.5) {
887
1369
  return truncated.slice(0, lastSpace) + "...";
888
1370
  }
889
1371
  return truncated + "...";
@@ -914,10 +1396,12 @@ var SlideFactory = class {
914
1396
  return sentences.slice(0, 5).map((s) => s.trim());
915
1397
  }
916
1398
  /**
917
- * Remove a statistic from text.
1399
+ * Remove a statistic from text and clean thoroughly.
918
1400
  */
919
1401
  removeStatistic(text, stat) {
920
- return text.replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1402
+ const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1403
+ const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1404
+ return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
921
1405
  }
922
1406
  };
923
1407
 
@@ -1817,19 +2301,28 @@ var ScoreCalculator = class {
1817
2301
  var import_playwright = require("playwright");
1818
2302
  var QAEngine = class {
1819
2303
  browser = null;
2304
+ kb;
1820
2305
  /**
1821
- * Validate a presentation.
2306
+ * Initialize KnowledgeGateway
2307
+ */
2308
+ async initialize() {
2309
+ this.kb = await getKnowledgeGateway();
2310
+ }
2311
+ /**
2312
+ * Validate a presentation using KB-driven quality metrics
1822
2313
  */
1823
2314
  async validate(presentation, options) {
2315
+ await this.initialize();
1824
2316
  const html = typeof presentation === "string" ? presentation : presentation.toString("utf-8");
1825
2317
  const mode = options?.mode ?? "keynote";
2318
+ const qualityMetrics = this.kb.getQualityMetrics();
1826
2319
  await this.initBrowser();
1827
2320
  try {
1828
2321
  const [visualResults, contentResults, expertResults, accessibilityResults] = await Promise.all([
1829
- this.runVisualTests(html, mode),
1830
- this.runContentTests(html, mode),
2322
+ this.runVisualTests(html, mode, qualityMetrics),
2323
+ this.runContentTests(html, mode, qualityMetrics),
1831
2324
  this.runExpertTests(html, mode),
1832
- this.runAccessibilityTests(html)
2325
+ this.runAccessibilityTests(html, qualityMetrics)
1833
2326
  ]);
1834
2327
  const issues = this.collectIssues(visualResults, contentResults, expertResults, accessibilityResults);
1835
2328
  const errorCount = issues.filter((i) => i.severity === "error").length;
@@ -1847,18 +2340,18 @@ var QAEngine = class {
1847
2340
  }
1848
2341
  }
1849
2342
  /**
1850
- * Calculate overall QA score.
2343
+ * Calculate overall QA score using KB-driven weights
1851
2344
  */
1852
2345
  calculateScore(results) {
1853
2346
  const weights = {
1854
2347
  visual: 0.35,
1855
- // 35%
2348
+ // 35% - Design quality
1856
2349
  content: 0.3,
1857
- // 30%
2350
+ // 30% - Content quality
1858
2351
  expert: 0.25,
1859
- // 25%
2352
+ // 25% - Expert methodology
1860
2353
  accessibility: 0.1
1861
- // 10%
2354
+ // 10% - Accessibility
1862
2355
  };
1863
2356
  const visualScore = this.calculateVisualScore(results.visual);
1864
2357
  const contentScore = this.calculateContentScore(results.content);
@@ -1905,13 +2398,15 @@ var QAEngine = class {
1905
2398
  };
1906
2399
  }
1907
2400
  // ===========================================================================
1908
- // VISUAL TESTS
2401
+ // VISUAL TESTS - KB-DRIVEN
1909
2402
  // ===========================================================================
1910
- async runVisualTests(html, mode) {
2403
+ async runVisualTests(html, mode, qualityMetrics) {
1911
2404
  const page = await this.browser.newPage();
1912
2405
  await page.setViewportSize({ width: 1280, height: 720 });
1913
2406
  await page.setContent(html);
1914
2407
  await page.waitForTimeout(1e3);
2408
+ const modeMetrics = mode === "keynote" ? qualityMetrics.keynote_mode : qualityMetrics.business_mode;
2409
+ const minWhitespace = this.kb.getWhitespaceRequirement(mode) * 100;
1915
2410
  const slideCount = await page.evaluate(() => {
1916
2411
  return window.Reveal?.getTotalSlides?.() ?? document.querySelectorAll(".slides > section").length;
1917
2412
  });
@@ -1977,9 +2472,8 @@ var QAEngine = class {
1977
2472
  }, { slideIndex: i });
1978
2473
  if (slideAnalysis) {
1979
2474
  const issues = [];
1980
- const minWhitespace = mode === "keynote" ? 40 : 25;
1981
2475
  if (slideAnalysis.whitespace < minWhitespace) {
1982
- issues.push(`Whitespace ${slideAnalysis.whitespace}% below ${minWhitespace}% minimum`);
2476
+ issues.push(`Whitespace ${slideAnalysis.whitespace}% below ${minWhitespace}% minimum (KB-defined)`);
1983
2477
  }
1984
2478
  if (slideAnalysis.whitespace > 80) {
1985
2479
  issues.push(`Whitespace ${slideAnalysis.whitespace}% - slide appears sparse`);
@@ -2034,34 +2528,35 @@ var QAEngine = class {
2034
2528
  };
2035
2529
  }
2036
2530
  // ===========================================================================
2037
- // CONTENT TESTS
2531
+ // CONTENT TESTS - KB-DRIVEN
2038
2532
  // ===========================================================================
2039
- async runContentTests(html, mode) {
2533
+ async runContentTests(html, mode, qualityMetrics) {
2040
2534
  const page = await this.browser.newPage();
2041
2535
  await page.setContent(html);
2042
2536
  await page.waitForTimeout(500);
2043
- const results = await page.evaluate((targetMode) => {
2537
+ const wordLimits = this.kb.getWordLimits(mode);
2538
+ const glanceTest = this.kb.getDuarteGlanceTest();
2539
+ const results = await page.evaluate((params) => {
2540
+ const { targetMode, maxWords, minWords, glanceWordLimit } = params;
2044
2541
  const slides = document.querySelectorAll(".slides > section");
2045
2542
  const perSlide = [];
2046
- const glanceTest = [];
2543
+ const glanceTestResults = [];
2047
2544
  const signalNoise = [];
2048
2545
  const oneIdea = [];
2049
2546
  slides.forEach((slide, index) => {
2050
2547
  const text = slide.innerText || "";
2051
2548
  const words = text.split(/\s+/).filter((w) => w.length > 0);
2052
2549
  const wordCount = words.length;
2053
- const maxWords = targetMode === "keynote" ? 25 : 80;
2054
- const minWords = targetMode === "business" ? 20 : 0;
2055
2550
  const withinLimit = wordCount <= maxWords && wordCount >= minWords;
2056
2551
  const title = slide.querySelector("h2")?.textContent || "";
2057
2552
  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);
2058
2553
  const hasInsight = title.length > 30 && hasVerb;
2059
2554
  const issues = [];
2060
2555
  if (!withinLimit) {
2061
- issues.push(`Word count ${wordCount} outside ${minWords}-${maxWords} range`);
2556
+ issues.push(`Word count ${wordCount} outside ${minWords}-${maxWords} range (KB-defined)`);
2062
2557
  }
2063
2558
  if (targetMode === "business" && !hasInsight && index > 0) {
2064
- issues.push("Title is not action-oriented (missing insight)");
2559
+ issues.push("Title is not action-oriented (missing insight per Minto)");
2065
2560
  }
2066
2561
  perSlide.push({
2067
2562
  slideIndex: index,
@@ -2074,12 +2569,12 @@ var QAEngine = class {
2074
2569
  const keyMessage = prominentElement?.textContent?.trim() || "";
2075
2570
  const keyWordCount = keyMessage.split(/\s+/).filter((w) => w.length > 0).length;
2076
2571
  const readingTime = keyWordCount / 4.2;
2077
- glanceTest.push({
2572
+ glanceTestResults.push({
2078
2573
  slideIndex: index,
2079
2574
  keyMessage,
2080
2575
  wordCount: keyWordCount,
2081
2576
  readingTime: Math.round(readingTime * 10) / 10,
2082
- passed: readingTime <= 3 && keyWordCount <= 15,
2577
+ passed: readingTime <= 3 && keyWordCount <= glanceWordLimit,
2083
2578
  recommendation: readingTime > 3 ? `Shorten to ${Math.floor(3 * 4.2)} words or less` : void 0
2084
2579
  });
2085
2580
  const elements = slide.querySelectorAll("h1, h2, h3, p, li, img");
@@ -2106,35 +2601,143 @@ var QAEngine = class {
2106
2601
  conflictingIdeas: ideaCount > 2 ? ["Multiple competing ideas detected"] : void 0
2107
2602
  });
2108
2603
  });
2109
- return { perSlide, glanceTest, signalNoise, oneIdea };
2110
- }, mode);
2604
+ return { perSlide, glanceTest: glanceTestResults, signalNoise, oneIdea };
2605
+ }, {
2606
+ targetMode: mode,
2607
+ maxWords: wordLimits.max,
2608
+ minWords: mode === "business" ? 20 : 0,
2609
+ glanceWordLimit: glanceTest.wordLimit
2610
+ });
2111
2611
  await page.close();
2112
2612
  return results;
2113
2613
  }
2114
2614
  // ===========================================================================
2115
- // EXPERT TESTS
2615
+ // EXPERT TESTS - KB-DRIVEN
2116
2616
  // ===========================================================================
2117
2617
  async runExpertTests(html, mode) {
2618
+ const duartePrinciples = this.kb.getExpertPrinciples("nancy_duarte");
2619
+ const reynoldsPrinciples = this.kb.getExpertPrinciples("garr_reynolds");
2620
+ const galloPrinciples = this.kb.getExpertPrinciples("carmine_gallo");
2621
+ const andersonPrinciples = this.kb.getExpertPrinciples("chris_anderson");
2118
2622
  return {
2119
- duarte: this.createExpertResult("Nancy Duarte", ["Glance Test", "STAR Moment", "Sparkline"], 85),
2120
- reynolds: this.createExpertResult("Garr Reynolds", ["Signal-to-Noise", "Simplicity", "Picture Superiority"], 80),
2121
- gallo: this.createExpertResult("Carmine Gallo", ["Rule of Three", "Emotional Connection"], 85),
2122
- anderson: this.createExpertResult("Chris Anderson", ["One Idea", "Clarity"], 90)
2623
+ duarte: this.validateDuartePrinciples(html, duartePrinciples),
2624
+ reynolds: this.validateReynoldsPrinciples(html, reynoldsPrinciples),
2625
+ gallo: this.validateGalloPrinciples(html, galloPrinciples),
2626
+ anderson: this.validateAndersonPrinciples(html, andersonPrinciples)
2123
2627
  };
2124
2628
  }
2125
- createExpertResult(name, principles, score) {
2629
+ validateDuartePrinciples(html, principles) {
2630
+ const violations = [];
2631
+ let score = 100;
2632
+ const slideMatches = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
2633
+ slideMatches.forEach((slide, index) => {
2634
+ const h1Match = slide.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
2635
+ const h2Match = slide.match(/<h2[^>]*>([\s\S]*?)<\/h2>/i);
2636
+ const keyMessage = (h1Match?.[1] || h2Match?.[1] || "").replace(/<[^>]+>/g, "").trim();
2637
+ const wordCount = keyMessage.split(/\s+/).filter((w) => w.length > 0).length;
2638
+ if (wordCount > 25) {
2639
+ violations.push(`Slide ${index + 1}: Key message exceeds 25 words (Duarte Glance Test)`);
2640
+ score -= 5;
2641
+ }
2642
+ });
2643
+ const hasStarMoment = html.includes("callout") || html.includes("highlight") || html.includes("big-idea");
2644
+ if (!hasStarMoment && slideMatches.length > 5) {
2645
+ violations.push("Missing STAR moment (Something They'll Always Remember)");
2646
+ score -= 10;
2647
+ }
2126
2648
  return {
2127
- expertName: name,
2128
- principlesChecked: principles,
2649
+ expertName: "Nancy Duarte",
2650
+ principlesChecked: ["Glance Test", "STAR Moment", "Sparkline Structure"],
2129
2651
  passed: score >= 80,
2130
- score,
2131
- violations: score < 80 ? [`${name} principles not fully met`] : []
2652
+ score: Math.max(0, score),
2653
+ violations
2654
+ };
2655
+ }
2656
+ validateReynoldsPrinciples(html, principles) {
2657
+ const violations = [];
2658
+ let score = 100;
2659
+ const decorativeElements = (html.match(/class="[^"]*decorative[^"]*"/gi) || []).length;
2660
+ const randomImages = (html.match(/picsum\.photos/gi) || []).length;
2661
+ if (randomImages > 0) {
2662
+ violations.push(`Found ${randomImages} random stock images (Reynolds: only use purposeful images)`);
2663
+ score -= randomImages * 10;
2664
+ }
2665
+ if (decorativeElements > 3) {
2666
+ violations.push(`Too many decorative elements (${decorativeElements}) - reduces signal-to-noise ratio`);
2667
+ score -= 10;
2668
+ }
2669
+ const columnLayouts = (html.match(/class="[^"]*columns[^"]*"/gi) || []).length;
2670
+ const slideCount = (html.match(/<section/gi) || []).length;
2671
+ if (columnLayouts > slideCount * 0.5) {
2672
+ violations.push("Overuse of column layouts - simplify per Reynolds");
2673
+ score -= 10;
2674
+ }
2675
+ return {
2676
+ expertName: "Garr Reynolds",
2677
+ principlesChecked: ["Signal-to-Noise", "Simplicity", "Picture Superiority"],
2678
+ passed: score >= 80,
2679
+ score: Math.max(0, score),
2680
+ violations
2681
+ };
2682
+ }
2683
+ validateGalloPrinciples(html, principles) {
2684
+ const violations = [];
2685
+ let score = 100;
2686
+ const bulletLists = html.match(/<ul[^>]*>[\s\S]*?<\/ul>/gi) || [];
2687
+ bulletLists.forEach((list, index) => {
2688
+ const bullets = (list.match(/<li/gi) || []).length;
2689
+ if (bullets > 5) {
2690
+ violations.push(`Bullet list ${index + 1} has ${bullets} items (Gallo: max 3-5)`);
2691
+ score -= 5;
2692
+ }
2693
+ });
2694
+ const hasQuote = html.includes("blockquote") || html.includes("quote");
2695
+ const hasStory = html.toLowerCase().includes("story") || html.toLowerCase().includes("journey");
2696
+ if (!hasQuote && !hasStory) {
2697
+ violations.push("Missing emotional connection elements (quotes, stories)");
2698
+ score -= 10;
2699
+ }
2700
+ return {
2701
+ expertName: "Carmine Gallo",
2702
+ principlesChecked: ["Rule of Three", "Emotional Connection", "Headline Power"],
2703
+ passed: score >= 80,
2704
+ score: Math.max(0, score),
2705
+ violations
2706
+ };
2707
+ }
2708
+ validateAndersonPrinciples(html, principles) {
2709
+ const violations = [];
2710
+ let score = 100;
2711
+ const slideMatches = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
2712
+ slideMatches.forEach((slide, index) => {
2713
+ const headings = (slide.match(/<h[12][^>]*>/gi) || []).length;
2714
+ if (headings > 2) {
2715
+ violations.push(`Slide ${index + 1}: Multiple ideas detected (Anderson: one idea per slide)`);
2716
+ score -= 5;
2717
+ }
2718
+ });
2719
+ const jargonPatterns = ["synergy", "leverage", "paradigm", "holistic", "streamline"];
2720
+ let jargonCount = 0;
2721
+ jargonPatterns.forEach((pattern) => {
2722
+ const matches = html.toLowerCase().match(new RegExp(pattern, "gi")) || [];
2723
+ jargonCount += matches.length;
2724
+ });
2725
+ if (jargonCount > 5) {
2726
+ violations.push(`Excessive jargon detected (${jargonCount} instances) - simplify language`);
2727
+ score -= 10;
2728
+ }
2729
+ return {
2730
+ expertName: "Chris Anderson",
2731
+ principlesChecked: ["One Idea", "Clarity", "Throughline"],
2732
+ passed: score >= 80,
2733
+ score: Math.max(0, score),
2734
+ violations
2132
2735
  };
2133
2736
  }
2134
2737
  // ===========================================================================
2135
- // ACCESSIBILITY TESTS
2738
+ // ACCESSIBILITY TESTS - KB-DRIVEN
2136
2739
  // ===========================================================================
2137
- async runAccessibilityTests(html) {
2740
+ async runAccessibilityTests(html, qualityMetrics) {
2138
2741
  const page = await this.browser.newPage();
2139
2742
  await page.setContent(html);
2140
2743
  await page.waitForTimeout(500);
@@ -2184,7 +2787,7 @@ var QAEngine = class {
2184
2787
  };
2185
2788
  }
2186
2789
  // ===========================================================================
2187
- // SCORING
2790
+ // SCORING - KB-DRIVEN
2188
2791
  // ===========================================================================
2189
2792
  calculateVisualScore(results) {
2190
2793
  let score = 100;
@@ -2308,6 +2911,7 @@ var QAEngine = class {
2308
2911
  // src/generators/html/RevealJsGenerator.ts
2309
2912
  var RevealJsGenerator = class {
2310
2913
  templateEngine;
2914
+ kb;
2311
2915
  defaultRevealConfig = {
2312
2916
  revealVersion: "5.0.4",
2313
2917
  hash: true,
@@ -2326,20 +2930,32 @@ var RevealJsGenerator = class {
2326
2930
  constructor() {
2327
2931
  this.templateEngine = new TemplateEngine();
2328
2932
  }
2933
+ /**
2934
+ * Initialize KnowledgeGateway
2935
+ */
2936
+ async initialize() {
2937
+ this.kb = await getKnowledgeGateway();
2938
+ }
2329
2939
  /**
2330
2940
  * Generate complete Reveal.js HTML presentation.
2331
2941
  */
2332
2942
  async generate(slides, config) {
2943
+ await this.initialize();
2333
2944
  const templateConfig = {};
2334
2945
  if (config.theme) templateConfig.theme = config.theme;
2335
2946
  if (config.customTemplates) templateConfig.customTemplates = config.customTemplates;
2336
2947
  const slideHtml = this.templateEngine.renderAll(slides, templateConfig);
2948
+ const presentationType = this.detectPresentationType(config);
2949
+ const palette = this.kb.getRecommendedPalette(presentationType);
2950
+ const typography = this.kb.getTypography(config.mode);
2337
2951
  const docConfig = {
2338
2952
  title: config.title,
2339
2953
  slides: slideHtml.join("\n"),
2340
2954
  theme: config.theme ?? "default",
2341
2955
  revealConfig: this.defaultRevealConfig,
2342
- mode: config.mode
2956
+ mode: config.mode,
2957
+ palette,
2958
+ typography
2343
2959
  };
2344
2960
  if (config.author) docConfig.author = config.author;
2345
2961
  if (config.subject) docConfig.subject = config.subject;
@@ -2350,11 +2966,23 @@ var RevealJsGenerator = class {
2350
2966
  }
2351
2967
  return html;
2352
2968
  }
2969
+ /**
2970
+ * Detect presentation type from config
2971
+ */
2972
+ detectPresentationType(config) {
2973
+ if (config.presentationType) {
2974
+ return config.presentationType;
2975
+ }
2976
+ if (config.mode === "keynote") {
2977
+ return "ted_keynote";
2978
+ }
2979
+ return "consulting_deck";
2980
+ }
2353
2981
  /**
2354
2982
  * Build the complete HTML document.
2355
2983
  */
2356
2984
  buildDocument(options) {
2357
- const { title, author, subject, slides, theme, customCSS, revealConfig, mode } = options;
2985
+ const { title, author, subject, slides, theme, customCSS, revealConfig, mode, palette, typography } = options;
2358
2986
  return `<!DOCTYPE html>
2359
2987
  <html lang="en">
2360
2988
  <head>
@@ -2362,7 +2990,7 @@ var RevealJsGenerator = class {
2362
2990
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2363
2991
  <meta name="author" content="${this.escapeHtml(author ?? "Claude Presentation Master")}">
2364
2992
  <meta name="description" content="${this.escapeHtml(subject ?? "")}">
2365
- <meta name="generator" content="Claude Presentation Master v1.0.0">
2993
+ <meta name="generator" content="Claude Presentation Master v6.0.0">
2366
2994
  <title>${this.escapeHtml(title)}</title>
2367
2995
 
2368
2996
  <!-- Reveal.js CSS -->
@@ -2376,10 +3004,10 @@ var RevealJsGenerator = class {
2376
3004
  <!-- Mermaid for diagrams -->
2377
3005
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
2378
3006
 
2379
- <!-- Presentation Engine CSS -->
3007
+ <!-- Presentation Engine CSS - KB-Driven Design System -->
2380
3008
  <style>
2381
- ${this.getBaseStyles(mode)}
2382
- ${this.getThemeStyles(theme)}
3009
+ ${this.getBaseStyles(mode, palette, typography)}
3010
+ ${this.getThemeStyles(theme, palette)}
2383
3011
  ${this.getAnimationStyles()}
2384
3012
  ${customCSS ?? ""}
2385
3013
  </style>
@@ -2431,26 +3059,35 @@ ${slides}
2431
3059
  </html>`;
2432
3060
  }
2433
3061
  /**
2434
- * Get base styles for slides.
3062
+ * Get base styles for slides - KB-DRIVEN
2435
3063
  */
2436
- getBaseStyles(mode) {
3064
+ getBaseStyles(mode, palette, typography) {
2437
3065
  const fontSize = mode === "keynote" ? "2.5em" : "1.8em";
2438
3066
  const lineHeight = mode === "keynote" ? "1.4" : "1.5";
3067
+ const primary = palette.primary || "#1a1a2e";
3068
+ const secondary = palette.secondary || "#16213e";
3069
+ const accent = palette.accent || "#0f3460";
3070
+ const highlight = palette.accent || "#e94560";
3071
+ const text = palette.text || "#1a1a2e";
3072
+ const background = palette.background || "#ffffff";
2439
3073
  return `
2440
- /* Base Styles */
3074
+ /* Base Styles - KB-Driven Design System */
2441
3075
  :root {
3076
+ /* Typography from KB */
2442
3077
  --font-heading: 'Source Sans Pro', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, sans-serif;
2443
3078
  --font-body: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2444
3079
  --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
2445
3080
 
2446
- --color-primary: #1a1a2e;
2447
- --color-secondary: #16213e;
2448
- --color-accent: #0f3460;
2449
- --color-highlight: #e94560;
2450
- --color-text: #1a1a2e;
2451
- --color-text-light: #4a4a68;
2452
- --color-background: #ffffff;
3081
+ /* Colors from KB Palette: ${palette.name} */
3082
+ --color-primary: ${primary};
3083
+ --color-secondary: ${secondary};
3084
+ --color-accent: ${accent};
3085
+ --color-highlight: ${highlight};
3086
+ --color-text: ${text};
3087
+ --color-text-light: ${this.lightenColor(text, 30)};
3088
+ --color-background: ${background};
2453
3089
 
3090
+ /* Layout */
2454
3091
  --slide-padding: 60px;
2455
3092
  --content-max-width: 1200px;
2456
3093
  }
@@ -2566,7 +3203,7 @@ ${slides}
2566
3203
  font-size: 0.8em;
2567
3204
  }
2568
3205
 
2569
- /* Images */
3206
+ /* Images - NO RANDOM PICSUM - Only user-provided or none */
2570
3207
  .reveal img {
2571
3208
  max-width: 100%;
2572
3209
  height: auto;
@@ -2672,9 +3309,25 @@ ${slides}
2672
3309
  `;
2673
3310
  }
2674
3311
  /**
2675
- * Get theme-specific styles.
3312
+ * Lighten a hex color
2676
3313
  */
2677
- getThemeStyles(theme) {
3314
+ lightenColor(hex, percent) {
3315
+ hex = hex.replace("#", "");
3316
+ let r = parseInt(hex.substring(0, 2), 16);
3317
+ let g = parseInt(hex.substring(2, 4), 16);
3318
+ let b = parseInt(hex.substring(4, 6), 16);
3319
+ r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));
3320
+ g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));
3321
+ b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
3322
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
3323
+ }
3324
+ /**
3325
+ * Get theme-specific styles - KB-DRIVEN
3326
+ */
3327
+ getThemeStyles(theme, palette) {
3328
+ if (theme === "default") {
3329
+ return "";
3330
+ }
2678
3331
  const themes = {
2679
3332
  "default": "",
2680
3333
  "light-corporate": `
@@ -2688,10 +3341,10 @@ ${slides}
2688
3341
  `,
2689
3342
  "modern-tech": `
2690
3343
  :root {
2691
- --color-primary: #1a1a2e;
2692
- --color-secondary: #16213e;
2693
- --color-accent: #0f3460;
2694
- --color-highlight: #e94560;
3344
+ --color-primary: ${palette.primary || "#1a1a2e"};
3345
+ --color-secondary: ${palette.secondary || "#16213e"};
3346
+ --color-accent: ${palette.accent || "#0f3460"};
3347
+ --color-highlight: ${palette.accent || "#e94560"};
2695
3348
  --color-background: #f8f9fa;
2696
3349
  }
2697
3350
  `,
@@ -3937,240 +4590,6 @@ function createDefaultImageProvider(options) {
3937
4590
  return new CompositeImageProvider(providers);
3938
4591
  }
3939
4592
 
3940
- // src/knowledge/KnowledgeBase.ts
3941
- var import_fs = require("fs");
3942
- var import_path = require("path");
3943
- var import_url = require("url");
3944
- var yaml = __toESM(require("yaml"));
3945
- var import_meta = {};
3946
- function getModuleDir() {
3947
- if (typeof import_meta !== "undefined" && import_meta.url) {
3948
- return (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
3949
- }
3950
- if (typeof __dirname !== "undefined") {
3951
- return __dirname;
3952
- }
3953
- return process.cwd();
3954
- }
3955
- var moduleDir = getModuleDir();
3956
- var KnowledgeBase = class {
3957
- data = null;
3958
- loaded = false;
3959
- /**
3960
- * Load the knowledge base from the bundled YAML file.
3961
- */
3962
- async load() {
3963
- if (this.loaded) return;
3964
- try {
3965
- const possiblePaths = [
3966
- // From dist/: go up to package root
3967
- (0, import_path.join)(moduleDir, "../../assets/presentation-knowledge.yaml"),
3968
- (0, import_path.join)(moduleDir, "../assets/presentation-knowledge.yaml"),
3969
- (0, import_path.join)(moduleDir, "../../../assets/presentation-knowledge.yaml"),
3970
- // From bundle in dist/index.js: go up one level
3971
- (0, import_path.join)(moduleDir, "assets/presentation-knowledge.yaml"),
3972
- // CWD-based paths for development or local installs
3973
- (0, import_path.join)(process.cwd(), "assets/presentation-knowledge.yaml"),
3974
- (0, import_path.join)(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
3975
- ];
3976
- let assetPath = "";
3977
- for (const p of possiblePaths) {
3978
- try {
3979
- (0, import_fs.readFileSync)(p);
3980
- assetPath = p;
3981
- break;
3982
- } catch {
3983
- continue;
3984
- }
3985
- }
3986
- if (!assetPath) {
3987
- throw new Error("Could not locate knowledge base file");
3988
- }
3989
- const content = (0, import_fs.readFileSync)(assetPath, "utf-8");
3990
- this.data = yaml.parse(content);
3991
- this.loaded = true;
3992
- console.log(`\u{1F4DA} Knowledge base loaded: v${this.data.version}`);
3993
- } catch (error) {
3994
- console.warn("\u26A0\uFE0F Could not load knowledge base, using defaults");
3995
- this.data = this.getDefaultData();
3996
- this.loaded = true;
3997
- }
3998
- }
3999
- /**
4000
- * Get expert methodology by name.
4001
- */
4002
- getExpert(name) {
4003
- this.ensureLoaded();
4004
- return this.data?.experts?.[name];
4005
- }
4006
- /**
4007
- * Get all expert names.
4008
- */
4009
- getExpertNames() {
4010
- this.ensureLoaded();
4011
- return Object.keys(this.data?.experts ?? {});
4012
- }
4013
- /**
4014
- * Get framework recommendation for audience.
4015
- */
4016
- getFrameworkForAudience(audience) {
4017
- this.ensureLoaded();
4018
- return this.data?.frameworkSelector?.byAudience?.[audience];
4019
- }
4020
- /**
4021
- * Get framework recommendation for goal.
4022
- */
4023
- getFrameworkForGoal(goal) {
4024
- this.ensureLoaded();
4025
- return this.data?.frameworkSelector?.byGoal?.[goal];
4026
- }
4027
- /**
4028
- * Get QA scoring rubric.
4029
- */
4030
- getScoringRubric() {
4031
- this.ensureLoaded();
4032
- return this.data?.automatedQA?.scoringRubric;
4033
- }
4034
- /**
4035
- * Get mode configuration (keynote or business).
4036
- */
4037
- getModeConfig(mode) {
4038
- this.ensureLoaded();
4039
- return this.data?.modes?.[mode];
4040
- }
4041
- /**
4042
- * Get slide type configuration.
4043
- */
4044
- getSlideType(type) {
4045
- this.ensureLoaded();
4046
- return this.data?.slideTypes?.[type];
4047
- }
4048
- /**
4049
- * Get the knowledge base version.
4050
- */
4051
- getVersion() {
4052
- this.ensureLoaded();
4053
- return this.data?.version ?? "unknown";
4054
- }
4055
- /**
4056
- * Validate a slide against expert principles.
4057
- */
4058
- validateAgainstExpert(expertName, slideData) {
4059
- const expert = this.getExpert(expertName);
4060
- if (!expert) {
4061
- return { passed: true, violations: [] };
4062
- }
4063
- const violations = [];
4064
- if (expert.wordLimits) {
4065
- if (expert.wordLimits.max && slideData.wordCount > expert.wordLimits.max) {
4066
- violations.push(`Exceeds ${expertName} word limit of ${expert.wordLimits.max}`);
4067
- }
4068
- if (expert.wordLimits.min && slideData.wordCount < expert.wordLimits.min) {
4069
- violations.push(`Below ${expertName} minimum of ${expert.wordLimits.min} words`);
4070
- }
4071
- }
4072
- return {
4073
- passed: violations.length === 0,
4074
- violations
4075
- };
4076
- }
4077
- /**
4078
- * Ensure knowledge base is loaded.
4079
- */
4080
- ensureLoaded() {
4081
- if (!this.loaded) {
4082
- this.data = this.getDefaultData();
4083
- this.loaded = true;
4084
- }
4085
- }
4086
- /**
4087
- * Get default data if YAML can't be loaded.
4088
- */
4089
- getDefaultData() {
4090
- return {
4091
- version: "1.0.0-fallback",
4092
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
4093
- experts: {
4094
- "Nancy Duarte": {
4095
- name: "Nancy Duarte",
4096
- principles: [
4097
- { name: "Glance Test", description: "Message clear in 3 seconds" },
4098
- { name: "STAR Moment", description: "Something They'll Always Remember" },
4099
- { name: "Sparkline", description: "Contrast What Is vs What Could Be" }
4100
- ]
4101
- },
4102
- "Garr Reynolds": {
4103
- name: "Garr Reynolds",
4104
- principles: [
4105
- { name: "Signal-to-Noise", description: "Maximize signal, minimize noise" },
4106
- { name: "Simplicity", description: "Amplify through simplification" }
4107
- ]
4108
- },
4109
- "Carmine Gallo": {
4110
- name: "Carmine Gallo",
4111
- principles: [
4112
- { name: "Rule of Three", description: "Maximum 3 key messages" },
4113
- { name: "Emotional Connection", description: "Connect emotionally first" }
4114
- ]
4115
- },
4116
- "Chris Anderson": {
4117
- name: "Chris Anderson",
4118
- principles: [
4119
- { name: "One Idea", description: "One powerful idea per talk" },
4120
- { name: "Dead Laptop Test", description: "Present without slides" }
4121
- ]
4122
- }
4123
- },
4124
- frameworkSelector: {
4125
- byAudience: {
4126
- "board": {
4127
- primaryFramework: "Barbara Minto",
4128
- slideTypes: ["executive_summary", "data_insight"]
4129
- },
4130
- "sales": {
4131
- primaryFramework: "Nancy Duarte",
4132
- slideTypes: ["big_idea", "social_proof"]
4133
- }
4134
- },
4135
- byGoal: {
4136
- "persuade": {
4137
- primaryFramework: "Nancy Duarte",
4138
- slideTypes: ["big_idea", "star_moment"]
4139
- },
4140
- "inform": {
4141
- primaryFramework: "Barbara Minto",
4142
- slideTypes: ["bullet_points", "data_insight"]
4143
- }
4144
- }
4145
- },
4146
- automatedQA: {
4147
- scoringRubric: {
4148
- totalPoints: 100,
4149
- passingThreshold: 95,
4150
- categories: {
4151
- visual: { weight: 35, checks: {} },
4152
- content: { weight: 30, checks: {} },
4153
- expert: { weight: 25, checks: {} },
4154
- accessibility: { weight: 10, checks: {} }
4155
- }
4156
- }
4157
- },
4158
- slideTypes: {},
4159
- modes: {
4160
- keynote: { maxWords: 25, minWhitespace: 35 },
4161
- business: { maxWords: 80, minWhitespace: 25 }
4162
- }
4163
- };
4164
- }
4165
- };
4166
- var knowledgeBaseInstance = null;
4167
- function getKnowledgeBase() {
4168
- if (!knowledgeBaseInstance) {
4169
- knowledgeBaseInstance = new KnowledgeBase();
4170
- }
4171
- return knowledgeBaseInstance;
4172
- }
4173
-
4174
4593
  // src/index.ts
4175
4594
  async function generate(config) {
4176
4595
  const engine = new PresentationEngine();
@@ -4185,7 +4604,7 @@ async function validate(presentation, options) {
4185
4604
  score
4186
4605
  };
4187
4606
  }
4188
- var VERSION = "1.0.0";
4607
+ var VERSION = "6.0.0";
4189
4608
  var index_default = {
4190
4609
  generate,
4191
4610
  validate,
@@ -4199,7 +4618,7 @@ var index_default = {
4199
4618
  CompositeChartProvider,
4200
4619
  CompositeImageProvider,
4201
4620
  ContentAnalyzer,
4202
- KnowledgeBase,
4621
+ KnowledgeGateway,
4203
4622
  LocalImageProvider,
4204
4623
  MermaidProvider,
4205
4624
  PlaceholderImageProvider,
@@ -4219,7 +4638,7 @@ var index_default = {
4219
4638
  createDefaultChartProvider,
4220
4639
  createDefaultImageProvider,
4221
4640
  generate,
4222
- getKnowledgeBase,
4641
+ getKnowledgeGateway,
4223
4642
  validate
4224
4643
  });
4225
4644
  /**