claude-presentation-master 6.1.1 → 7.2.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
@@ -287,6 +287,439 @@ var KnowledgeGateway = class {
287
287
  this.ensureLoaded();
288
288
  return this.kb;
289
289
  }
290
+ // ==========================================================================
291
+ // KB-DRIVEN SLIDEFACTORY METHODS (v7.0.0)
292
+ // ==========================================================================
293
+ /**
294
+ * Get allowed slide types for a specific presentation type.
295
+ * CRITICAL: SlideFactory must ONLY use types from this list.
296
+ */
297
+ getAllowedSlideTypes(type) {
298
+ this.ensureLoaded();
299
+ const typeConfig = this.kb.presentation_types?.[type];
300
+ if (!typeConfig?.slide_types_allowed) {
301
+ const mode = this.getModeForType(type);
302
+ const templates = this.getSlideTemplates(mode);
303
+ return templates.map((t) => t.name.toLowerCase().replace(/\s+/g, "_"));
304
+ }
305
+ return typeConfig.slide_types_allowed;
306
+ }
307
+ /**
308
+ * Get validation rules for a specific presentation type.
309
+ * Returns word limits, bullet limits, whitespace requirements, etc.
310
+ */
311
+ getValidationRules(type) {
312
+ this.ensureLoaded();
313
+ const typeConfig = this.kb.presentation_types?.[type];
314
+ if (!typeConfig?.validation_rules) {
315
+ const mode = this.getModeForType(type);
316
+ return mode === "keynote" ? {
317
+ wordsPerSlide: { min: 1, max: 25, ideal: 10 },
318
+ whitespace: { min: 40, ideal: 50 },
319
+ bulletsPerSlide: { max: 3 },
320
+ actionTitlesRequired: false,
321
+ sourcesRequired: false
322
+ } : {
323
+ wordsPerSlide: { min: 40, max: 80, ideal: 60 },
324
+ whitespace: { min: 25, ideal: 30 },
325
+ bulletsPerSlide: { max: 5 },
326
+ actionTitlesRequired: true,
327
+ sourcesRequired: true
328
+ };
329
+ }
330
+ const rules = typeConfig.validation_rules;
331
+ return {
332
+ wordsPerSlide: {
333
+ min: rules.words_per_slide?.min ?? 1,
334
+ max: rules.words_per_slide?.max ?? 80,
335
+ ideal: rules.words_per_slide?.ideal ?? 40
336
+ },
337
+ whitespace: {
338
+ min: rules.whitespace?.min ?? 25,
339
+ ideal: rules.whitespace?.ideal ?? 30
340
+ },
341
+ bulletsPerSlide: {
342
+ max: rules.bullets_per_slide?.max ?? 5
343
+ },
344
+ actionTitlesRequired: rules.action_titles_required ?? false,
345
+ sourcesRequired: rules.sources_required ?? false,
346
+ calloutsRequired: rules.callouts_required,
347
+ denseDataAllowed: rules.dense_data_allowed,
348
+ codeBlocksAllowed: rules.code_blocks_allowed,
349
+ diagramsRequired: rules.diagrams_required
350
+ };
351
+ }
352
+ /**
353
+ * Get required elements that MUST be in the deck for this type.
354
+ */
355
+ getRequiredElements(type) {
356
+ this.ensureLoaded();
357
+ const typeConfig = this.kb.presentation_types?.[type];
358
+ return typeConfig?.required_elements ?? [];
359
+ }
360
+ /**
361
+ * Get anti-patterns to avoid for this presentation type.
362
+ */
363
+ getAntiPatternsForType(type) {
364
+ this.ensureLoaded();
365
+ const typeConfig = this.kb.presentation_types?.[type];
366
+ return typeConfig?.anti_patterns ?? [];
367
+ }
368
+ /**
369
+ * Get typography specifications for this presentation type.
370
+ */
371
+ getTypographyForType(type) {
372
+ this.ensureLoaded();
373
+ const typeConfig = this.kb.presentation_types?.[type];
374
+ const typo = typeConfig?.typography;
375
+ if (!typo) {
376
+ const mode = this.getModeForType(type);
377
+ const modeTypo = this.getTypography(mode);
378
+ const maxFontsValue = modeTypo.max_fonts;
379
+ return {
380
+ titles: modeTypo.titles ?? "48px, Bold",
381
+ body: modeTypo.body_text ?? "24px",
382
+ maxFonts: typeof maxFontsValue === "number" ? maxFontsValue : 2
383
+ };
384
+ }
385
+ return {
386
+ titles: typo.titles ?? "48px, Bold",
387
+ body: typo.body ?? "24px",
388
+ maxFonts: typeof typo.max_fonts === "number" ? typo.max_fonts : 2,
389
+ actionTitle: typo.action_title,
390
+ sectionHeaders: typo.section_headers,
391
+ dataLabels: typo.data_labels,
392
+ code: typo.code
393
+ };
394
+ }
395
+ /**
396
+ * Get CSS variables recipe for this presentation type.
397
+ */
398
+ getCSSVariablesForType(type) {
399
+ this.ensureLoaded();
400
+ const recipes = this.kb.type_visual_recipes;
401
+ if (recipes?.[type]?.css_variables) {
402
+ return recipes[type].css_variables;
403
+ }
404
+ return `:root {
405
+ --color-background: #FAFAF9;
406
+ --color-primary: #0F172A;
407
+ --color-secondary: #475569;
408
+ --color-accent: #0369A1;
409
+ --color-text: #18181B;
410
+ --font-display: 'Inter', sans-serif;
411
+ --font-body: 'Inter', sans-serif;
412
+ }`;
413
+ }
414
+ /**
415
+ * Get scoring weights for QA evaluation for this type.
416
+ */
417
+ getScoringWeights(type) {
418
+ this.ensureLoaded();
419
+ const typeConfig = this.kb.presentation_types?.[type];
420
+ const weights = typeConfig?.scoring_weights;
421
+ if (!weights) {
422
+ return {
423
+ visualQuality: 30,
424
+ contentQuality: 30,
425
+ expertCompliance: 30,
426
+ accessibility: 10
427
+ };
428
+ }
429
+ return {
430
+ visualQuality: weights.visual_quality ?? 30,
431
+ contentQuality: weights.content_quality ?? 30,
432
+ expertCompliance: weights.expert_compliance ?? 30,
433
+ accessibility: weights.accessibility ?? 10
434
+ };
435
+ }
436
+ /**
437
+ * Get story structure framework for this presentation type.
438
+ */
439
+ getStoryStructure(type) {
440
+ this.ensureLoaded();
441
+ const typeConfig = this.kb.presentation_types?.[type];
442
+ const mode = this.getModeForType(type);
443
+ if (mode === "business") {
444
+ return {
445
+ framework: "scqa",
446
+ requiredElements: ["situation", "complication", "answer"],
447
+ optionalElements: ["question", "evidence", "recommendation"]
448
+ };
449
+ }
450
+ return {
451
+ framework: "sparkline",
452
+ requiredElements: ["what_is", "what_could_be", "call_to_action"],
453
+ optionalElements: ["star_moment", "hook"]
454
+ };
455
+ }
456
+ /**
457
+ * Map a content pattern to the best allowed slide type.
458
+ * CRITICAL: This is how SlideFactory decides which slide type to use.
459
+ */
460
+ mapContentPatternToSlideType(pattern, allowedTypes) {
461
+ const patternToTypes = {
462
+ big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
463
+ comparison: ["comparison", "options_comparison", "two_column", "two-column"],
464
+ timeline: ["timeline", "process_timeline", "process", "roadmap"],
465
+ process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
466
+ metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
467
+ quote: ["quote", "testimonial", "social_proof", "social-proof"],
468
+ code: ["code_snippet", "technical", "two_column", "two-column"],
469
+ bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
470
+ prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
471
+ };
472
+ const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
473
+ for (const preferred of preferredTypes) {
474
+ const underscoreVersion = preferred.replace(/-/g, "_");
475
+ const dashVersion = preferred.replace(/_/g, "-");
476
+ if (allowedTypes.includes(preferred)) return preferred;
477
+ if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
478
+ if (allowedTypes.includes(dashVersion)) return dashVersion;
479
+ }
480
+ const contentTypes = [
481
+ "bullet_points",
482
+ "bullet-points",
483
+ "two_column",
484
+ "two-column",
485
+ "three_column",
486
+ "three-column",
487
+ "data_insight"
488
+ ];
489
+ for (const ct of contentTypes) {
490
+ if (allowedTypes.includes(ct)) return ct;
491
+ }
492
+ return allowedTypes[0] ?? "bullet-points";
493
+ }
494
+ /**
495
+ * Get a specific slide template by name from the KB.
496
+ */
497
+ getSlideTemplateByName(name) {
498
+ this.ensureLoaded();
499
+ const keynoteTemplates = this.kb.slide_templates?.keynote_mode ?? [];
500
+ const businessTemplates = this.kb.slide_templates?.business_mode ?? [];
501
+ const normalizedName = name.toLowerCase().replace(/[-_]/g, " ");
502
+ for (const template of [...keynoteTemplates, ...businessTemplates]) {
503
+ const templateName = template.name.toLowerCase().replace(/[-_]/g, " ");
504
+ if (templateName.includes(normalizedName) || normalizedName.includes(templateName)) {
505
+ return {
506
+ name: template.name,
507
+ purpose: template.purpose,
508
+ elements: template.elements,
509
+ components: template.components,
510
+ wordLimit: template.word_limit ?? 60
511
+ };
512
+ }
513
+ }
514
+ return void 0;
515
+ }
516
+ // ==========================================================================
517
+ // SLIDE DEFAULTS & LABELS (v7.1.0 - Replace ALL Hardcoded Values)
518
+ // ==========================================================================
519
+ /**
520
+ * Get slide defaults for structural slides (titles, messages, labels).
521
+ * CRITICAL: SlideFactory must use these instead of hardcoded strings.
522
+ */
523
+ getSlideDefaults(type) {
524
+ this.ensureLoaded();
525
+ const mode = this.getModeForType(type);
526
+ const rules = this.getValidationRules(type);
527
+ const maxWords = rules.wordsPerSlide.max;
528
+ return {
529
+ agenda: { title: mode === "business" ? "Agenda" : "What We'll Cover" },
530
+ thankYou: {
531
+ title: mode === "business" ? "Thank You" : "Thank You",
532
+ subtitle: mode === "business" ? "Questions?" : "Let's Connect"
533
+ },
534
+ cta: {
535
+ title: mode === "business" ? "Next Steps" : "Take Action",
536
+ message: mode === "business" ? "Recommended Actions" : "Ready to Begin?",
537
+ fallback: mode === "business" ? "Contact us to learn more" : "Take the next step"
538
+ },
539
+ metricsGrid: {
540
+ title: mode === "business" ? "Key Metrics" : "The Numbers",
541
+ maxMetrics: 4
542
+ },
543
+ code: {
544
+ label: "Code Example",
545
+ maxChars: 500
546
+ },
547
+ comparison: {
548
+ leftLabel: mode === "business" ? "Current State" : "Before",
549
+ rightLabel: mode === "business" ? "Future State" : "After",
550
+ optionLabels: ["Option A", "Option B", "Option C", "Option D"]
551
+ },
552
+ column: { labelTemplate: "Point {n}" },
553
+ subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
554
+ context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
555
+ step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
556
+ columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
557
+ };
558
+ }
559
+ /**
560
+ * Get SCQA framework titles from KB (for business mode).
561
+ */
562
+ getSCQATitles(type) {
563
+ this.ensureLoaded();
564
+ const mode = this.getModeForType(type);
565
+ if (mode === "business") {
566
+ return {
567
+ situation: "Current Situation",
568
+ complication: "The Challenge",
569
+ question: "The Critical Question",
570
+ answer: "Our Recommendation"
571
+ };
572
+ }
573
+ return {
574
+ situation: "Where We Are",
575
+ complication: "What's At Stake",
576
+ question: "The Question We Face",
577
+ answer: "The Path Forward"
578
+ };
579
+ }
580
+ /**
581
+ * Get Sparkline framework titles from KB (for keynote mode).
582
+ */
583
+ getSparklineTitles(type) {
584
+ this.ensureLoaded();
585
+ return {
586
+ whatIs: "Where We Are Today",
587
+ whatCouldBe: "What Could Be",
588
+ callToAdventure: "The Call to Adventure",
589
+ newBliss: "The New World"
590
+ };
591
+ }
592
+ /**
593
+ * Get insight markers for detecting action titles.
594
+ * These are words/phrases that indicate an insight vs a topic.
595
+ */
596
+ getInsightMarkers() {
597
+ this.ensureLoaded();
598
+ return [
599
+ // Trend indicators
600
+ "increase",
601
+ "decrease",
602
+ "grew",
603
+ "declined",
604
+ "achieve",
605
+ "exceed",
606
+ "improve",
607
+ "reduce",
608
+ "save",
609
+ "gain",
610
+ "lost",
611
+ "doubled",
612
+ "tripled",
613
+ "outperform",
614
+ "underperform",
615
+ "accelerate",
616
+ "decelerate",
617
+ // Quantitative markers
618
+ "%",
619
+ "percent",
620
+ "million",
621
+ "billion",
622
+ "thousand",
623
+ "$",
624
+ "\u20AC",
625
+ "\xA3",
626
+ "ROI",
627
+ "revenue",
628
+ "cost",
629
+ "margin",
630
+ "growth",
631
+ "decline",
632
+ // Causality markers
633
+ "due to",
634
+ "because",
635
+ "resulting in",
636
+ "leading to",
637
+ "enabling",
638
+ "driving",
639
+ "caused by",
640
+ "attributed to",
641
+ "as a result of",
642
+ // Action verbs (Minto Pyramid style)
643
+ "should",
644
+ "must",
645
+ "need to",
646
+ "recommend",
647
+ "propose",
648
+ "suggest",
649
+ "requires",
650
+ "demands",
651
+ "enables",
652
+ "prevents",
653
+ "ensures"
654
+ ];
655
+ }
656
+ /**
657
+ * Get word limit for a specific slide element type.
658
+ */
659
+ getWordLimitForElement(type, element) {
660
+ const rules = this.getValidationRules(type);
661
+ const maxWords = rules.wordsPerSlide.max;
662
+ const bulletsMax = rules.bulletsPerSlide.max;
663
+ switch (element) {
664
+ case "title":
665
+ return Math.min(15, Math.floor(maxWords / 4));
666
+ case "subtitle":
667
+ return Math.min(15, Math.floor(maxWords / 5));
668
+ case "bullet":
669
+ return Math.floor(maxWords / (bulletsMax || 5));
670
+ case "body":
671
+ return maxWords;
672
+ case "quote":
673
+ return Math.min(40, maxWords);
674
+ case "step":
675
+ return Math.min(20, Math.floor(maxWords / 4));
676
+ default:
677
+ return maxWords;
678
+ }
679
+ }
680
+ /**
681
+ * Validate a slide against KB rules for the given presentation type.
682
+ */
683
+ validateSlideAgainstKB(slide, type) {
684
+ const rules = this.getValidationRules(type);
685
+ const allowedTypes = this.getAllowedSlideTypes(type);
686
+ const violations = [];
687
+ const warnings = [];
688
+ const normalizedType = slide.type.replace(/-/g, "_");
689
+ const isAllowed = allowedTypes.some(
690
+ (t) => t === slide.type || t === normalizedType || t.replace(/_/g, "-") === slide.type
691
+ );
692
+ if (!isAllowed) {
693
+ violations.push(`Slide type '${slide.type}' not allowed for ${type}. Allowed: ${allowedTypes.join(", ")}`);
694
+ }
695
+ if (slide.wordCount > rules.wordsPerSlide.max) {
696
+ violations.push(`Word count ${slide.wordCount} exceeds max ${rules.wordsPerSlide.max}`);
697
+ } else if (slide.wordCount < rules.wordsPerSlide.min) {
698
+ warnings.push(`Word count ${slide.wordCount} below min ${rules.wordsPerSlide.min}`);
699
+ }
700
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
701
+ violations.push(`Bullet count ${slide.bulletCount} exceeds max ${rules.bulletsPerSlide.max}`);
702
+ }
703
+ if (rules.actionTitlesRequired && !slide.hasActionTitle) {
704
+ violations.push(`Action title required for ${type} but not present`);
705
+ }
706
+ if (rules.sourcesRequired && !slide.hasSource) {
707
+ warnings.push(`Source citation recommended for ${type}`);
708
+ }
709
+ const fixes = {};
710
+ if (slide.wordCount > rules.wordsPerSlide.max) {
711
+ fixes.wordCount = rules.wordsPerSlide.max;
712
+ }
713
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
714
+ fixes.bulletCount = rules.bulletsPerSlide.max;
715
+ }
716
+ return {
717
+ valid: violations.length === 0,
718
+ violations,
719
+ warnings,
720
+ fixes
721
+ };
722
+ }
290
723
  };
291
724
  var gatewayInstance = null;
292
725
  async function getKnowledgeGateway() {
@@ -424,12 +857,22 @@ var ContentAnalyzer = class {
424
857
  }
425
858
  /**
426
859
  * Parse content based on type
860
+ * CRITICAL: Strip code blocks FIRST to prevent code from becoming slides
427
861
  */
428
862
  parseContent(content, contentType) {
429
- if (contentType === "markdown" || content.includes("#") || content.includes("- ")) {
430
- return content;
431
- }
432
- return content;
863
+ let text = content;
864
+ text = text.replace(/```[\s\S]*?```/g, "");
865
+ text = text.replace(/`[^`]{50,}`/g, "");
866
+ text = text.split("\n").filter((line) => {
867
+ const trimmed = line.trim();
868
+ if (/^(import|export|const|let|var|function|class|interface|type|async|await|return|if|for|while)\s/.test(trimmed)) return false;
869
+ if (/^(\/\/|\/\*|\*|#!)/.test(trimmed)) return false;
870
+ if (/^\s*[{}\[\]();]/.test(trimmed)) return false;
871
+ if (/^[\w.]+\s*\(.*\)\s*;?\s*$/.test(trimmed)) return false;
872
+ if (/^[\w.]+\s*=\s*/.test(trimmed) && /[{(\[]/.test(trimmed)) return false;
873
+ return true;
874
+ }).join("\n");
875
+ return text;
433
876
  }
434
877
  /**
435
878
  * Extract the main title from content
@@ -547,6 +990,7 @@ var ContentAnalyzer = class {
547
990
  }
548
991
  /**
549
992
  * Extract SCQA structure (Barbara Minto)
993
+ * CRITICAL: Answer must be clean prose, NOT bullet points
550
994
  */
551
995
  extractSCQA(text) {
552
996
  const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
@@ -554,34 +998,45 @@ var ContentAnalyzer = class {
554
998
  let complication = "";
555
999
  let question = "";
556
1000
  let answer = "";
557
- for (const para of paragraphs.slice(0, 3)) {
1001
+ for (const para of paragraphs.slice(0, 5)) {
1002
+ if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
558
1003
  if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
559
1004
  situation = this.extractFirstSentence(para);
560
1005
  break;
561
1006
  }
562
1007
  }
563
1008
  for (const para of paragraphs) {
1009
+ if (para.startsWith("-") || para.startsWith("|")) continue;
564
1010
  if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
565
1011
  complication = this.extractFirstSentence(para);
566
1012
  break;
567
1013
  }
568
1014
  }
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)) {
1015
+ for (const para of paragraphs) {
1016
+ if (para.startsWith("-") || para.includes("\n-") || para.startsWith("|")) continue;
1017
+ if (para.startsWith("*") && !para.startsWith("**")) continue;
572
1018
  const lowerPara = para.toLowerCase();
573
1019
  if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
574
- answer = this.extractFirstSentence(para);
575
- break;
1020
+ const sentence = this.extractFirstSentence(para);
1021
+ if (sentence.split(/\s+/).length >= 8 && /\b(is|are|will|can|provides?|delivers?|enables?)\b/i.test(sentence)) {
1022
+ answer = sentence;
1023
+ break;
1024
+ }
576
1025
  }
577
1026
  }
578
- if (!situation && paragraphs.length > 0 && paragraphs[0]) {
579
- situation = this.extractFirstSentence(paragraphs[0]);
1027
+ if (!situation && paragraphs.length > 0) {
1028
+ for (const para of paragraphs.slice(0, 5)) {
1029
+ if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
1030
+ situation = this.extractFirstSentence(para);
1031
+ break;
1032
+ }
1033
+ }
580
1034
  }
581
- if (!answer && paragraphs.length > 2) {
582
- for (const para of paragraphs.slice(1, Math.floor(paragraphs.length * 0.5))) {
1035
+ if (!answer) {
1036
+ for (const para of paragraphs) {
1037
+ if (para.startsWith("-") || para.includes("\n-")) continue;
583
1038
  const lowerPara = para.toLowerCase();
584
- if (lowerPara.includes("recommend") || lowerPara.includes("strategy") || lowerPara.includes("solution") || lowerPara.includes("approach")) {
1039
+ if (lowerPara.includes("bottom line") || lowerPara.includes("delivers both") || lowerPara.includes("the result") || lowerPara.includes("in summary")) {
585
1040
  answer = this.extractFirstSentence(para);
586
1041
  break;
587
1042
  }
@@ -740,56 +1195,123 @@ var ContentAnalyzer = class {
740
1195
  }
741
1196
  /**
742
1197
  * Extract data points (metrics with values)
743
- * IMPROVED: Smarter label extraction that understands markdown tables
1198
+ * REWRITTEN: Proper markdown table parsing and meaningful label extraction
744
1199
  */
745
1200
  extractDataPoints(text) {
746
1201
  const dataPoints = [];
747
1202
  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
- }
1203
+ const bulletPatterns = text.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?(?:\s*\([^)]+\))?)/gm);
1204
+ if (bulletPatterns) {
1205
+ for (const match of bulletPatterns) {
1206
+ const parsed = match.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?)/);
1207
+ if (parsed && parsed[1] && parsed[2]) {
1208
+ const label = parsed[1].trim();
1209
+ const value = parsed[2].trim();
1210
+ if (!usedValues.has(value) && label.length >= 5) {
1211
+ usedValues.add(value);
1212
+ dataPoints.push({ value, label: this.cleanMetricLabel(label) });
761
1213
  }
762
1214
  }
763
1215
  }
764
1216
  }
765
1217
  const lines = text.split("\n");
1218
+ let inTable = false;
1219
+ let tableRows = [];
1220
+ let headerRow = [];
766
1221
  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) });
1222
+ if (line.includes("|") && line.trim().length > 3) {
1223
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
1224
+ if (cells.every((c) => /^[-:]+$/.test(c))) continue;
1225
+ if (!inTable) {
1226
+ headerRow = cells;
1227
+ inTable = true;
1228
+ } else {
1229
+ tableRows.push(cells);
1230
+ }
1231
+ } else if (inTable && tableRows.length > 0) {
1232
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1233
+ inTable = false;
1234
+ tableRows = [];
1235
+ headerRow = [];
772
1236
  }
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 });
1237
+ }
1238
+ if (tableRows.length > 0) {
1239
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1240
+ }
1241
+ for (const line of lines) {
1242
+ if (line.includes("|")) continue;
1243
+ if (/```/.test(line)) continue;
1244
+ const percentPattern = line.match(/(\d+(?:\.\d+)?%)\s+(reduction|increase|improvement|growth|decrease)\s+(?:in\s+)?([^.]+)/i);
1245
+ if (percentPattern && percentPattern[1] && percentPattern[3]) {
1246
+ const value = percentPattern[1];
1247
+ if (!usedValues.has(value)) {
1248
+ usedValues.add(value);
1249
+ const action = percentPattern[2] || "change";
1250
+ const subject = percentPattern[3].slice(0, 30).trim();
1251
+ dataPoints.push({ value, label: `${action} in ${subject}` });
780
1252
  }
781
1253
  }
782
1254
  }
783
1255
  return dataPoints.slice(0, 4);
784
1256
  }
1257
+ /**
1258
+ * Extract metrics from a parsed markdown table
1259
+ */
1260
+ extractMetricsFromTable(headers, rows, dataPoints, usedValues) {
1261
+ for (const row of rows) {
1262
+ const label = row[0];
1263
+ if (!label || label.length < 3) continue;
1264
+ for (let i = 1; i < row.length; i++) {
1265
+ const cell = row[i];
1266
+ if (!cell) continue;
1267
+ if (/^\$?[\d,]+\.?\d*[%MBK]?$/.test(cell.replace(/[,\s]/g, "")) || /^\d+\s*(seconds?|minutes?|hours?|ms|gb|mb)$/i.test(cell)) {
1268
+ if (!usedValues.has(cell)) {
1269
+ usedValues.add(cell);
1270
+ const colHeader = headers[i] || "";
1271
+ const fullLabel = colHeader && colHeader !== label ? `${label} (${colHeader})` : label;
1272
+ dataPoints.push({
1273
+ value: cell,
1274
+ label: this.cleanMetricLabel(fullLabel)
1275
+ });
1276
+ break;
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ /**
1283
+ * Clean a metric label to ensure it's complete and meaningful
1284
+ */
1285
+ cleanMetricLabel(raw) {
1286
+ let label = raw.replace(/\*\*/g, "").replace(/\|/g, " ").replace(/[:\-–—]+$/, "").replace(/\s+/g, " ").trim();
1287
+ if (label.length > 0) {
1288
+ label = label.charAt(0).toUpperCase() + label.slice(1);
1289
+ }
1290
+ if (label.length > 40) {
1291
+ const words = label.split(/\s+/);
1292
+ label = "";
1293
+ for (const word of words) {
1294
+ if ((label + " " + word).length <= 40) {
1295
+ label = label ? label + " " + word : word;
1296
+ } else {
1297
+ break;
1298
+ }
1299
+ }
1300
+ }
1301
+ return label || "Value";
1302
+ }
785
1303
  /**
786
1304
  * Extract a meaningful label from a line containing a metric
787
1305
  */
788
1306
  extractLabelFromLine(line, value) {
1307
+ const colonMatch = line.match(/([^:]+):\s*\$?[\d]/);
1308
+ if (colonMatch && colonMatch[1]) {
1309
+ return this.cleanMetricLabel(colonMatch[1]);
1310
+ }
789
1311
  const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
790
1312
  const beforeValue = cleaned.split(value)[0] || "";
791
1313
  const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
792
- return words.slice(-4).join(" ").slice(0, 40) || "Value";
1314
+ return this.cleanMetricLabel(words.slice(-4).join(" "));
793
1315
  }
794
1316
  /**
795
1317
  * Check if text contains any of the signals
@@ -816,531 +1338,897 @@ var ContentAnalyzer = class {
816
1338
  }
817
1339
  };
818
1340
 
819
- // src/core/SlideFactory.ts
820
- var SlideFactory = class {
821
- templates;
822
- usedContent = /* @__PURE__ */ new Set();
823
- constructor() {
824
- this.templates = this.initializeTemplates();
825
- }
1341
+ // src/core/ContentPatternClassifier.ts
1342
+ var ContentPatternClassifier = class {
1343
+ // Regex patterns for content detection
1344
+ patterns = {
1345
+ // Big numbers: $4.88M, 23%, 10x, 500+, etc.
1346
+ bigNumber: /(\$[\d,.]+[MBK]?|\d+(?:\.\d+)?%|\d+[xX]|\d{2,}(?:\+|,\d{3})+)/,
1347
+ // Comparison language
1348
+ comparison: /\b(vs\.?|versus|compared\s+to|before\s+(?:and\s+)?after|better\s+than|worse\s+than|alternative|option\s+[A-Z1-3])\b/i,
1349
+ // Timeline markers
1350
+ timeline: /\b(phase\s*\d|week\s*\d|month\s*\d|day\s*\d|Q[1-4]|20\d{2}|step\s*\d|year\s*\d|sprint\s*\d)\b/i,
1351
+ // Process/steps language
1352
+ process: /\b(step\s*\d|stage\s*\d|pillar|phase|first|second|third|finally|then|next|workflow|process)\b/i,
1353
+ // Quote patterns
1354
+ quote: /^[""]|[""]\s*[-—–]\s*|^\s*>\s*|[""]$/,
1355
+ // Code patterns
1356
+ code: /```|`[^`]+`|function\s*\(|const\s+\w+\s*=|import\s+{|class\s+\w+|=>\s*{/,
1357
+ // Metric patterns (for detecting multiple metrics)
1358
+ metric: /(\d+(?:\.\d+)?(?:%|[xX]|[MBK])?)\s*[-–:]\s*|\b(?:increased|decreased|grew|improved|reduced)\s+(?:by\s+)?(\d+)/gi
1359
+ };
826
1360
  /**
827
- * Check if content has already been used (deduplication)
1361
+ * Classify a content section to determine its primary pattern.
1362
+ * Returns a ContentPattern object used by SlideFactory to select slide type.
828
1363
  */
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;
1364
+ classify(section) {
1365
+ const fullText = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
1366
+ const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
1367
+ const hasBigNumber = this.patterns.bigNumber.test(fullText);
1368
+ const bigNumberMatch = fullText.match(this.patterns.bigNumber);
1369
+ const hasComparison = this.patterns.comparison.test(fullText);
1370
+ const hasTimeline = this.patterns.timeline.test(fullText);
1371
+ const hasProcess = this.patterns.process.test(section.header) || section.bullets.length >= 3 && this.hasNumberedItems(section.bullets);
1372
+ const hasQuote = this.patterns.quote.test(section.content);
1373
+ const hasCode = this.patterns.code.test(section.content);
1374
+ const metricMatches = fullText.match(this.patterns.metric);
1375
+ const hasMetrics = section.metrics.length >= 3 || metricMatches && metricMatches.length >= 3;
1376
+ let primaryPattern = "prose";
1377
+ if (hasQuote && section.content.length > 20) {
1378
+ primaryPattern = "quote";
1379
+ } else if (hasCode) {
1380
+ primaryPattern = "code";
1381
+ } else if (hasBigNumber && wordCount < 30) {
1382
+ primaryPattern = "big_number";
1383
+ } else if (hasMetrics) {
1384
+ primaryPattern = "metrics";
1385
+ } else if (hasComparison) {
1386
+ primaryPattern = "comparison";
1387
+ } else if (hasTimeline) {
1388
+ primaryPattern = "timeline";
1389
+ } else if (hasProcess && section.bullets.length >= 3) {
1390
+ primaryPattern = "process";
1391
+ } else if (section.bullets.length >= 2) {
1392
+ primaryPattern = "bullets";
1393
+ } else {
1394
+ primaryPattern = "prose";
834
1395
  }
835
- this.usedContent.add(normalized);
836
- return false;
1396
+ const result = {
1397
+ hasBigNumber,
1398
+ hasComparison,
1399
+ hasTimeline,
1400
+ hasProcess,
1401
+ hasMetrics: !!hasMetrics,
1402
+ hasQuote,
1403
+ hasCode,
1404
+ bulletCount: section.bullets.length,
1405
+ wordCount,
1406
+ primaryPattern
1407
+ };
1408
+ if (bigNumberMatch && bigNumberMatch[1]) {
1409
+ result.bigNumberValue = bigNumberMatch[1];
1410
+ }
1411
+ return result;
837
1412
  }
838
1413
  /**
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
851
- */
852
- async createSlides(analysis, mode) {
853
- const slides = [];
854
- let slideIndex = 0;
855
- this.usedContent.clear();
856
- slides.push(this.createTitleSlide(slideIndex++, analysis));
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) {
862
- slides.push(this.createAgendaSlide(slideIndex++, analysis));
863
- }
864
- if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
865
- slides.push(this.createContextSlide(slideIndex++, analysis, mode));
866
- }
867
- if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
868
- slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
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
- }
1414
+ * Check if bullets appear to be numbered/ordered items
1415
+ */
1416
+ hasNumberedItems(bullets) {
1417
+ let numberedCount = 0;
1418
+ for (const bullet of bullets) {
1419
+ if (/^\d+[.)]\s|^[a-z][.)]\s|^step\s*\d/i.test(bullet.trim())) {
1420
+ numberedCount++;
879
1421
  }
880
1422
  }
881
- for (const message of analysis.keyMessages) {
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));
1423
+ return numberedCount >= 2;
1424
+ }
1425
+ /**
1426
+ * Extract the big number value from content for display
1427
+ */
1428
+ extractBigNumber(content) {
1429
+ const match = content.match(this.patterns.bigNumber);
1430
+ if (!match || !match[1]) return null;
1431
+ const value = match[1];
1432
+ const afterNumber = content.slice(content.indexOf(value) + value.length);
1433
+ const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
1434
+ return { value, context };
1435
+ }
1436
+ /**
1437
+ * Extract comparison elements from content
1438
+ */
1439
+ extractComparison(section) {
1440
+ const text = section.content;
1441
+ const vsMatch = text.match(/(.{10,50})\s+(?:vs\.?|versus)\s+(.{10,50})/i);
1442
+ if (vsMatch && vsMatch[1] && vsMatch[2]) {
1443
+ return { left: vsMatch[1].trim(), right: vsMatch[2].trim() };
1444
+ }
1445
+ const beforeAfterMatch = text.match(/before[:\s]+(.{10,100})(?:after[:\s]+(.{10,100}))?/i);
1446
+ if (beforeAfterMatch && beforeAfterMatch[1]) {
1447
+ const left = beforeAfterMatch[1].trim();
1448
+ const right = beforeAfterMatch[2] ? beforeAfterMatch[2].trim() : "After";
1449
+ return { left, right };
1450
+ }
1451
+ if (section.bullets.length === 2) {
1452
+ const left = section.bullets[0];
1453
+ const right = section.bullets[1];
1454
+ if (left && right) {
1455
+ return { left, right };
885
1456
  }
886
1457
  }
887
- if (analysis.dataPoints.length >= 2) {
888
- slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
889
- }
890
- if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
891
- slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
892
- }
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));
1458
+ return null;
1459
+ }
1460
+ /**
1461
+ * Extract timeline/process steps from content
1462
+ */
1463
+ extractSteps(section) {
1464
+ const steps = [];
1465
+ for (const bullet of section.bullets) {
1466
+ const stepMatch = bullet.match(/^((?:step|phase|stage)\s*\d+|[1-9]\.|\d+\))\s*[-:.]?\s*(.+)/i);
1467
+ if (stepMatch && stepMatch[1] && stepMatch[2]) {
1468
+ steps.push({
1469
+ label: stepMatch[1].trim(),
1470
+ description: stepMatch[2].trim()
1471
+ });
1472
+ } else {
1473
+ steps.push({
1474
+ label: `${steps.length + 1}`,
1475
+ description: bullet
1476
+ });
899
1477
  }
900
1478
  }
901
- if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
902
- slides.push(this.createCTASlide(slideIndex++, analysis, mode));
903
- }
904
- slides.push(this.createThankYouSlide(slideIndex++));
905
- return slides;
1479
+ return steps;
906
1480
  }
907
1481
  /**
908
- * Create a slide from a section with bullets
1482
+ * Determine if content is suitable for a three-column layout
1483
+ * (3 pillars, 3 key points, etc.)
909
1484
  */
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
- };
1485
+ isSuitableForThreeColumn(section) {
1486
+ if (section.bullets.length === 3) return true;
1487
+ if (/three|3\s*(pillar|point|key|step)/i.test(section.header)) return true;
1488
+ const threePartPattern = /first[,.].*second[,.].*third/i;
1489
+ if (threePartPattern.test(section.content)) return true;
1490
+ return false;
921
1491
  }
922
1492
  /**
923
- * Create a slide from a section with body content
1493
+ * Check if content should be displayed as a quote slide
924
1494
  */
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
- };
1495
+ isQuoteContent(section) {
1496
+ const content = section.content.trim();
1497
+ if (!this.patterns.quote.test(content)) {
1498
+ return { isQuote: false };
936
1499
  }
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
- };
1500
+ const attrMatch = content.match(/[-—–]\s*(.+)$/);
1501
+ if (attrMatch && attrMatch[1]) {
1502
+ return { isQuote: true, attribution: attrMatch[1].trim() };
1503
+ }
1504
+ return { isQuote: true };
946
1505
  }
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);
1506
+ };
1507
+
1508
+ // src/core/SlideFactory.ts
1509
+ var SlideFactory = class {
1510
+ kb;
1511
+ presentationType;
1512
+ classifier;
1513
+ config;
1514
+ usedContent;
1515
+ usedTitles;
1516
+ constructor(kb, type) {
1517
+ this.kb = kb;
1518
+ this.presentationType = type;
1519
+ this.classifier = new ContentPatternClassifier();
1520
+ this.usedContent = /* @__PURE__ */ new Set();
1521
+ this.usedTitles = /* @__PURE__ */ new Set();
1522
+ this.config = this.loadKBConfig(type);
1523
+ console.log(` \u2713 SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
1524
+ console.log(` \u2713 KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
954
1525
  }
955
1526
  /**
956
- * Create a metrics slide from data points
1527
+ * Load ALL configuration from KB - ZERO hardcoded values after this.
957
1528
  */
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
- }));
1529
+ loadKBConfig(type) {
963
1530
  return {
964
- index,
965
- type: "metrics-grid",
966
- data: {
967
- title: "Key Metrics",
968
- metrics
969
- },
970
- classes: ["slide-metrics-grid"]
1531
+ defaults: this.kb.getSlideDefaults(type),
1532
+ scqaTitles: this.kb.getSCQATitles(type),
1533
+ sparklineTitles: this.kb.getSparklineTitles(type),
1534
+ rules: this.kb.getValidationRules(type),
1535
+ allowedTypes: this.kb.getAllowedSlideTypes(type),
1536
+ mode: this.kb.getModeForType(type),
1537
+ insightMarkers: this.kb.getInsightMarkers(),
1538
+ glanceTest: this.kb.getDuarteGlanceTest(),
1539
+ millersLaw: this.kb.getMillersLaw(),
1540
+ typography: this.kb.getTypographyForType(type),
1541
+ antiPatterns: this.kb.getAntiPatternsForType(type),
1542
+ requiredElements: this.kb.getRequiredElements(type)
971
1543
  };
972
1544
  }
973
1545
  /**
974
- * Create a title slide.
1546
+ * Create all slides from content analysis.
1547
+ * This is the main entry point - orchestrates the entire slide creation process.
975
1548
  */
1549
+ async createSlides(analysis) {
1550
+ const slides = [];
1551
+ const storyStructure = this.kb.getStoryStructure(this.presentationType);
1552
+ console.log(` \u2713 Using ${storyStructure.framework} story framework`);
1553
+ slides.push(this.createTitleSlide(0, analysis));
1554
+ const minSectionsForAgenda = Math.max(3, this.config.rules.bulletsPerSlide.max - 2);
1555
+ if (this.config.mode === "business" && analysis.sections.length >= minSectionsForAgenda) {
1556
+ slides.push(this.createAgendaSlide(slides.length, analysis));
1557
+ }
1558
+ if (storyStructure.framework === "scqa") {
1559
+ this.addSCQASlides(slides, analysis);
1560
+ } else if (storyStructure.framework === "sparkline") {
1561
+ this.addSparklineSlides(slides, analysis);
1562
+ }
1563
+ for (const section of analysis.sections) {
1564
+ const contentKey = this.normalizeKey(section.header);
1565
+ if (this.usedContent.has(contentKey)) continue;
1566
+ this.usedContent.add(contentKey);
1567
+ const pattern = this.classifier.classify(section);
1568
+ const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
1569
+ const slide = this.createSlideByType(slides.length, slideType, section, pattern);
1570
+ if (slide) {
1571
+ slides.push(slide);
1572
+ }
1573
+ }
1574
+ const minDataPointsForMetrics = 2;
1575
+ if (analysis.dataPoints.length >= minDataPointsForMetrics) {
1576
+ const hasMetricsSlide = slides.some(
1577
+ (s) => s.type === "metrics-grid" || s.type === "big-number"
1578
+ );
1579
+ if (!hasMetricsSlide) {
1580
+ slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
1581
+ }
1582
+ }
1583
+ if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
1584
+ slides.push(this.createCTASlide(slides.length, analysis));
1585
+ }
1586
+ slides.push(this.createThankYouSlide(slides.length));
1587
+ const validatedSlides = this.validateAndFixSlides(slides);
1588
+ this.checkRequiredElements(validatedSlides);
1589
+ this.checkAntiPatterns(validatedSlides);
1590
+ console.log(` \u2713 Created ${validatedSlides.length} validated slides`);
1591
+ return validatedSlides;
1592
+ }
1593
+ // ===========================================================================
1594
+ // SLIDE TYPE ROUTER - Routes to appropriate creator based on KB-selected type
1595
+ // ===========================================================================
1596
+ createSlideByType(index, type, section, pattern) {
1597
+ const normalizedType = type.toLowerCase().replace(/_/g, "-");
1598
+ switch (normalizedType) {
1599
+ case "big-number":
1600
+ case "data-insight":
1601
+ return this.createBigNumberSlide(index, section, pattern);
1602
+ case "comparison":
1603
+ case "options-comparison":
1604
+ return this.createComparisonSlide(index, section);
1605
+ case "timeline":
1606
+ case "process-timeline":
1607
+ case "roadmap":
1608
+ return this.createTimelineSlide(index, section);
1609
+ case "process":
1610
+ return this.createProcessSlide(index, section);
1611
+ case "three-column":
1612
+ return this.createThreeColumnSlide(index, section);
1613
+ case "two-column":
1614
+ return this.createTwoColumnSlide(index, section);
1615
+ case "quote":
1616
+ case "testimonial":
1617
+ case "social-proof":
1618
+ return this.createQuoteSlide(index, section);
1619
+ case "metrics-grid":
1620
+ return this.createMetricsGridSlide(index, section.metrics);
1621
+ case "bullet-points":
1622
+ case "detailed-findings":
1623
+ return this.createBulletSlide(index, section);
1624
+ case "single-statement":
1625
+ case "big-idea":
1626
+ return this.createSingleStatementSlide(index, section);
1627
+ case "code-snippet":
1628
+ case "technical":
1629
+ return this.createCodeSlide(index, section);
1630
+ default:
1631
+ console.log(` \u26A0 Unknown slide type '${type}', using bullet-points`);
1632
+ return this.createBulletSlide(index, section);
1633
+ }
1634
+ }
1635
+ // ===========================================================================
1636
+ // STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
1637
+ // ===========================================================================
976
1638
  createTitleSlide(index, analysis) {
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];
1639
+ const data = {
1640
+ title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
1641
+ };
1642
+ if (analysis.keyMessages[0]) {
1643
+ data.subtitle = this.truncateText(
1644
+ analysis.keyMessages[0],
1645
+ this.config.defaults.subtitle.maxWords
1646
+ // FROM KB
1647
+ );
982
1648
  }
983
1649
  return {
984
1650
  index,
985
1651
  type: "title",
986
- data: {
987
- title: analysis.titles[0] ?? "Presentation",
988
- subtitle: this.truncate(subtitle, 80),
989
- keyMessage: analysis.scqa.answer
990
- },
991
- classes: ["slide-title"]
1652
+ data,
1653
+ classes: ["title-slide"]
992
1654
  };
993
1655
  }
994
- /**
995
- * Create an agenda slide.
996
- */
997
1656
  createAgendaSlide(index, analysis) {
1657
+ const agendaItems = analysis.sections.filter((s) => s.level <= 2).slice(0, this.config.rules.bulletsPerSlide.max).map((s) => this.cleanText(s.header));
998
1658
  return {
999
1659
  index,
1000
1660
  type: "agenda",
1001
1661
  data: {
1002
- title: "Agenda",
1003
- bullets: analysis.keyMessages.map((msg, i) => `${i + 1}. ${this.truncate(msg, 50)}`)
1662
+ title: this.config.defaults.agenda.title,
1663
+ // FROM KB - not hardcoded 'Agenda'
1664
+ bullets: agendaItems
1004
1665
  },
1005
- classes: ["slide-agenda"]
1666
+ classes: ["agenda-slide"]
1006
1667
  };
1007
1668
  }
1008
- /**
1009
- * Create a context/situation slide.
1010
- */
1011
- createContextSlide(index, analysis, mode) {
1012
- if (mode === "keynote") {
1013
- return {
1014
- index,
1669
+ createThankYouSlide(index) {
1670
+ return {
1671
+ index,
1672
+ type: "thank-you",
1673
+ data: {
1674
+ title: this.config.defaults.thankYou.title,
1675
+ // FROM KB - not hardcoded 'Thank You'
1676
+ subtitle: this.config.defaults.thankYou.subtitle
1677
+ // FROM KB - not hardcoded 'Questions?'
1678
+ },
1679
+ classes: ["thank-you-slide"]
1680
+ };
1681
+ }
1682
+ // ===========================================================================
1683
+ // STORY FRAMEWORK SLIDES (SCQA, Sparkline) - ALL titles from KB
1684
+ // ===========================================================================
1685
+ addSCQASlides(slides, analysis) {
1686
+ const scqa = analysis.scqa;
1687
+ const titles = this.config.scqaTitles;
1688
+ if (scqa?.situation && !this.usedContent.has("scqa-situation")) {
1689
+ this.usedContent.add("scqa-situation");
1690
+ slides.push({
1691
+ index: slides.length,
1015
1692
  type: "single-statement",
1016
1693
  data: {
1017
- title: this.truncate(analysis.scqa.situation, 80),
1018
- keyMessage: "The current state"
1694
+ title: titles.situation,
1695
+ // FROM KB - not hardcoded 'Current Situation'
1696
+ body: this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max)
1019
1697
  },
1020
- classes: ["slide-single-statement"]
1021
- };
1698
+ classes: ["situation-slide"]
1699
+ });
1700
+ }
1701
+ if (scqa?.complication && !this.usedContent.has("scqa-complication")) {
1702
+ this.usedContent.add("scqa-complication");
1703
+ slides.push({
1704
+ index: slides.length,
1705
+ type: "single-statement",
1706
+ data: {
1707
+ title: titles.complication,
1708
+ // FROM KB - not hardcoded 'The Challenge'
1709
+ body: this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max)
1710
+ },
1711
+ classes: ["complication-slide"]
1712
+ });
1713
+ }
1714
+ if (scqa?.question && !this.usedContent.has("scqa-question")) {
1715
+ this.usedContent.add("scqa-question");
1716
+ slides.push({
1717
+ index: slides.length,
1718
+ type: "single-statement",
1719
+ data: {
1720
+ title: titles.question,
1721
+ // FROM KB - not hardcoded 'The Question'
1722
+ body: this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max)
1723
+ },
1724
+ classes: ["question-slide"]
1725
+ });
1726
+ }
1727
+ }
1728
+ addSparklineSlides(slides, analysis) {
1729
+ const spark = analysis.sparkline;
1730
+ const titles = this.config.sparklineTitles;
1731
+ const whatIsFirst = spark?.whatIs?.[0];
1732
+ if (whatIsFirst && !this.usedContent.has("spark-what-is")) {
1733
+ this.usedContent.add("spark-what-is");
1734
+ slides.push({
1735
+ index: slides.length,
1736
+ type: "single-statement",
1737
+ data: {
1738
+ title: titles.whatIs,
1739
+ // FROM KB - not hardcoded 'Where We Are Today'
1740
+ body: this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max)
1741
+ },
1742
+ classes: ["what-is-slide"]
1743
+ });
1744
+ }
1745
+ const whatCouldBeFirst = spark?.whatCouldBe?.[0];
1746
+ if (whatCouldBeFirst && !this.usedContent.has("spark-could-be")) {
1747
+ this.usedContent.add("spark-could-be");
1748
+ slides.push({
1749
+ index: slides.length,
1750
+ type: "single-statement",
1751
+ data: {
1752
+ title: titles.whatCouldBe,
1753
+ // FROM KB - not hardcoded 'What Could Be'
1754
+ body: this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max)
1755
+ },
1756
+ classes: ["what-could-be-slide"]
1757
+ });
1022
1758
  }
1759
+ }
1760
+ // ===========================================================================
1761
+ // CONTENT SLIDES - ALL values from KB
1762
+ // ===========================================================================
1763
+ createBigNumberSlide(index, section, pattern) {
1764
+ const bigNumber = this.classifier.extractBigNumber(
1765
+ `${section.header} ${section.content} ${section.bullets.join(" ")}`
1766
+ );
1023
1767
  return {
1024
1768
  index,
1025
- type: "two-column",
1769
+ type: "big-number",
1026
1770
  data: {
1027
- title: "Current Situation",
1028
- body: analysis.scqa.situation,
1029
- bullets: analysis.sparkline.whatIs.slice(0, 3)
1771
+ title: this.createTitle(section.header, section),
1772
+ keyMessage: bigNumber?.value || pattern.bigNumberValue || "0",
1773
+ body: bigNumber?.context || this.truncateText(
1774
+ section.content,
1775
+ this.config.defaults.context.maxWords
1776
+ // FROM KB - not hardcoded 30
1777
+ )
1030
1778
  },
1031
- classes: ["slide-two-column"]
1779
+ classes: ["big-number-slide"]
1032
1780
  };
1033
1781
  }
1034
- /**
1035
- * Create a problem/complication slide.
1036
- */
1037
- createProblemSlide(index, analysis, mode) {
1038
- if (mode === "keynote") {
1039
- return {
1040
- index,
1041
- type: "big-idea",
1042
- data: {
1043
- title: this.truncate(analysis.scqa.complication, 60),
1044
- keyMessage: "The challenge we face"
1045
- },
1046
- classes: ["slide-big-idea"]
1047
- };
1048
- }
1782
+ createComparisonSlide(index, section) {
1783
+ const comparison = this.classifier.extractComparison(section);
1784
+ const labels = this.config.defaults.comparison;
1785
+ const leftFallback = labels.optionLabels[0] ?? "Option A";
1786
+ const rightFallback = labels.optionLabels[1] ?? "Option B";
1787
+ const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
1788
+ const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
1049
1789
  return {
1050
1790
  index,
1051
- type: "bullet-points",
1791
+ type: "comparison",
1052
1792
  data: {
1053
- title: "The Challenge",
1054
- body: analysis.scqa.complication,
1055
- bullets: this.extractBullets(analysis.scqa.complication)
1793
+ title: this.createTitle(section.header, section),
1794
+ columns: [
1795
+ {
1796
+ title: labels.leftLabel,
1797
+ // FROM KB - not hardcoded 'Before'
1798
+ content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
1799
+ },
1800
+ {
1801
+ title: labels.rightLabel,
1802
+ // FROM KB - not hardcoded 'After'
1803
+ content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
1804
+ }
1805
+ ]
1056
1806
  },
1057
- classes: ["slide-bullet-points"]
1807
+ classes: ["comparison-slide"]
1058
1808
  };
1059
1809
  }
1060
- /**
1061
- * Create a key message slide.
1062
- */
1063
- createMessageSlide(index, message, mode) {
1064
- if (mode === "keynote") {
1065
- return {
1066
- index,
1067
- type: "single-statement",
1068
- data: {
1069
- title: this.truncate(message, 60),
1070
- keyMessage: message
1071
- },
1072
- classes: ["slide-single-statement"]
1073
- };
1074
- }
1810
+ createTimelineSlide(index, section) {
1811
+ const steps = this.classifier.extractSteps(section);
1812
+ const maxSteps = Math.min(
1813
+ steps.length,
1814
+ this.config.rules.bulletsPerSlide.max,
1815
+ this.config.millersLaw.maxItems
1816
+ // FROM KB - 7±2 rule
1817
+ );
1075
1818
  return {
1076
1819
  index,
1077
- type: "bullet-points",
1820
+ type: "timeline",
1078
1821
  data: {
1079
- title: this.extractActionTitle(message),
1080
- body: message,
1081
- bullets: this.extractBullets(message)
1822
+ title: this.createTitle(section.header, section),
1823
+ steps: steps.slice(0, maxSteps).map((step) => ({
1824
+ label: step.label,
1825
+ description: this.truncateText(
1826
+ step.description,
1827
+ this.config.defaults.step.maxWords
1828
+ // FROM KB - not hardcoded 20
1829
+ )
1830
+ }))
1082
1831
  },
1083
- classes: ["slide-bullet-points"]
1832
+ classes: ["timeline-slide"]
1084
1833
  };
1085
1834
  }
1086
- /**
1087
- * Create a STAR moment slide.
1088
- */
1089
- createStarMomentSlide(index, starMoment, mode) {
1090
- const statMatch = starMoment.match(/(\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion))?)/);
1091
- if (statMatch && statMatch[1]) {
1092
- const stat = statMatch[1];
1093
- return {
1094
- index,
1095
- type: "big-number",
1096
- data: {
1097
- title: stat,
1098
- subtitle: this.removeStatistic(starMoment, stat),
1099
- keyMessage: starMoment
1100
- },
1101
- classes: ["slide-big-number"]
1102
- };
1835
+ createProcessSlide(index, section) {
1836
+ const steps = this.classifier.extractSteps(section);
1837
+ const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
1838
+ return {
1839
+ index,
1840
+ type: "process",
1841
+ data: {
1842
+ title: this.createTitle(section.header, section),
1843
+ steps: steps.slice(0, maxSteps).map((step, i) => ({
1844
+ number: i + 1,
1845
+ title: step.label,
1846
+ description: this.truncateText(
1847
+ step.description,
1848
+ this.config.defaults.step.maxWords
1849
+ // FROM KB - not hardcoded 15
1850
+ )
1851
+ }))
1852
+ },
1853
+ classes: ["process-slide"]
1854
+ };
1855
+ }
1856
+ createThreeColumnSlide(index, section) {
1857
+ const columns = section.bullets.slice(0, 3);
1858
+ while (columns.length < 3) {
1859
+ columns.push("");
1103
1860
  }
1104
- if (mode === "keynote") {
1105
- return {
1106
- index,
1107
- type: "big-idea",
1108
- data: {
1109
- title: this.truncate(starMoment, 80),
1110
- keyMessage: "A key insight"
1861
+ const labelTemplate = this.config.defaults.column.labelTemplate;
1862
+ return {
1863
+ index,
1864
+ type: "three-column",
1865
+ data: {
1866
+ title: this.createTitle(section.header, section),
1867
+ columns: columns.map((content, i) => ({
1868
+ title: labelTemplate.replace("{n}", String(i + 1)),
1869
+ // FROM KB - not hardcoded 'Point ${i+1}'
1870
+ content: this.truncateText(
1871
+ content,
1872
+ this.config.defaults.columnContent.maxWords
1873
+ // FROM KB - not hardcoded 25
1874
+ )
1875
+ }))
1876
+ },
1877
+ classes: ["three-column-slide"]
1878
+ };
1879
+ }
1880
+ createTwoColumnSlide(index, section) {
1881
+ const midpoint = Math.ceil(section.bullets.length / 2);
1882
+ const leftBullets = section.bullets.slice(0, midpoint);
1883
+ const rightBullets = section.bullets.slice(midpoint);
1884
+ const wordsPerBullet = Math.floor(
1885
+ this.config.rules.wordsPerSlide.max / this.config.rules.bulletsPerSlide.max
1886
+ );
1887
+ return {
1888
+ index,
1889
+ type: "two-column",
1890
+ data: {
1891
+ title: this.createTitle(section.header, section),
1892
+ leftColumn: {
1893
+ bullets: leftBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
1111
1894
  },
1112
- classes: ["slide-big-idea"]
1113
- };
1895
+ rightColumn: {
1896
+ bullets: rightBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
1897
+ }
1898
+ },
1899
+ classes: ["two-column-slide"]
1900
+ };
1901
+ }
1902
+ createQuoteSlide(index, section) {
1903
+ const quoteInfo = this.classifier.isQuoteContent(section);
1904
+ const quoteText = section.content.replace(/^[""]|[""]$/g, "").trim();
1905
+ const quoteWordLimit = Math.min(
1906
+ this.config.glanceTest.wordLimit * 2,
1907
+ // FROM KB - not hardcoded
1908
+ this.config.rules.wordsPerSlide.max
1909
+ );
1910
+ const data = {
1911
+ title: this.createTitle(section.header, section),
1912
+ quote: this.truncateText(quoteText, quoteWordLimit)
1913
+ };
1914
+ if (quoteInfo.attribution) {
1915
+ data.attribution = quoteInfo.attribution;
1114
1916
  }
1115
1917
  return {
1116
1918
  index,
1117
1919
  type: "quote",
1920
+ data,
1921
+ classes: ["quote-slide"]
1922
+ };
1923
+ }
1924
+ createMetricsGridSlide(index, metrics) {
1925
+ const maxMetrics = this.config.defaults.metricsGrid.maxMetrics;
1926
+ const cleanedMetrics = metrics.slice(0, maxMetrics).map((m) => ({
1927
+ value: this.cleanText(m.value),
1928
+ label: this.cleanMetricLabel(m.label)
1929
+ }));
1930
+ return {
1931
+ index,
1932
+ type: "metrics-grid",
1118
1933
  data: {
1119
- quote: starMoment,
1120
- attribution: "Key Insight"
1934
+ title: this.config.defaults.metricsGrid.title,
1935
+ // FROM KB - not hardcoded 'Key Metrics'
1936
+ metrics: cleanedMetrics
1121
1937
  },
1122
- classes: ["slide-quote"]
1938
+ classes: ["metrics-grid-slide"]
1123
1939
  };
1124
1940
  }
1125
- /**
1126
- * Create a solution/answer slide.
1127
- */
1128
- createSolutionSlide(index, analysis, mode) {
1129
- if (mode === "keynote") {
1130
- return {
1131
- index,
1132
- type: "big-idea",
1133
- data: {
1134
- title: this.truncate(analysis.scqa.answer, 60),
1135
- keyMessage: "Our answer"
1136
- },
1137
- classes: ["slide-big-idea"]
1138
- };
1941
+ createBulletSlide(index, section) {
1942
+ const maxBullets = this.config.rules.bulletsPerSlide.max;
1943
+ const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
1944
+ const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
1945
+ if (cleanedBullets.length === 0 && section.content) {
1946
+ return this.createSingleStatementSlide(index, section);
1139
1947
  }
1140
1948
  return {
1141
1949
  index,
1142
- type: "two-column",
1950
+ type: "bullet-points",
1143
1951
  data: {
1144
- title: "The Solution",
1145
- body: analysis.scqa.answer,
1146
- bullets: analysis.sparkline.whatCouldBe.slice(0, 4)
1952
+ title: this.createTitle(section.header, section),
1953
+ bullets: cleanedBullets
1147
1954
  },
1148
- classes: ["slide-two-column"]
1955
+ classes: ["bullet-points-slide"]
1149
1956
  };
1150
1957
  }
1151
- /**
1152
- * Create a call-to-action slide.
1153
- */
1154
- createCTASlide(index, analysis, mode) {
1958
+ createSingleStatementSlide(index, section) {
1155
1959
  return {
1156
1960
  index,
1157
- type: "cta",
1961
+ type: "single-statement",
1158
1962
  data: {
1159
- title: mode === "keynote" ? "Take Action" : "Next Steps",
1160
- body: analysis.sparkline.callToAdventure,
1161
- keyMessage: "What we need from you"
1963
+ title: this.createTitle(section.header, section),
1964
+ body: this.truncateText(
1965
+ section.content || section.bullets[0] || "",
1966
+ this.config.rules.wordsPerSlide.max
1967
+ // FROM KB
1968
+ )
1162
1969
  },
1163
- classes: ["slide-cta"]
1970
+ classes: ["single-statement-slide"]
1164
1971
  };
1165
1972
  }
1166
- /**
1167
- * Create a thank you slide.
1168
- */
1169
- createThankYouSlide(index) {
1973
+ createCodeSlide(index, section) {
1974
+ const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
1975
+ const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
1170
1976
  return {
1171
1977
  index,
1172
- type: "thank-you",
1978
+ type: "two-column",
1979
+ // Code slides use two-column layout
1173
1980
  data: {
1174
- title: "Thank You",
1175
- subtitle: "Questions?"
1981
+ title: this.createTitle(section.header, section),
1982
+ leftColumn: {
1983
+ body: this.config.defaults.code.label
1984
+ // FROM KB - not hardcoded 'Code Example'
1985
+ },
1986
+ rightColumn: {
1987
+ body: code.slice(0, this.config.defaults.code.maxChars)
1988
+ // FROM KB - not hardcoded 500
1989
+ }
1176
1990
  },
1177
- classes: ["slide-thank-you"]
1991
+ classes: ["code-slide"]
1178
1992
  };
1179
1993
  }
1180
- /**
1181
- * Initialize slide templates with constraints.
1182
- */
1183
- initializeTemplates() {
1184
- const templates = /* @__PURE__ */ new Map();
1185
- templates.set("title", {
1186
- type: "title",
1187
- requiredFields: ["title"],
1188
- optionalFields: ["subtitle", "author", "date"],
1189
- keynoteSuitable: true,
1190
- businessSuitable: true,
1191
- maxWords: 15
1192
- });
1193
- templates.set("big-idea", {
1194
- type: "big-idea",
1195
- requiredFields: ["title"],
1196
- optionalFields: ["keyMessage"],
1197
- keynoteSuitable: true,
1198
- businessSuitable: false,
1199
- maxWords: 10
1200
- });
1201
- templates.set("single-statement", {
1202
- type: "single-statement",
1203
- requiredFields: ["title"],
1204
- optionalFields: ["keyMessage"],
1205
- keynoteSuitable: true,
1206
- businessSuitable: false,
1207
- maxWords: 15
1208
- });
1209
- templates.set("big-number", {
1210
- type: "big-number",
1211
- requiredFields: ["title"],
1212
- optionalFields: ["subtitle", "source"],
1213
- keynoteSuitable: true,
1214
- businessSuitable: true,
1215
- maxWords: 10
1216
- });
1217
- templates.set("quote", {
1218
- type: "quote",
1219
- requiredFields: ["quote"],
1220
- optionalFields: ["attribution", "source"],
1221
- keynoteSuitable: true,
1222
- businessSuitable: true,
1223
- maxWords: 30
1224
- });
1225
- templates.set("bullet-points", {
1226
- type: "bullet-points",
1227
- requiredFields: ["title", "bullets"],
1228
- optionalFields: ["body"],
1229
- keynoteSuitable: false,
1230
- businessSuitable: true,
1231
- maxWords: 80
1232
- });
1233
- templates.set("two-column", {
1234
- type: "two-column",
1235
- requiredFields: ["title"],
1236
- optionalFields: ["body", "bullets", "images"],
1237
- keynoteSuitable: false,
1238
- businessSuitable: true,
1239
- maxWords: 100
1240
- });
1241
- templates.set("agenda", {
1242
- type: "agenda",
1243
- requiredFields: ["title", "bullets"],
1244
- optionalFields: [],
1245
- keynoteSuitable: false,
1246
- businessSuitable: true,
1247
- maxWords: 50
1248
- });
1249
- templates.set("cta", {
1994
+ createCTASlide(index, analysis) {
1995
+ const ctaDefaults = this.config.defaults.cta;
1996
+ const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
1997
+ return {
1998
+ index,
1250
1999
  type: "cta",
1251
- requiredFields: ["title"],
1252
- optionalFields: ["body", "keyMessage"],
1253
- keynoteSuitable: true,
1254
- businessSuitable: true,
1255
- maxWords: 30
1256
- });
1257
- templates.set("thank-you", {
1258
- type: "thank-you",
1259
- requiredFields: ["title"],
1260
- optionalFields: ["subtitle"],
1261
- keynoteSuitable: true,
1262
- businessSuitable: true,
1263
- maxWords: 10
1264
- });
1265
- return templates;
2000
+ data: {
2001
+ title: ctaDefaults.title,
2002
+ // FROM KB - not hardcoded 'Next Steps'
2003
+ body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
2004
+ keyMessage: ctaDefaults.message
2005
+ // FROM KB - not hardcoded 'Ready to Begin?'
2006
+ },
2007
+ classes: ["cta-slide"]
2008
+ };
2009
+ }
2010
+ // ===========================================================================
2011
+ // VALIDATION - Ensure all slides comply with KB rules AND FIX ISSUES
2012
+ // ===========================================================================
2013
+ validateAndFixSlides(slides) {
2014
+ const validatedSlides = [];
2015
+ for (const slide of slides) {
2016
+ const wordCount = this.countWords(slide);
2017
+ const bulletCount = slide.data.bullets?.length || 0;
2018
+ const validation = this.kb.validateSlideAgainstKB(
2019
+ {
2020
+ type: slide.type,
2021
+ wordCount,
2022
+ bulletCount,
2023
+ hasActionTitle: this.isActionTitle(slide.data.title),
2024
+ hasSource: !!slide.data.source
2025
+ },
2026
+ this.presentationType
2027
+ );
2028
+ if (validation.fixes.wordCount) {
2029
+ if (slide.data.body) {
2030
+ slide.data.body = this.truncateText(slide.data.body, validation.fixes.wordCount);
2031
+ }
2032
+ if (slide.data.bullets) {
2033
+ const wordsPerBullet = Math.floor(validation.fixes.wordCount / slide.data.bullets.length);
2034
+ slide.data.bullets = slide.data.bullets.map((b) => this.truncateText(b, wordsPerBullet));
2035
+ }
2036
+ }
2037
+ if (validation.fixes.bulletCount && slide.data.bullets) {
2038
+ slide.data.bullets = slide.data.bullets.slice(0, validation.fixes.bulletCount);
2039
+ }
2040
+ if (validation.violations.length > 0) {
2041
+ console.log(` \u26A0 Slide ${slide.index} (${slide.type}): Fixed ${Object.keys(validation.fixes).length} issues, ${validation.violations.length} remaining`);
2042
+ }
2043
+ if (validation.warnings.length > 0) {
2044
+ slide.notes = (slide.notes || "") + "\nWarnings: " + validation.warnings.join(", ");
2045
+ }
2046
+ validatedSlides.push(slide);
2047
+ }
2048
+ return validatedSlides;
1266
2049
  }
1267
- // === Helper Methods ===
1268
2050
  /**
1269
- * Clean text by removing all markdown and content markers.
1270
- * CRITICAL: Must strip all formatting to prevent garbage in slides
2051
+ * Check that required elements exist in the deck (from KB)
1271
2052
  */
1272
- cleanText(text) {
1273
- if (!text) return "";
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();
2053
+ checkRequiredElements(slides) {
2054
+ const required = this.config.requiredElements;
2055
+ if (required.length === 0) return;
2056
+ const slideTypes = slides.map((s) => s.type);
2057
+ const missingElements = [];
2058
+ for (const element of required) {
2059
+ const lowerElement = element.toLowerCase();
2060
+ if (lowerElement.includes("hook") || lowerElement.includes("opening")) {
2061
+ const titleSlide = slides.find((s) => s.type === "title");
2062
+ if (!titleSlide || !titleSlide.data.subtitle) {
2063
+ missingElements.push(element);
2064
+ }
2065
+ }
2066
+ if (lowerElement.includes("call to action") || lowerElement.includes("cta")) {
2067
+ if (!slideTypes.includes("cta")) {
2068
+ missingElements.push(element);
2069
+ }
2070
+ }
2071
+ if (lowerElement.includes("star moment") || lowerElement.includes("memorable")) {
2072
+ const hasStarMoment = slideTypes.some(
2073
+ (t) => t === "big-number" || t === "quote" || t === "single-statement"
2074
+ );
2075
+ if (!hasStarMoment) {
2076
+ missingElements.push(element);
2077
+ }
2078
+ }
2079
+ }
2080
+ if (missingElements.length > 0) {
2081
+ console.log(` \u26A0 Missing required elements: ${missingElements.join(", ")}`);
2082
+ }
1275
2083
  }
1276
2084
  /**
1277
- * Truncate text to max length at sentence boundary when possible.
1278
- * CRITICAL: Never cut mid-number (99.5% should not become 99.)
2085
+ * Check for anti-patterns from KB
1279
2086
  */
1280
- truncate(text, maxLength) {
1281
- const cleanedText = this.cleanText(text);
1282
- if (!cleanedText || cleanedText.length <= maxLength) {
1283
- return cleanedText;
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;
2087
+ checkAntiPatterns(slides) {
2088
+ const antiPatterns = this.config.antiPatterns;
2089
+ if (antiPatterns.length === 0) return;
2090
+ const violations = [];
2091
+ for (const pattern of antiPatterns) {
2092
+ const lowerPattern = pattern.toLowerCase();
2093
+ if (lowerPattern.includes("bullet") && this.config.mode === "keynote") {
2094
+ const bulletSlides = slides.filter(
2095
+ (s) => s.type === "bullet-points" || s.data.bullets && s.data.bullets.length > 3
2096
+ );
2097
+ if (bulletSlides.length > 0) {
2098
+ violations.push(`${pattern} (${bulletSlides.length} slides)`);
1293
2099
  }
1294
2100
  }
1295
- if (result.length > maxLength * 0.5) {
1296
- return result.trim();
2101
+ if (lowerPattern.includes("text") || lowerPattern.includes("overload")) {
2102
+ const overloadedSlides = slides.filter((s) => {
2103
+ const wordCount = this.countWords(s);
2104
+ return wordCount > this.config.glanceTest.wordLimit;
2105
+ });
2106
+ if (overloadedSlides.length > 0) {
2107
+ violations.push(`${pattern} (${overloadedSlides.length} slides exceed glance test)`);
2108
+ }
1297
2109
  }
1298
2110
  }
1299
- const truncated = cleanedText.slice(0, maxLength);
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(" ");
2111
+ if (violations.length > 0) {
2112
+ console.log(` \u26A0 Anti-pattern violations: ${violations.join(", ")}`);
2113
+ }
2114
+ }
2115
+ // ===========================================================================
2116
+ // HELPER METHODS - All use KB configuration
2117
+ // ===========================================================================
2118
+ /**
2119
+ * Create a title for a slide - uses action titles for business mode per KB
2120
+ */
2121
+ createTitle(header, section) {
2122
+ const cleanHeader = this.cleanText(header);
2123
+ if (this.usedTitles.has(cleanHeader.toLowerCase())) {
2124
+ return `${cleanHeader} (continued)`;
1304
2125
  }
1305
- if (lastSpace > maxLength * 0.5) {
1306
- return truncated.slice(0, lastSpace) + "...";
2126
+ this.usedTitles.add(cleanHeader.toLowerCase());
2127
+ if (this.config.rules.actionTitlesRequired) {
2128
+ return this.createActionTitle(cleanHeader, section);
1307
2129
  }
1308
- return truncated + "...";
2130
+ return cleanHeader;
1309
2131
  }
1310
2132
  /**
1311
- * Extract an action title from a message.
2133
+ * Create an action title (Minto/McKinsey style)
2134
+ * Title should communicate the conclusion, not the topic
1312
2135
  */
1313
- extractActionTitle(message) {
1314
- const cleanedMessage = this.cleanText(message);
1315
- const firstSentence = cleanedMessage.split(/[.!?]/)[0];
1316
- if (firstSentence && firstSentence.length <= 50) {
1317
- return firstSentence;
2136
+ createActionTitle(header, section) {
2137
+ const titleWordLimit = this.kb.getWordLimitForElement(this.presentationType, "title");
2138
+ const firstBullet = section.bullets[0];
2139
+ if (firstBullet && this.isInsightful(firstBullet)) {
2140
+ return this.truncateText(this.cleanText(firstBullet), titleWordLimit);
2141
+ }
2142
+ if (section.content && this.isInsightful(section.content)) {
2143
+ const sentences = section.content.split(/[.!?]/);
2144
+ const firstSentence = sentences[0];
2145
+ if (firstSentence) {
2146
+ return this.truncateText(this.cleanText(firstSentence), titleWordLimit);
2147
+ }
1318
2148
  }
1319
- const words = cleanedMessage.split(/\s+/).slice(0, 6);
1320
- return words.join(" ");
2149
+ return header;
2150
+ }
2151
+ /**
2152
+ * Check if text contains an insight (not just a topic) - uses KB markers
2153
+ */
2154
+ isInsightful(text) {
2155
+ const lowerText = text.toLowerCase();
2156
+ return this.config.insightMarkers.some(
2157
+ (marker) => (
2158
+ // FROM KB - not hardcoded array
2159
+ lowerText.includes(marker.toLowerCase())
2160
+ )
2161
+ );
2162
+ }
2163
+ /**
2164
+ * Check if a title is an action title
2165
+ */
2166
+ isActionTitle(title) {
2167
+ if (!title) return false;
2168
+ return this.isInsightful(title);
2169
+ }
2170
+ /**
2171
+ * Count words in a slide
2172
+ */
2173
+ countWords(slide) {
2174
+ let text = "";
2175
+ const data = slide.data;
2176
+ if (data.title) text += data.title + " ";
2177
+ if (data.subtitle) text += data.subtitle + " ";
2178
+ if (data.body) text += data.body + " ";
2179
+ if (data.keyMessage) text += data.keyMessage + " ";
2180
+ if (data.bullets) text += data.bullets.join(" ") + " ";
2181
+ if (data.quote) text += data.quote + " ";
2182
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
1321
2183
  }
1322
2184
  /**
1323
- * Extract bullet points from text.
2185
+ * Truncate text to word limit at sentence boundaries
1324
2186
  */
1325
- extractBullets(text) {
1326
- if (!text) return [];
1327
- const bulletMatches = text.match(/\[BULLET\]\s*(.+)/g);
1328
- if (bulletMatches && bulletMatches.length > 0) {
1329
- return bulletMatches.map((b) => this.cleanText(b)).slice(0, 5);
2187
+ truncateText(text, maxWords) {
2188
+ const cleaned = this.cleanText(text);
2189
+ const words = cleaned.split(/\s+/);
2190
+ if (words.length <= maxWords) {
2191
+ return cleaned;
2192
+ }
2193
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
2194
+ let result = "";
2195
+ for (const sentence of sentences) {
2196
+ const testResult = result + sentence;
2197
+ if (testResult.split(/\s+/).length <= maxWords) {
2198
+ result = testResult;
2199
+ } else {
2200
+ break;
2201
+ }
1330
2202
  }
1331
- const cleanedText = this.cleanText(text);
1332
- const sentences = cleanedText.split(/[.!?]+/).filter((s) => s.trim().length > 10);
1333
- return sentences.slice(0, 5).map((s) => s.trim());
2203
+ if (result.trim().length > 0) {
2204
+ return result.trim();
2205
+ }
2206
+ return words.slice(0, maxWords).join(" ") + "...";
2207
+ }
2208
+ /**
2209
+ * Clean text by removing content markers and normalizing
2210
+ */
2211
+ cleanText(text) {
2212
+ if (!text) return "";
2213
+ 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(/\*\*/g, "").replace(/\*/g, "").replace(/#{1,6}\s*/g, "").replace(/\s+/g, " ").trim();
2214
+ }
2215
+ /**
2216
+ * Clean metric labels (strip table syntax)
2217
+ */
2218
+ cleanMetricLabel(label) {
2219
+ if (!label) return "";
2220
+ return label.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/^\s*\d+\.\s*/, "").trim();
1334
2221
  }
1335
2222
  /**
1336
- * Remove a statistic from text and clean thoroughly.
2223
+ * Normalize a key for deduplication
1337
2224
  */
1338
- removeStatistic(text, stat) {
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);
2225
+ normalizeKey(text) {
2226
+ return text.toLowerCase().replace(/[^a-z0-9]/g, "");
1342
2227
  }
1343
2228
  };
2229
+ function createSlideFactory(kb, type) {
2230
+ return new SlideFactory(kb, type);
2231
+ }
1344
2232
 
1345
2233
  // src/core/TemplateEngine.ts
1346
2234
  import Handlebars from "handlebars";
@@ -2751,99 +3639,1044 @@ var QAEngine = class {
2751
3639
  score -= Math.round((1 - signalPass / slideCount) * 30);
2752
3640
  return Math.max(0, score);
2753
3641
  }
2754
- calculateExpertScore(results) {
2755
- const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
2756
- const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
2757
- return totalScore / experts.length;
3642
+ calculateExpertScore(results) {
3643
+ const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
3644
+ const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
3645
+ return totalScore / experts.length;
3646
+ }
3647
+ calculateA11yScore(results) {
3648
+ let score = 100;
3649
+ score -= Math.min(40, results.fontSizeIssues.length * 10);
3650
+ score -= Math.min(40, results.contrastIssues.length * 10);
3651
+ if (results.focusCoverage < 1) score -= 20;
3652
+ return Math.max(0, score);
3653
+ }
3654
+ // ===========================================================================
3655
+ // ISSUE COLLECTION
3656
+ // ===========================================================================
3657
+ collectIssues(visual, content, expert, accessibility) {
3658
+ const issues = [];
3659
+ visual.perSlide.forEach((slide) => {
3660
+ slide.issues.forEach((issue) => {
3661
+ issues.push({
3662
+ severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
3663
+ category: "visual",
3664
+ slideIndex: slide.slideIndex,
3665
+ message: issue
3666
+ });
3667
+ });
3668
+ });
3669
+ content.perSlide.forEach((slide) => {
3670
+ slide.issues.forEach((issue) => {
3671
+ issues.push({
3672
+ severity: "warning",
3673
+ category: "content",
3674
+ slideIndex: slide.slideIndex,
3675
+ message: issue
3676
+ });
3677
+ });
3678
+ });
3679
+ content.glanceTest.filter((g) => !g.passed).forEach((g) => {
3680
+ const issue = {
3681
+ severity: "warning",
3682
+ category: "content",
3683
+ slideIndex: g.slideIndex,
3684
+ message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
3685
+ };
3686
+ if (g.recommendation) {
3687
+ issue.suggestion = g.recommendation;
3688
+ }
3689
+ issues.push(issue);
3690
+ });
3691
+ [expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
3692
+ e.violations.forEach((v) => {
3693
+ issues.push({
3694
+ severity: "warning",
3695
+ category: "expert",
3696
+ message: `${e.expertName}: ${v}`
3697
+ });
3698
+ });
3699
+ });
3700
+ accessibility.fontSizeIssues.forEach((issue) => {
3701
+ issues.push({
3702
+ severity: "error",
3703
+ category: "accessibility",
3704
+ slideIndex: issue.slideIndex,
3705
+ message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
3706
+ suggestion: `Increase font size to at least ${issue.minimumSize}px`
3707
+ });
3708
+ });
3709
+ accessibility.contrastIssues.forEach((issue) => {
3710
+ issues.push({
3711
+ severity: "error",
3712
+ category: "accessibility",
3713
+ slideIndex: issue.slideIndex,
3714
+ message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
3715
+ suggestion: "Increase contrast between text and background"
3716
+ });
3717
+ });
3718
+ return issues;
3719
+ }
3720
+ // ===========================================================================
3721
+ // BROWSER MANAGEMENT
3722
+ // ===========================================================================
3723
+ async initBrowser() {
3724
+ if (!this.browser) {
3725
+ this.browser = await chromium.launch({ headless: true });
3726
+ }
3727
+ }
3728
+ async closeBrowser() {
3729
+ if (this.browser) {
3730
+ await this.browser.close();
3731
+ this.browser = null;
3732
+ }
3733
+ }
3734
+ };
3735
+
3736
+ // src/qa/SevenDimensionScorer.ts
3737
+ var DIMENSION_WEIGHTS = {
3738
+ layout: 0.15,
3739
+ contrast: 0.15,
3740
+ graphics: 0.1,
3741
+ content: 0.2,
3742
+ clarity: 0.15,
3743
+ effectiveness: 0.15,
3744
+ consistency: 0.1
3745
+ };
3746
+ var SevenDimensionScorer = class {
3747
+ kb;
3748
+ mode;
3749
+ presentationType;
3750
+ constructor(mode, presentationType) {
3751
+ this.mode = mode;
3752
+ this.presentationType = presentationType;
3753
+ }
3754
+ /**
3755
+ * Score a presentation across all 7 dimensions.
3756
+ */
3757
+ async score(slides, html, threshold = 95) {
3758
+ this.kb = await getKnowledgeGateway();
3759
+ const layout = await this.scoreLayout(slides, html);
3760
+ const contrast = await this.scoreContrast(html);
3761
+ const graphics = await this.scoreGraphics(slides);
3762
+ const content = await this.scoreContent(slides);
3763
+ const clarity = await this.scoreClarity(slides);
3764
+ const effectiveness = await this.scoreEffectiveness(slides);
3765
+ const consistency = await this.scoreConsistency(slides, html);
3766
+ const overallScore = Math.round(
3767
+ layout.score * DIMENSION_WEIGHTS.layout + contrast.score * DIMENSION_WEIGHTS.contrast + graphics.score * DIMENSION_WEIGHTS.graphics + content.score * DIMENSION_WEIGHTS.content + clarity.score * DIMENSION_WEIGHTS.clarity + effectiveness.score * DIMENSION_WEIGHTS.effectiveness + consistency.score * DIMENSION_WEIGHTS.consistency
3768
+ );
3769
+ const issues = [
3770
+ ...layout.issues,
3771
+ ...contrast.issues,
3772
+ ...graphics.issues,
3773
+ ...content.issues,
3774
+ ...clarity.issues,
3775
+ ...effectiveness.issues,
3776
+ ...consistency.issues
3777
+ ];
3778
+ return {
3779
+ overallScore,
3780
+ dimensions: {
3781
+ layout,
3782
+ contrast,
3783
+ graphics,
3784
+ content,
3785
+ clarity,
3786
+ effectiveness,
3787
+ consistency
3788
+ },
3789
+ issues,
3790
+ passed: overallScore >= threshold,
3791
+ threshold
3792
+ };
3793
+ }
3794
+ /**
3795
+ * Score layout dimension (whitespace, visual balance, structure)
3796
+ */
3797
+ async scoreLayout(slides, html) {
3798
+ const issues = [];
3799
+ let totalScore = 0;
3800
+ let checks = 0;
3801
+ const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
3802
+ const maxWhitespace = 0.6;
3803
+ const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
3804
+ for (let i = 0; i < slideSections.length; i++) {
3805
+ const section = slideSections[i];
3806
+ if (!section) continue;
3807
+ const textContent = section.replace(/<[^>]+>/g, "").trim();
3808
+ const totalArea = 1920 * 1080;
3809
+ const estimatedTextArea = textContent.length * 100;
3810
+ const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
3811
+ if (whitespaceRatio < minWhitespace) {
3812
+ issues.push({
3813
+ slideIndex: i,
3814
+ dimension: "layout",
3815
+ severity: "error",
3816
+ message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
3817
+ currentValue: whitespaceRatio,
3818
+ expectedValue: minWhitespace,
3819
+ autoFixable: true,
3820
+ fixSuggestion: "Reduce content or increase margins"
3821
+ });
3822
+ totalScore += 50;
3823
+ } else if (whitespaceRatio > maxWhitespace) {
3824
+ issues.push({
3825
+ slideIndex: i,
3826
+ dimension: "layout",
3827
+ severity: "warning",
3828
+ message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
3829
+ currentValue: whitespaceRatio,
3830
+ expectedValue: maxWhitespace,
3831
+ autoFixable: false,
3832
+ fixSuggestion: "Add more content or reduce margins"
3833
+ });
3834
+ totalScore += 80;
3835
+ } else {
3836
+ totalScore += 100;
3837
+ }
3838
+ checks++;
3839
+ }
3840
+ for (let i = 0; i < slides.length; i++) {
3841
+ const slide = slides[i];
3842
+ if (!slide) continue;
3843
+ if (!["thank-you", "section-divider"].includes(slide.type)) {
3844
+ if (!slide.data.title || slide.data.title.trim().length === 0) {
3845
+ issues.push({
3846
+ slideIndex: i,
3847
+ dimension: "layout",
3848
+ severity: "warning",
3849
+ message: `Slide ${i + 1}: Missing title`,
3850
+ autoFixable: false,
3851
+ fixSuggestion: "Add a clear slide title"
3852
+ });
3853
+ totalScore += 70;
3854
+ } else {
3855
+ totalScore += 100;
3856
+ }
3857
+ checks++;
3858
+ }
3859
+ }
3860
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
3861
+ return {
3862
+ name: "Layout",
3863
+ score,
3864
+ weight: DIMENSION_WEIGHTS.layout,
3865
+ issues,
3866
+ details: {
3867
+ slidesAnalyzed: slides.length,
3868
+ whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
3869
+ }
3870
+ };
3871
+ }
3872
+ /**
3873
+ * Score contrast dimension (WCAG compliance, readability)
3874
+ */
3875
+ async scoreContrast(html) {
3876
+ const issues = [];
3877
+ let score = 100;
3878
+ const minContrastRatio = 4.5;
3879
+ const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
3880
+ const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
3881
+ const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
3882
+ const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
3883
+ if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
3884
+ issues.push({
3885
+ slideIndex: -1,
3886
+ dimension: "contrast",
3887
+ severity: "error",
3888
+ message: "Low contrast text color detected (gray text)",
3889
+ currentValue: "gray",
3890
+ expectedValue: "High contrast color",
3891
+ autoFixable: true,
3892
+ fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
3893
+ });
3894
+ score -= 20;
3895
+ }
3896
+ if (!hasGoodContrast && !hasDarkBackground) {
3897
+ issues.push({
3898
+ slideIndex: -1,
3899
+ dimension: "contrast",
3900
+ severity: "warning",
3901
+ message: "Could not verify WCAG-compliant contrast ratios",
3902
+ autoFixable: false,
3903
+ fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
3904
+ });
3905
+ score -= 10;
3906
+ }
3907
+ const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
3908
+ if (hasSmallFont) {
3909
+ issues.push({
3910
+ slideIndex: -1,
3911
+ dimension: "contrast",
3912
+ severity: "error",
3913
+ message: "Font size too small for presentation (< 14px)",
3914
+ currentValue: "Small font",
3915
+ expectedValue: "18px minimum for body text",
3916
+ autoFixable: true,
3917
+ fixSuggestion: "Increase font size to minimum 18px"
3918
+ });
3919
+ score -= 15;
3920
+ }
3921
+ return {
3922
+ name: "Contrast",
3923
+ score: Math.max(0, score),
3924
+ weight: DIMENSION_WEIGHTS.contrast,
3925
+ issues,
3926
+ details: {
3927
+ wcagLevel: "AA",
3928
+ minContrastRatio,
3929
+ colorDefinitions: colorMatches.length,
3930
+ backgroundDefinitions: bgColorMatches.length
3931
+ }
3932
+ };
3933
+ }
3934
+ /**
3935
+ * Score graphics dimension (images, placement, relevance)
3936
+ */
3937
+ async scoreGraphics(slides) {
3938
+ const issues = [];
3939
+ let score = 100;
3940
+ let slidesWithImages = 0;
3941
+ let totalImageSlides = 0;
3942
+ for (let i = 0; i < slides.length; i++) {
3943
+ const slide = slides[i];
3944
+ if (!slide) continue;
3945
+ const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
3946
+ if (shouldHaveImage) {
3947
+ totalImageSlides++;
3948
+ if (slide.data.image || slide.data.backgroundImage) {
3949
+ slidesWithImages++;
3950
+ } else {
3951
+ issues.push({
3952
+ slideIndex: i,
3953
+ dimension: "graphics",
3954
+ severity: "warning",
3955
+ message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
3956
+ autoFixable: false,
3957
+ fixSuggestion: "Add a relevant image or change slide type"
3958
+ });
3959
+ score -= 5;
3960
+ }
3961
+ }
3962
+ }
3963
+ return {
3964
+ name: "Graphics",
3965
+ score: Math.max(0, score),
3966
+ weight: DIMENSION_WEIGHTS.graphics,
3967
+ issues,
3968
+ details: {
3969
+ slidesWithImages,
3970
+ expectedImageSlides: totalImageSlides,
3971
+ imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
3972
+ }
3973
+ };
3974
+ }
3975
+ /**
3976
+ * Score content dimension (word limits, bullet counts, structure)
3977
+ * This is critical - must use KB rules exactly.
3978
+ */
3979
+ async scoreContent(slides) {
3980
+ const issues = [];
3981
+ let totalScore = 0;
3982
+ let checks = 0;
3983
+ const wordLimits = this.kb.getWordLimits(this.mode);
3984
+ const bulletLimits = this.kb.getBulletLimits(this.mode);
3985
+ const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
3986
+ const defaultMinWords = this.mode === "keynote" ? 3 : 15;
3987
+ const defaultMaxBullets = 5;
3988
+ for (let i = 0; i < slides.length; i++) {
3989
+ const slide = slides[i];
3990
+ if (!slide) continue;
3991
+ const wordCount = this.countWords(slide);
3992
+ const slideType = slide.type;
3993
+ const maxWords = wordLimits[slideType] ?? defaultMaxWords;
3994
+ const minWords = defaultMinWords;
3995
+ if (wordCount > maxWords) {
3996
+ const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
3997
+ issues.push({
3998
+ slideIndex: i,
3999
+ dimension: "content",
4000
+ severity,
4001
+ message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
4002
+ currentValue: wordCount,
4003
+ expectedValue: maxWords,
4004
+ autoFixable: true,
4005
+ fixSuggestion: "Condense text to key points only"
4006
+ });
4007
+ totalScore += severity === "error" ? 40 : 70;
4008
+ } else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
4009
+ issues.push({
4010
+ slideIndex: i,
4011
+ dimension: "content",
4012
+ severity: "warning",
4013
+ message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
4014
+ currentValue: wordCount,
4015
+ expectedValue: minWords,
4016
+ autoFixable: false,
4017
+ fixSuggestion: "Add supporting content"
4018
+ });
4019
+ totalScore += 80;
4020
+ } else {
4021
+ totalScore += 100;
4022
+ }
4023
+ checks++;
4024
+ if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
4025
+ const bulletCount = slide.data.bullets.length;
4026
+ const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
4027
+ if (bulletCount > maxBullets) {
4028
+ issues.push({
4029
+ slideIndex: i,
4030
+ dimension: "content",
4031
+ severity: "error",
4032
+ message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
4033
+ currentValue: bulletCount,
4034
+ expectedValue: maxBullets,
4035
+ autoFixable: true,
4036
+ fixSuggestion: "Reduce to top 3-5 key points"
4037
+ });
4038
+ totalScore += 50;
4039
+ } else {
4040
+ totalScore += 100;
4041
+ }
4042
+ checks++;
4043
+ }
4044
+ }
4045
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4046
+ return {
4047
+ name: "Content",
4048
+ score,
4049
+ weight: DIMENSION_WEIGHTS.content,
4050
+ issues,
4051
+ details: {
4052
+ mode: this.mode,
4053
+ wordLimits,
4054
+ bulletLimits,
4055
+ totalSlides: slides.length
4056
+ }
4057
+ };
4058
+ }
4059
+ /**
4060
+ * Score clarity dimension (message focus, information density)
4061
+ */
4062
+ async scoreClarity(slides) {
4063
+ const issues = [];
4064
+ let totalScore = 0;
4065
+ let checks = 0;
4066
+ for (let i = 0; i < slides.length; i++) {
4067
+ const slide = slides[i];
4068
+ if (!slide) continue;
4069
+ if (slide.data.keyMessage) {
4070
+ const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
4071
+ const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
4072
+ if (keyMessageWords > maxKeyMessageWords) {
4073
+ issues.push({
4074
+ slideIndex: i,
4075
+ dimension: "clarity",
4076
+ severity: "warning",
4077
+ message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
4078
+ currentValue: keyMessageWords,
4079
+ expectedValue: maxKeyMessageWords,
4080
+ autoFixable: true,
4081
+ fixSuggestion: "Shorten key message to one impactful sentence"
4082
+ });
4083
+ totalScore += 70;
4084
+ } else {
4085
+ totalScore += 100;
4086
+ }
4087
+ checks++;
4088
+ }
4089
+ if (slide.data.title) {
4090
+ const title = slide.data.title;
4091
+ const titleWords = title.split(/\s+/).length;
4092
+ if (titleWords > 10) {
4093
+ issues.push({
4094
+ slideIndex: i,
4095
+ dimension: "clarity",
4096
+ severity: "warning",
4097
+ message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
4098
+ currentValue: titleWords,
4099
+ expectedValue: "2-8 words",
4100
+ autoFixable: true,
4101
+ fixSuggestion: "Use action-oriented, concise title"
4102
+ });
4103
+ totalScore += 75;
4104
+ } else {
4105
+ totalScore += 100;
4106
+ }
4107
+ checks++;
4108
+ }
4109
+ const elementCount = (slide.data.title ? 1 : 0) + (slide.data.subtitle ? 1 : 0) + (slide.data.body ? 1 : 0) + (slide.data.bullets?.length ?? 0) + (slide.data.keyMessage ? 1 : 0) + (slide.data.image ? 1 : 0);
4110
+ const maxElements = this.mode === "keynote" ? 4 : 6;
4111
+ if (elementCount > maxElements) {
4112
+ issues.push({
4113
+ slideIndex: i,
4114
+ dimension: "clarity",
4115
+ severity: "warning",
4116
+ message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
4117
+ currentValue: elementCount,
4118
+ expectedValue: maxElements,
4119
+ autoFixable: true,
4120
+ fixSuggestion: "Split into multiple slides for clarity"
4121
+ });
4122
+ totalScore += 70;
4123
+ } else {
4124
+ totalScore += 100;
4125
+ }
4126
+ checks++;
4127
+ }
4128
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4129
+ return {
4130
+ name: "Clarity",
4131
+ score,
4132
+ weight: DIMENSION_WEIGHTS.clarity,
4133
+ issues,
4134
+ details: {
4135
+ slidesAnalyzed: slides.length,
4136
+ mode: this.mode
4137
+ }
4138
+ };
4139
+ }
4140
+ /**
4141
+ * Score effectiveness dimension (expert methodology compliance)
4142
+ */
4143
+ async scoreEffectiveness(slides) {
4144
+ const issues = [];
4145
+ let score = 100;
4146
+ if (slides.length >= 3) {
4147
+ const firstSlide = slides[0];
4148
+ const lastSlide = slides[slides.length - 1];
4149
+ if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
4150
+ issues.push({
4151
+ slideIndex: 0,
4152
+ dimension: "effectiveness",
4153
+ severity: "warning",
4154
+ message: "Presentation should start with a title or hero slide",
4155
+ currentValue: firstSlide.type,
4156
+ expectedValue: "title or hero",
4157
+ autoFixable: false,
4158
+ fixSuggestion: "Add a compelling opening slide"
4159
+ });
4160
+ score -= 10;
4161
+ }
4162
+ if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
4163
+ issues.push({
4164
+ slideIndex: slides.length - 1,
4165
+ dimension: "effectiveness",
4166
+ severity: "warning",
4167
+ message: "Presentation should end with a closing or CTA slide",
4168
+ currentValue: lastSlide.type,
4169
+ expectedValue: "thank-you, cta, or closing",
4170
+ autoFixable: false,
4171
+ fixSuggestion: "Add a clear call-to-action or closing"
4172
+ });
4173
+ score -= 10;
4174
+ }
4175
+ }
4176
+ const keyMessages = slides.filter((s) => s.data.keyMessage);
4177
+ if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
4178
+ issues.push({
4179
+ slideIndex: -1,
4180
+ dimension: "effectiveness",
4181
+ severity: "info",
4182
+ message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
4183
+ currentValue: keyMessages.length,
4184
+ expectedValue: 3,
4185
+ autoFixable: false,
4186
+ fixSuggestion: "Group messages into 3 main themes"
4187
+ });
4188
+ score -= 5;
4189
+ }
4190
+ const hasScqaElements = slides.some(
4191
+ (s) => s.data.scqaType || s.data.title && s.data.title.toLowerCase().includes("challenge") || s.data.title && s.data.title.toLowerCase().includes("solution") || s.data.title && s.data.title.toLowerCase().includes("recommendation")
4192
+ );
4193
+ if (!hasScqaElements && this.presentationType === "consulting_deck") {
4194
+ issues.push({
4195
+ slideIndex: -1,
4196
+ dimension: "effectiveness",
4197
+ severity: "warning",
4198
+ message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
4199
+ autoFixable: false,
4200
+ fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
4201
+ });
4202
+ score -= 10;
4203
+ }
4204
+ const firstSlideType = slides[0]?.type;
4205
+ const lastSlideType = slides[slides.length - 1]?.type;
4206
+ return {
4207
+ name: "Effectiveness",
4208
+ score: Math.max(0, score),
4209
+ weight: DIMENSION_WEIGHTS.effectiveness,
4210
+ issues,
4211
+ details: {
4212
+ presentationType: this.presentationType,
4213
+ slideCount: slides.length,
4214
+ hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
4215
+ hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
4216
+ }
4217
+ };
4218
+ }
4219
+ /**
4220
+ * Score consistency dimension (style uniformity, design coherence)
4221
+ */
4222
+ async scoreConsistency(slides, html) {
4223
+ const issues = [];
4224
+ let score = 100;
4225
+ const hasCssVariables = html.includes("var(--") || html.includes(":root");
4226
+ if (!hasCssVariables) {
4227
+ issues.push({
4228
+ slideIndex: -1,
4229
+ dimension: "consistency",
4230
+ severity: "warning",
4231
+ message: "Presentation lacks CSS variables for consistent styling",
4232
+ autoFixable: true,
4233
+ fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
4234
+ });
4235
+ score -= 10;
4236
+ }
4237
+ const titlePatterns = /* @__PURE__ */ new Set();
4238
+ for (const slide of slides) {
4239
+ if (slide.data.title) {
4240
+ const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
4241
+ const words = slide.data.title.split(" ").filter((w) => w.length > 0);
4242
+ const isTitleCase = words.length > 0 && words.every(
4243
+ (w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
4244
+ );
4245
+ titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
4246
+ }
4247
+ }
4248
+ if (titlePatterns.size > 1) {
4249
+ issues.push({
4250
+ slideIndex: -1,
4251
+ dimension: "consistency",
4252
+ severity: "warning",
4253
+ message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
4254
+ autoFixable: true,
4255
+ fixSuggestion: "Use consistent title case throughout"
4256
+ });
4257
+ score -= 10;
4258
+ }
4259
+ const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
4260
+ const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
4261
+ if (uniqueFonts.size > 3) {
4262
+ issues.push({
4263
+ slideIndex: -1,
4264
+ dimension: "consistency",
4265
+ severity: "warning",
4266
+ message: `Too many font families (${uniqueFonts.size} > 3)`,
4267
+ autoFixable: true,
4268
+ fixSuggestion: "Use 2-3 complementary fonts max"
4269
+ });
4270
+ score -= 10;
4271
+ }
4272
+ return {
4273
+ name: "Consistency",
4274
+ score: Math.max(0, score),
4275
+ weight: DIMENSION_WEIGHTS.consistency,
4276
+ issues,
4277
+ details: {
4278
+ hasCssVariables,
4279
+ titlePatterns: Array.from(titlePatterns),
4280
+ fontFamilyCount: uniqueFonts.size
4281
+ }
4282
+ };
4283
+ }
4284
+ /**
4285
+ * Count words in a slide.
4286
+ */
4287
+ countWords(slide) {
4288
+ let text = "";
4289
+ if (slide.data.title) text += slide.data.title + " ";
4290
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4291
+ if (slide.data.body) text += slide.data.body + " ";
4292
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4293
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4294
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4295
+ }
4296
+ /**
4297
+ * Get a formatted report of the scoring results.
4298
+ */
4299
+ formatReport(result) {
4300
+ const lines = [];
4301
+ lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
4302
+ lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
4303
+ lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
4304
+ lines.push("");
4305
+ lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
4306
+ lines.push(`Threshold: ${result.threshold}/100`);
4307
+ lines.push("");
4308
+ lines.push("Dimension Breakdown:");
4309
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4310
+ const dims = result.dimensions;
4311
+ const formatDim = (name, d) => {
4312
+ const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
4313
+ const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
4314
+ return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
4315
+ };
4316
+ lines.push(formatDim("Layout", dims.layout));
4317
+ lines.push(formatDim("Contrast", dims.contrast));
4318
+ lines.push(formatDim("Graphics", dims.graphics));
4319
+ lines.push(formatDim("Content", dims.content));
4320
+ lines.push(formatDim("Clarity", dims.clarity));
4321
+ lines.push(formatDim("Effectiveness", dims.effectiveness));
4322
+ lines.push(formatDim("Consistency", dims.consistency));
4323
+ lines.push("");
4324
+ const errors = result.issues.filter((i) => i.severity === "error");
4325
+ const warnings = result.issues.filter((i) => i.severity === "warning");
4326
+ if (errors.length > 0) {
4327
+ lines.push("\u274C Errors:");
4328
+ errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
4329
+ lines.push("");
4330
+ }
4331
+ if (warnings.length > 0) {
4332
+ lines.push("\u26A0\uFE0F Warnings:");
4333
+ warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
4334
+ if (warnings.length > 10) {
4335
+ lines.push(` ... and ${warnings.length - 10} more warnings`);
4336
+ }
4337
+ lines.push("");
4338
+ }
4339
+ const autoFixable = result.issues.filter((i) => i.autoFixable);
4340
+ if (autoFixable.length > 0) {
4341
+ lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
4342
+ }
4343
+ return lines.join("\n");
4344
+ }
4345
+ };
4346
+
4347
+ // src/qa/AutoFixEngine.ts
4348
+ var AutoFixEngine = class {
4349
+ kb;
4350
+ mode;
4351
+ presentationType;
4352
+ constructor(mode, presentationType) {
4353
+ this.mode = mode;
4354
+ this.presentationType = presentationType;
4355
+ }
4356
+ /**
4357
+ * Apply automatic fixes to slides based on scoring results.
4358
+ */
4359
+ async fix(slides, scoringResult) {
4360
+ this.kb = await getKnowledgeGateway();
4361
+ const slidesFixed = JSON.parse(JSON.stringify(slides));
4362
+ const fixesApplied = [];
4363
+ const fixesSkipped = [];
4364
+ const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
4365
+ for (const issue of autoFixableIssues) {
4366
+ const result = await this.applyFix(slidesFixed, issue);
4367
+ if (result.applied) {
4368
+ fixesApplied.push(result);
4369
+ } else {
4370
+ fixesSkipped.push(result);
4371
+ }
4372
+ }
4373
+ const summary = this.generateSummary(fixesApplied, fixesSkipped);
4374
+ return {
4375
+ slidesFixed,
4376
+ fixesApplied,
4377
+ fixesSkipped,
4378
+ summary
4379
+ };
4380
+ }
4381
+ /**
4382
+ * Apply a single fix based on the issue.
4383
+ */
4384
+ async applyFix(slides, issue) {
4385
+ const result = {
4386
+ slideIndex: issue.slideIndex,
4387
+ dimension: issue.dimension,
4388
+ originalValue: issue.currentValue,
4389
+ newValue: null,
4390
+ description: issue.message,
4391
+ applied: false
4392
+ };
4393
+ switch (issue.dimension) {
4394
+ case "content":
4395
+ return this.fixContentIssue(slides, issue, result);
4396
+ case "clarity":
4397
+ return this.fixClarityIssue(slides, issue, result);
4398
+ case "layout":
4399
+ return this.fixLayoutIssue(slides, issue, result);
4400
+ case "consistency":
4401
+ return this.fixConsistencyIssue(slides, issue, result);
4402
+ case "contrast":
4403
+ return this.fixContrastIssue(slides, issue, result);
4404
+ default:
4405
+ result.description = `No auto-fix available for ${issue.dimension}`;
4406
+ return result;
4407
+ }
4408
+ }
4409
+ /**
4410
+ * Fix content-related issues (word count, bullet count).
4411
+ */
4412
+ fixContentIssue(slides, issue, result) {
4413
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4414
+ return result;
4415
+ }
4416
+ const slide = slides[issue.slideIndex];
4417
+ if (!slide) return result;
4418
+ if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
4419
+ const maxWords = issue.expectedValue;
4420
+ result.originalValue = this.countWords(slide);
4421
+ if (slide.data.body) {
4422
+ slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
4423
+ }
4424
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4425
+ const bulletCount = slide.data.bullets.length;
4426
+ slide.data.bullets = slide.data.bullets.map(
4427
+ (bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
4428
+ );
4429
+ }
4430
+ if (slide.data.subtitle) {
4431
+ slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
4432
+ }
4433
+ result.newValue = this.countWords(slide);
4434
+ result.applied = result.newValue <= maxWords;
4435
+ result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
4436
+ }
4437
+ if (issue.message.includes("bullets exceeds")) {
4438
+ const maxBullets = issue.expectedValue;
4439
+ if (slide.data.bullets) {
4440
+ result.originalValue = slide.data.bullets.length;
4441
+ slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
4442
+ result.newValue = slide.data.bullets.length;
4443
+ result.applied = true;
4444
+ result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
4445
+ }
4446
+ }
4447
+ return result;
4448
+ }
4449
+ /**
4450
+ * Fix clarity-related issues (key message length, title length).
4451
+ */
4452
+ fixClarityIssue(slides, issue, result) {
4453
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4454
+ return result;
4455
+ }
4456
+ const slide = slides[issue.slideIndex];
4457
+ if (!slide) return result;
4458
+ if (issue.message.includes("Key message too long")) {
4459
+ if (slide.data.keyMessage) {
4460
+ result.originalValue = slide.data.keyMessage;
4461
+ const maxWords = issue.expectedValue;
4462
+ slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
4463
+ result.newValue = slide.data.keyMessage;
4464
+ result.applied = true;
4465
+ result.description = `Shortened key message to ${maxWords} words`;
4466
+ }
4467
+ }
4468
+ if (issue.message.includes("Title too long")) {
4469
+ if (slide.data.title) {
4470
+ result.originalValue = slide.data.title;
4471
+ const words = slide.data.title.split(/\s+/);
4472
+ const originalLength = words.length;
4473
+ if (words.length > 6) {
4474
+ slide.data.title = words.slice(0, 6).join(" ");
4475
+ }
4476
+ result.newValue = slide.data.title;
4477
+ result.applied = true;
4478
+ result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
4479
+ }
4480
+ }
4481
+ if (issue.message.includes("Too many elements")) {
4482
+ result.originalValue = issue.currentValue;
4483
+ if (slide.data.subtitle && slide.data.body) {
4484
+ delete slide.data.subtitle;
4485
+ result.applied = true;
4486
+ result.description = "Removed subtitle to reduce element count";
4487
+ } else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
4488
+ delete slide.data.body;
4489
+ result.applied = true;
4490
+ result.description = "Removed body text, keeping bullets";
4491
+ }
4492
+ result.newValue = this.countElements(slide);
4493
+ }
4494
+ return result;
2758
4495
  }
2759
- calculateA11yScore(results) {
2760
- let score = 100;
2761
- score -= Math.min(40, results.fontSizeIssues.length * 10);
2762
- score -= Math.min(40, results.contrastIssues.length * 10);
2763
- if (results.focusCoverage < 1) score -= 20;
2764
- return Math.max(0, score);
4496
+ /**
4497
+ * Fix layout-related issues.
4498
+ */
4499
+ fixLayoutIssue(slides, issue, result) {
4500
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4501
+ return result;
4502
+ }
4503
+ const slide = slides[issue.slideIndex];
4504
+ if (!slide) return result;
4505
+ if (issue.message.includes("Insufficient whitespace")) {
4506
+ const currentWordCount = this.countWords(slide);
4507
+ const targetReduction = 0.3;
4508
+ const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
4509
+ result.originalValue = currentWordCount;
4510
+ if (slide.data.body) {
4511
+ slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
4512
+ }
4513
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4514
+ const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
4515
+ slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
4516
+ }
4517
+ result.newValue = this.countWords(slide);
4518
+ result.applied = true;
4519
+ result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
4520
+ }
4521
+ return result;
2765
4522
  }
2766
- // ===========================================================================
2767
- // ISSUE COLLECTION
2768
- // ===========================================================================
2769
- collectIssues(visual, content, expert, accessibility) {
2770
- const issues = [];
2771
- visual.perSlide.forEach((slide) => {
2772
- slide.issues.forEach((issue) => {
2773
- issues.push({
2774
- severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
2775
- category: "visual",
2776
- slideIndex: slide.slideIndex,
2777
- message: issue
2778
- });
2779
- });
2780
- });
2781
- content.perSlide.forEach((slide) => {
2782
- slide.issues.forEach((issue) => {
2783
- issues.push({
2784
- severity: "warning",
2785
- category: "content",
2786
- slideIndex: slide.slideIndex,
2787
- message: issue
2788
- });
2789
- });
2790
- });
2791
- content.glanceTest.filter((g) => !g.passed).forEach((g) => {
2792
- const issue = {
2793
- severity: "warning",
2794
- category: "content",
2795
- slideIndex: g.slideIndex,
2796
- message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
2797
- };
2798
- if (g.recommendation) {
2799
- issue.suggestion = g.recommendation;
4523
+ /**
4524
+ * Fix consistency-related issues.
4525
+ */
4526
+ fixConsistencyIssue(slides, issue, result) {
4527
+ if (issue.message.includes("Inconsistent title casing")) {
4528
+ let fixedCount = 0;
4529
+ for (const slide of slides) {
4530
+ if (slide.data.title) {
4531
+ const original = slide.data.title;
4532
+ slide.data.title = this.toTitleCase(slide.data.title);
4533
+ if (slide.data.title !== original) fixedCount++;
4534
+ }
2800
4535
  }
2801
- issues.push(issue);
2802
- });
2803
- [expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
2804
- e.violations.forEach((v) => {
2805
- issues.push({
2806
- severity: "warning",
2807
- category: "expert",
2808
- message: `${e.expertName}: ${v}`
2809
- });
2810
- });
2811
- });
2812
- accessibility.fontSizeIssues.forEach((issue) => {
2813
- issues.push({
2814
- severity: "error",
2815
- category: "accessibility",
2816
- slideIndex: issue.slideIndex,
2817
- message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
2818
- suggestion: `Increase font size to at least ${issue.minimumSize}px`
2819
- });
2820
- });
2821
- accessibility.contrastIssues.forEach((issue) => {
2822
- issues.push({
2823
- severity: "error",
2824
- category: "accessibility",
2825
- slideIndex: issue.slideIndex,
2826
- message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
2827
- suggestion: "Increase contrast between text and background"
2828
- });
2829
- });
2830
- return issues;
4536
+ result.originalValue = "Mixed casing";
4537
+ result.newValue = "Title Case";
4538
+ result.applied = fixedCount > 0;
4539
+ result.description = `Applied Title Case to ${fixedCount} slide titles`;
4540
+ }
4541
+ return result;
2831
4542
  }
2832
- // ===========================================================================
2833
- // BROWSER MANAGEMENT
2834
- // ===========================================================================
2835
- async initBrowser() {
2836
- if (!this.browser) {
2837
- this.browser = await chromium.launch({ headless: true });
4543
+ /**
4544
+ * Fix contrast-related issues.
4545
+ * Note: These are CSS fixes, typically handled at generation time.
4546
+ */
4547
+ fixContrastIssue(slides, issue, result) {
4548
+ result.description = "Contrast issues flagged for CSS regeneration";
4549
+ result.applied = false;
4550
+ return result;
4551
+ }
4552
+ /**
4553
+ * Condense text to approximately maxWords.
4554
+ * Uses smart truncation that preserves meaning.
4555
+ */
4556
+ condenseText(text, maxWords) {
4557
+ const words = text.split(/\s+/);
4558
+ if (words.length <= maxWords) {
4559
+ return text;
4560
+ }
4561
+ const fillerWords = /* @__PURE__ */ new Set([
4562
+ "very",
4563
+ "really",
4564
+ "actually",
4565
+ "basically",
4566
+ "literally",
4567
+ "obviously",
4568
+ "clearly",
4569
+ "simply",
4570
+ "just",
4571
+ "that",
4572
+ "which",
4573
+ "would",
4574
+ "could",
4575
+ "should",
4576
+ "might"
4577
+ ]);
4578
+ let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
4579
+ if (filtered.length <= maxWords) {
4580
+ return filtered.join(" ");
4581
+ }
4582
+ const punctuation = [".", ",", ";", ":", "-"];
4583
+ let breakPoint = maxWords;
4584
+ for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
4585
+ const word = filtered[i];
4586
+ if (word && punctuation.some((p) => word.endsWith(p))) {
4587
+ breakPoint = i + 1;
4588
+ break;
4589
+ }
2838
4590
  }
4591
+ filtered = filtered.slice(0, breakPoint);
4592
+ let result = filtered.join(" ");
4593
+ if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
4594
+ result = result.replace(/[,;:]$/, "") + "...";
4595
+ }
4596
+ return result;
2839
4597
  }
2840
- async closeBrowser() {
2841
- if (this.browser) {
2842
- await this.browser.close();
2843
- this.browser = null;
4598
+ /**
4599
+ * Convert text to Title Case.
4600
+ */
4601
+ toTitleCase(text) {
4602
+ const minorWords = /* @__PURE__ */ new Set([
4603
+ "a",
4604
+ "an",
4605
+ "the",
4606
+ "and",
4607
+ "but",
4608
+ "or",
4609
+ "for",
4610
+ "nor",
4611
+ "on",
4612
+ "at",
4613
+ "to",
4614
+ "by",
4615
+ "of",
4616
+ "in",
4617
+ "with"
4618
+ ]);
4619
+ return text.split(" ").map((word, index) => {
4620
+ if (index === 0) {
4621
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4622
+ }
4623
+ if (minorWords.has(word.toLowerCase())) {
4624
+ return word.toLowerCase();
4625
+ }
4626
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4627
+ }).join(" ");
4628
+ }
4629
+ /**
4630
+ * Count words in a slide.
4631
+ */
4632
+ countWords(slide) {
4633
+ let text = "";
4634
+ if (slide.data.title) text += slide.data.title + " ";
4635
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4636
+ if (slide.data.body) text += slide.data.body + " ";
4637
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4638
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4639
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4640
+ }
4641
+ /**
4642
+ * Count elements in a slide.
4643
+ */
4644
+ countElements(slide) {
4645
+ return (slide.data.title ? 1 : 0) + (slide.data.subtitle ? 1 : 0) + (slide.data.body ? 1 : 0) + (slide.data.bullets?.length || 0) + (slide.data.keyMessage ? 1 : 0) + (slide.data.image ? 1 : 0);
4646
+ }
4647
+ /**
4648
+ * Generate a summary of fixes applied.
4649
+ */
4650
+ generateSummary(applied, skipped) {
4651
+ const lines = [];
4652
+ lines.push(`
4653
+ \u{1F527} Auto-Fix Summary`);
4654
+ lines.push(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
4655
+ lines.push(`Fixes Applied: ${applied.length}`);
4656
+ lines.push(`Fixes Skipped: ${skipped.length}`);
4657
+ lines.push("");
4658
+ if (applied.length > 0) {
4659
+ lines.push("\u2705 Applied Fixes:");
4660
+ for (const fix of applied) {
4661
+ lines.push(` \u2022 ${fix.description}`);
4662
+ }
4663
+ lines.push("");
4664
+ }
4665
+ if (skipped.length > 0) {
4666
+ lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
4667
+ for (const fix of skipped.slice(0, 5)) {
4668
+ lines.push(` \u2022 ${fix.description}`);
4669
+ }
4670
+ if (skipped.length > 5) {
4671
+ lines.push(` ... and ${skipped.length - 5} more`);
4672
+ }
2844
4673
  }
4674
+ return lines.join("\n");
2845
4675
  }
2846
4676
  };
4677
+ function createAutoFixEngine(mode, presentationType) {
4678
+ return new AutoFixEngine(mode, presentationType);
4679
+ }
2847
4680
 
2848
4681
  // src/generators/html/RevealJsGenerator.ts
2849
4682
  var RevealJsGenerator = class {
@@ -3439,6 +5272,182 @@ ${slides}
3439
5272
  }
3440
5273
  };
3441
5274
 
5275
+ // src/qa/IterativeQAEngine.ts
5276
+ var DEFAULT_OPTIONS = {
5277
+ minScore: 95,
5278
+ maxIterations: 5,
5279
+ verbose: true
5280
+ };
5281
+ var IterativeQAEngine = class {
5282
+ kb;
5283
+ scorer;
5284
+ fixer;
5285
+ generator;
5286
+ mode;
5287
+ presentationType;
5288
+ config;
5289
+ constructor(mode, presentationType, config) {
5290
+ this.mode = mode;
5291
+ this.presentationType = presentationType;
5292
+ this.config = config;
5293
+ this.scorer = new SevenDimensionScorer(mode, presentationType);
5294
+ this.fixer = new AutoFixEngine(mode, presentationType);
5295
+ this.generator = new RevealJsGenerator();
5296
+ }
5297
+ /**
5298
+ * Run the iterative QA process.
5299
+ */
5300
+ async run(initialSlides, initialHtml, options = {}) {
5301
+ const opts = { ...DEFAULT_OPTIONS, ...options };
5302
+ this.kb = await getKnowledgeGateway();
5303
+ const iterations = [];
5304
+ let currentSlides = initialSlides;
5305
+ let currentHtml = initialHtml;
5306
+ let scoringResult;
5307
+ let autoFixSummary = "";
5308
+ let totalFixesApplied = 0;
5309
+ if (opts.verbose) {
5310
+ console.log("\n\u{1F504} Starting Iterative QA Process");
5311
+ console.log(` Target Score: ${opts.minScore}/100`);
5312
+ console.log(` Max Iterations: ${opts.maxIterations}`);
5313
+ console.log("");
5314
+ }
5315
+ for (let i = 0; i < opts.maxIterations; i++) {
5316
+ const iterationNum = i + 1;
5317
+ if (opts.verbose) {
5318
+ console.log(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
5319
+ }
5320
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5321
+ iterations.push({
5322
+ iteration: iterationNum,
5323
+ score: scoringResult.overallScore,
5324
+ dimensionScores: {
5325
+ layout: scoringResult.dimensions.layout.score,
5326
+ contrast: scoringResult.dimensions.contrast.score,
5327
+ graphics: scoringResult.dimensions.graphics.score,
5328
+ content: scoringResult.dimensions.content.score,
5329
+ clarity: scoringResult.dimensions.clarity.score,
5330
+ effectiveness: scoringResult.dimensions.effectiveness.score,
5331
+ consistency: scoringResult.dimensions.consistency.score
5332
+ },
5333
+ fixesApplied: 0,
5334
+ timestamp: /* @__PURE__ */ new Date()
5335
+ });
5336
+ if (opts.verbose) {
5337
+ console.log(` Score: ${scoringResult.overallScore}/100`);
5338
+ }
5339
+ if (scoringResult.passed) {
5340
+ if (opts.verbose) {
5341
+ console.log(` \u2705 PASSED - Score meets threshold (${opts.minScore})`);
5342
+ }
5343
+ break;
5344
+ }
5345
+ if (i < opts.maxIterations - 1) {
5346
+ if (opts.verbose) {
5347
+ console.log(` \u26A0\uFE0F Below threshold, applying auto-fixes...`);
5348
+ }
5349
+ const fixResult = await this.fixer.fix(currentSlides, scoringResult);
5350
+ if (fixResult.fixesApplied.length > 0) {
5351
+ currentSlides = fixResult.slidesFixed;
5352
+ currentHtml = await this.generator.generate(currentSlides, this.config);
5353
+ const lastIteration = iterations[iterations.length - 1];
5354
+ if (lastIteration) {
5355
+ lastIteration.fixesApplied = fixResult.fixesApplied.length;
5356
+ }
5357
+ totalFixesApplied += fixResult.fixesApplied.length;
5358
+ if (opts.verbose) {
5359
+ console.log(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
5360
+ }
5361
+ autoFixSummary += fixResult.summary + "\n";
5362
+ } else {
5363
+ if (opts.verbose) {
5364
+ console.log(` \u26A0\uFE0F No auto-fixes available, manual review needed`);
5365
+ }
5366
+ break;
5367
+ }
5368
+ }
5369
+ }
5370
+ if (!scoringResult.passed) {
5371
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5372
+ }
5373
+ const report = this.generateReport(
5374
+ scoringResult,
5375
+ iterations,
5376
+ opts,
5377
+ totalFixesApplied
5378
+ );
5379
+ if (opts.verbose) {
5380
+ console.log(report);
5381
+ }
5382
+ return {
5383
+ finalScore: scoringResult.overallScore,
5384
+ passed: scoringResult.passed,
5385
+ threshold: opts.minScore,
5386
+ iterations,
5387
+ totalIterations: iterations.length,
5388
+ maxIterations: opts.maxIterations,
5389
+ slides: currentSlides,
5390
+ html: currentHtml,
5391
+ finalScoring: scoringResult,
5392
+ autoFixSummary,
5393
+ report
5394
+ };
5395
+ }
5396
+ /**
5397
+ * Generate a comprehensive report.
5398
+ */
5399
+ generateReport(finalScoring, iterations, options, totalFixesApplied) {
5400
+ const lines = [];
5401
+ lines.push("");
5402
+ lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
5403
+ lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
5404
+ lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
5405
+ lines.push("");
5406
+ const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
5407
+ lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
5408
+ lines.push(`Threshold: ${options.minScore}/100`);
5409
+ lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
5410
+ lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
5411
+ lines.push("");
5412
+ if (iterations.length > 1) {
5413
+ lines.push("Score Progression:");
5414
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5415
+ for (const iter of iterations) {
5416
+ const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
5417
+ lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
5418
+ }
5419
+ lines.push("");
5420
+ }
5421
+ lines.push(this.scorer.formatReport(finalScoring));
5422
+ lines.push("");
5423
+ lines.push("\u{1F4DA} Knowledge Base Compliance:");
5424
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5425
+ lines.push(` Mode: ${this.mode}`);
5426
+ lines.push(` Presentation Type: ${this.presentationType}`);
5427
+ lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
5428
+ lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
5429
+ lines.push("");
5430
+ if (!finalScoring.passed) {
5431
+ lines.push("\u{1F4CB} Recommendations for Manual Review:");
5432
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5433
+ const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
5434
+ for (const issue of manualIssues.slice(0, 5)) {
5435
+ lines.push(` \u2022 ${issue.message}`);
5436
+ if (issue.fixSuggestion) {
5437
+ lines.push(` \u2192 ${issue.fixSuggestion}`);
5438
+ }
5439
+ }
5440
+ if (manualIssues.length > 5) {
5441
+ lines.push(` ... and ${manualIssues.length - 5} more issues`);
5442
+ }
5443
+ }
5444
+ return lines.join("\n");
5445
+ }
5446
+ };
5447
+ function createIterativeQAEngine(mode, presentationType, config) {
5448
+ return new IterativeQAEngine(mode, presentationType, config);
5449
+ }
5450
+
3442
5451
  // src/generators/pptx/PowerPointGenerator.ts
3443
5452
  import PptxGenJS from "pptxgenjs";
3444
5453
 
@@ -4173,15 +6182,14 @@ var PowerPointGenerator = class {
4173
6182
  // src/core/PresentationEngine.ts
4174
6183
  var PresentationEngine = class {
4175
6184
  contentAnalyzer;
4176
- slideFactory;
4177
6185
  templateEngine;
4178
6186
  scoreCalculator;
4179
6187
  qaEngine;
4180
6188
  htmlGenerator;
4181
6189
  pptxGenerator;
6190
+ kb;
4182
6191
  constructor() {
4183
6192
  this.contentAnalyzer = new ContentAnalyzer();
4184
- this.slideFactory = new SlideFactory();
4185
6193
  this.templateEngine = new TemplateEngine();
4186
6194
  this.scoreCalculator = new ScoreCalculator();
4187
6195
  this.qaEngine = new QAEngine();
@@ -4196,10 +6204,15 @@ var PresentationEngine = class {
4196
6204
  */
4197
6205
  async generate(config) {
4198
6206
  this.validateConfig(config);
6207
+ console.log("\u{1F4DA} Loading knowledge base...");
6208
+ this.kb = await getKnowledgeGateway();
4199
6209
  console.log("\u{1F4DD} Analyzing content...");
4200
6210
  const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
6211
+ const presentationType = config.presentationType || analysis.detectedType;
6212
+ console.log(` \u2713 Presentation type: ${presentationType}`);
6213
+ const slideFactory = createSlideFactory(this.kb, presentationType);
4201
6214
  console.log("\u{1F3A8} Creating slides...");
4202
- const slides = await this.slideFactory.createSlides(analysis, config.mode);
6215
+ const slides = await slideFactory.createSlides(analysis);
4203
6216
  console.log("\u2705 Validating structure...");
4204
6217
  const structureErrors = this.validateStructure(slides, config.mode);
4205
6218
  if (structureErrors.length > 0) {
@@ -4220,23 +6233,52 @@ var PresentationEngine = class {
4220
6233
  }
4221
6234
  let qaResults;
4222
6235
  let score = 100;
6236
+ let iterativeResult = null;
6237
+ let finalSlides = slides;
6238
+ let finalHtml = outputs.html;
4223
6239
  if (!config.skipQA && outputs.html) {
4224
- console.log("\u{1F50D} Running QA validation...");
4225
- qaResults = await this.qaEngine.validate(outputs.html, {
4226
- mode: config.mode,
4227
- strictMode: true
4228
- });
4229
- score = this.scoreCalculator.calculate(qaResults);
4230
- console.log(`\u{1F4CA} QA Score: ${score}/100`);
4231
6240
  const threshold = config.qaThreshold ?? 95;
4232
- if (score < threshold) {
4233
- throw new QAFailureError(score, threshold, qaResults);
6241
+ const maxIterations = config.maxIterations ?? 5;
6242
+ const useIterativeQA = config.useIterativeQA !== false;
6243
+ if (useIterativeQA) {
6244
+ console.log("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
6245
+ const iterativeEngine = createIterativeQAEngine(
6246
+ config.mode,
6247
+ presentationType,
6248
+ config
6249
+ );
6250
+ iterativeResult = await iterativeEngine.run(slides, outputs.html, {
6251
+ minScore: threshold,
6252
+ maxIterations,
6253
+ verbose: true
6254
+ });
6255
+ score = iterativeResult.finalScore;
6256
+ finalSlides = iterativeResult.slides;
6257
+ finalHtml = iterativeResult.html;
6258
+ if (outputs.html) {
6259
+ outputs.html = finalHtml;
6260
+ }
6261
+ qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
6262
+ if (!iterativeResult.passed) {
6263
+ throw new QAFailureError(score, threshold, qaResults);
6264
+ }
6265
+ } else {
6266
+ console.log("\u{1F50D} Running QA validation (legacy mode)...");
6267
+ qaResults = await this.qaEngine.validate(outputs.html, {
6268
+ mode: config.mode,
6269
+ strictMode: true
6270
+ });
6271
+ score = this.scoreCalculator.calculate(qaResults);
6272
+ console.log(`\u{1F4CA} QA Score: ${score}/100`);
6273
+ if (score < threshold) {
6274
+ throw new QAFailureError(score, threshold, qaResults);
6275
+ }
4234
6276
  }
4235
6277
  } else {
4236
6278
  qaResults = this.qaEngine.createEmptyResults();
4237
6279
  console.log("\u26A0\uFE0F QA validation skipped (NOT RECOMMENDED)");
4238
6280
  }
4239
- const metadata = this.buildMetadata(config, analysis, slides);
6281
+ const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
4240
6282
  return {
4241
6283
  outputs,
4242
6284
  qaResults,
@@ -4244,6 +6286,46 @@ var PresentationEngine = class {
4244
6286
  metadata
4245
6287
  };
4246
6288
  }
6289
+ /**
6290
+ * Build QA results structure from 7-dimension scoring.
6291
+ */
6292
+ buildQAResultsFrom7Dimension(iterativeResult) {
6293
+ const scoring = iterativeResult.finalScoring;
6294
+ return {
6295
+ passed: scoring.passed,
6296
+ score: scoring.overallScore,
6297
+ visual: {
6298
+ whitespacePercentage: scoring.dimensions.layout.score,
6299
+ layoutBalance: scoring.dimensions.layout.score / 100,
6300
+ colorContrast: scoring.dimensions.contrast.score / 100
6301
+ },
6302
+ content: {
6303
+ perSlide: [],
6304
+ issues: scoring.issues.filter((i) => i.dimension === "content")
6305
+ },
6306
+ accessibility: {
6307
+ wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
6308
+ issues: scoring.issues.filter((i) => i.dimension === "contrast")
6309
+ },
6310
+ issues: scoring.issues.map((issue) => ({
6311
+ severity: issue.severity,
6312
+ message: issue.message,
6313
+ slideIndex: issue.slideIndex,
6314
+ dimension: issue.dimension
6315
+ })),
6316
+ dimensions: {
6317
+ layout: scoring.dimensions.layout.score,
6318
+ contrast: scoring.dimensions.contrast.score,
6319
+ graphics: scoring.dimensions.graphics.score,
6320
+ content: scoring.dimensions.content.score,
6321
+ clarity: scoring.dimensions.clarity.score,
6322
+ effectiveness: scoring.dimensions.effectiveness.score,
6323
+ consistency: scoring.dimensions.consistency.score
6324
+ },
6325
+ iterations: iterativeResult.iterations,
6326
+ report: iterativeResult.report
6327
+ };
6328
+ }
4247
6329
  /**
4248
6330
  * Validate presentation configuration.
4249
6331
  */
@@ -4314,13 +6396,13 @@ var PresentationEngine = class {
4314
6396
  /**
4315
6397
  * Build presentation metadata.
4316
6398
  */
4317
- buildMetadata(config, analysis, slides) {
6399
+ buildMetadata(config, analysis, slides, iterativeResult) {
4318
6400
  const wordCounts = slides.map((s) => this.countWords(s));
4319
6401
  const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
4320
6402
  const avgWordsPerSlide = Math.round(totalWords / slides.length);
4321
6403
  const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
4322
6404
  const estimatedDuration = Math.round(slides.length * minutesPerSlide);
4323
- return {
6405
+ const metadata = {
4324
6406
  title: config.title,
4325
6407
  author: config.author ?? "Unknown",
4326
6408
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4329,8 +6411,15 @@ var PresentationEngine = class {
4329
6411
  wordCount: totalWords,
4330
6412
  avgWordsPerSlide,
4331
6413
  estimatedDuration,
4332
- frameworks: this.detectFrameworks(analysis)
6414
+ frameworks: this.detectFrameworks(analysis),
6415
+ presentationType: config.presentationType || analysis.detectedType
4333
6416
  };
6417
+ if (iterativeResult) {
6418
+ metadata.qaIterations = iterativeResult.totalIterations;
6419
+ metadata.qaMaxIterations = iterativeResult.maxIterations;
6420
+ metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
6421
+ }
6422
+ return metadata;
4334
6423
  }
4335
6424
  /**
4336
6425
  * Detect which expert frameworks were applied.
@@ -4353,6 +6442,95 @@ var PresentationEngine = class {
4353
6442
  }
4354
6443
  };
4355
6444
 
6445
+ // src/core/SlideGenerator.ts
6446
+ var SlideGenerator = class {
6447
+ kb;
6448
+ factory;
6449
+ mode = "keynote";
6450
+ presentationType = "ted_keynote";
6451
+ async initialize() {
6452
+ this.kb = await getKnowledgeGateway();
6453
+ }
6454
+ /**
6455
+ * Generate slides from analyzed content using KB-driven SlideFactory.
6456
+ *
6457
+ * @version 7.0.0 - Now delegates ALL slide creation to SlideFactory
6458
+ * SlideFactory queries KB for every decision:
6459
+ * - Allowed slide types per presentation type
6460
+ * - Word limits per type
6461
+ * - Bullet limits per type
6462
+ * - Content pattern to slide type mapping
6463
+ * - Slide validation against KB rules
6464
+ */
6465
+ async generate(analysis, type) {
6466
+ await this.initialize();
6467
+ this.presentationType = type || analysis.detectedType;
6468
+ this.mode = this.kb.getModeForType(this.presentationType);
6469
+ console.log(` \u2713 Using ${this.mode} mode for ${this.presentationType}`);
6470
+ this.factory = createSlideFactory(this.kb, this.presentationType);
6471
+ const slides = await this.factory.createSlides(analysis);
6472
+ const legacySlides = this.convertToLegacyFormat(slides);
6473
+ console.log(` \u2713 Generated ${legacySlides.length} KB-validated slides`);
6474
+ return legacySlides;
6475
+ }
6476
+ /**
6477
+ * Convert new Slide format to legacy format for backwards compatibility
6478
+ */
6479
+ convertToLegacyFormat(slides) {
6480
+ return slides.map((slide) => ({
6481
+ index: slide.index,
6482
+ type: slide.type,
6483
+ title: slide.data.title || "",
6484
+ content: {
6485
+ subtitle: slide.data.subtitle,
6486
+ statement: slide.data.body,
6487
+ body: slide.data.body,
6488
+ bullets: slide.data.bullets,
6489
+ metrics: slide.data.metrics,
6490
+ quote: slide.data.quote ? { text: slide.data.quote, attribution: slide.data.attribution } : void 0,
6491
+ callToAction: slide.data.keyMessage
6492
+ },
6493
+ notes: slide.notes,
6494
+ template: this.mapTypeToTemplate(slide.type)
6495
+ }));
6496
+ }
6497
+ /**
6498
+ * Map slide type to template name for backwards compatibility
6499
+ */
6500
+ mapTypeToTemplate(type) {
6501
+ const templateMap = {
6502
+ "title": "Title Impact",
6503
+ "agenda": "Detailed Findings",
6504
+ "big-number": "Data Insight",
6505
+ "metrics-grid": "Data Insight",
6506
+ "bullet-points": "Detailed Findings",
6507
+ "two-column": "Two Column",
6508
+ "three-column": "Three Column",
6509
+ "comparison": "Comparison",
6510
+ "timeline": "Process Timeline",
6511
+ "process": "Process",
6512
+ "quote": "Quote",
6513
+ "single-statement": "Single Statement",
6514
+ "cta": "Call to Action",
6515
+ "thank-you": "Title Impact"
6516
+ };
6517
+ return templateMap[type] || "Detailed Findings";
6518
+ }
6519
+ // =========================================================================
6520
+ // All slide creation is now handled by SlideFactory
6521
+ // Old methods removed in v7.0.0 - KB-driven SlideFactory handles:
6522
+ // - createTitleSlide, createAgendaSlide, createSituationSlide, etc.
6523
+ // - Content pattern classification
6524
+ // - KB-based slide type selection
6525
+ // - Text truncation, cleaning, deduplication
6526
+ // =========================================================================
6527
+ };
6528
+ async function initSlideGenerator() {
6529
+ const generator = new SlideGenerator();
6530
+ await generator.initialize();
6531
+ return generator;
6532
+ }
6533
+
4356
6534
  // src/media/ImageProvider.ts
4357
6535
  var LocalImageProvider = class {
4358
6536
  name = "local";
@@ -4541,7 +6719,7 @@ async function validate(presentation, options) {
4541
6719
  score
4542
6720
  };
4543
6721
  }
4544
- var VERSION = "6.0.0";
6722
+ var VERSION = "7.1.0";
4545
6723
  var index_default = {
4546
6724
  generate,
4547
6725
  validate,
@@ -4550,10 +6728,13 @@ var index_default = {
4550
6728
  VERSION
4551
6729
  };
4552
6730
  export {
6731
+ AutoFixEngine,
4553
6732
  ChartJsProvider,
4554
6733
  CompositeChartProvider,
4555
6734
  CompositeImageProvider,
4556
6735
  ContentAnalyzer,
6736
+ ContentPatternClassifier,
6737
+ IterativeQAEngine,
4557
6738
  KnowledgeGateway,
4558
6739
  LocalImageProvider,
4559
6740
  MermaidProvider,
@@ -4565,17 +6746,23 @@ export {
4565
6746
  QuickChartProvider,
4566
6747
  RevealJsGenerator,
4567
6748
  ScoreCalculator,
6749
+ SevenDimensionScorer,
4568
6750
  SlideFactory,
6751
+ SlideGenerator,
4569
6752
  TemplateEngine,
4570
6753
  TemplateNotFoundError,
4571
6754
  UnsplashImageProvider,
4572
6755
  VERSION,
4573
6756
  ValidationError,
6757
+ createAutoFixEngine,
4574
6758
  createDefaultChartProvider,
4575
6759
  createDefaultImageProvider,
6760
+ createIterativeQAEngine,
6761
+ createSlideFactory,
4576
6762
  index_default as default,
4577
6763
  generate,
4578
6764
  getKnowledgeGateway,
6765
+ initSlideGenerator,
4579
6766
  validate
4580
6767
  };
4581
6768
  /**