claude-presentation-master 6.1.0 → 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.js CHANGED
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AutoFixEngine: () => AutoFixEngine,
33
34
  ChartJsProvider: () => ChartJsProvider,
34
35
  CompositeChartProvider: () => CompositeChartProvider,
35
36
  CompositeImageProvider: () => CompositeImageProvider,
36
37
  ContentAnalyzer: () => ContentAnalyzer,
38
+ ContentPatternClassifier: () => ContentPatternClassifier,
39
+ IterativeQAEngine: () => IterativeQAEngine,
37
40
  KnowledgeGateway: () => KnowledgeGateway,
38
41
  LocalImageProvider: () => LocalImageProvider,
39
42
  MermaidProvider: () => MermaidProvider,
@@ -45,17 +48,23 @@ __export(index_exports, {
45
48
  QuickChartProvider: () => QuickChartProvider,
46
49
  RevealJsGenerator: () => RevealJsGenerator,
47
50
  ScoreCalculator: () => ScoreCalculator,
51
+ SevenDimensionScorer: () => SevenDimensionScorer,
48
52
  SlideFactory: () => SlideFactory,
53
+ SlideGenerator: () => SlideGenerator,
49
54
  TemplateEngine: () => TemplateEngine,
50
55
  TemplateNotFoundError: () => TemplateNotFoundError,
51
56
  UnsplashImageProvider: () => UnsplashImageProvider,
52
57
  VERSION: () => VERSION,
53
58
  ValidationError: () => ValidationError,
59
+ createAutoFixEngine: () => createAutoFixEngine,
54
60
  createDefaultChartProvider: () => createDefaultChartProvider,
55
61
  createDefaultImageProvider: () => createDefaultImageProvider,
62
+ createIterativeQAEngine: () => createIterativeQAEngine,
63
+ createSlideFactory: () => createSlideFactory,
56
64
  default: () => index_default,
57
65
  generate: () => generate,
58
66
  getKnowledgeGateway: () => getKnowledgeGateway,
67
+ initSlideGenerator: () => initSlideGenerator,
59
68
  validate: () => validate
60
69
  });
61
70
  module.exports = __toCommonJS(index_exports);
@@ -350,6 +359,439 @@ var KnowledgeGateway = class {
350
359
  this.ensureLoaded();
351
360
  return this.kb;
352
361
  }
362
+ // ==========================================================================
363
+ // KB-DRIVEN SLIDEFACTORY METHODS (v7.0.0)
364
+ // ==========================================================================
365
+ /**
366
+ * Get allowed slide types for a specific presentation type.
367
+ * CRITICAL: SlideFactory must ONLY use types from this list.
368
+ */
369
+ getAllowedSlideTypes(type) {
370
+ this.ensureLoaded();
371
+ const typeConfig = this.kb.presentation_types?.[type];
372
+ if (!typeConfig?.slide_types_allowed) {
373
+ const mode = this.getModeForType(type);
374
+ const templates = this.getSlideTemplates(mode);
375
+ return templates.map((t) => t.name.toLowerCase().replace(/\s+/g, "_"));
376
+ }
377
+ return typeConfig.slide_types_allowed;
378
+ }
379
+ /**
380
+ * Get validation rules for a specific presentation type.
381
+ * Returns word limits, bullet limits, whitespace requirements, etc.
382
+ */
383
+ getValidationRules(type) {
384
+ this.ensureLoaded();
385
+ const typeConfig = this.kb.presentation_types?.[type];
386
+ if (!typeConfig?.validation_rules) {
387
+ const mode = this.getModeForType(type);
388
+ return mode === "keynote" ? {
389
+ wordsPerSlide: { min: 1, max: 25, ideal: 10 },
390
+ whitespace: { min: 40, ideal: 50 },
391
+ bulletsPerSlide: { max: 3 },
392
+ actionTitlesRequired: false,
393
+ sourcesRequired: false
394
+ } : {
395
+ wordsPerSlide: { min: 40, max: 80, ideal: 60 },
396
+ whitespace: { min: 25, ideal: 30 },
397
+ bulletsPerSlide: { max: 5 },
398
+ actionTitlesRequired: true,
399
+ sourcesRequired: true
400
+ };
401
+ }
402
+ const rules = typeConfig.validation_rules;
403
+ return {
404
+ wordsPerSlide: {
405
+ min: rules.words_per_slide?.min ?? 1,
406
+ max: rules.words_per_slide?.max ?? 80,
407
+ ideal: rules.words_per_slide?.ideal ?? 40
408
+ },
409
+ whitespace: {
410
+ min: rules.whitespace?.min ?? 25,
411
+ ideal: rules.whitespace?.ideal ?? 30
412
+ },
413
+ bulletsPerSlide: {
414
+ max: rules.bullets_per_slide?.max ?? 5
415
+ },
416
+ actionTitlesRequired: rules.action_titles_required ?? false,
417
+ sourcesRequired: rules.sources_required ?? false,
418
+ calloutsRequired: rules.callouts_required,
419
+ denseDataAllowed: rules.dense_data_allowed,
420
+ codeBlocksAllowed: rules.code_blocks_allowed,
421
+ diagramsRequired: rules.diagrams_required
422
+ };
423
+ }
424
+ /**
425
+ * Get required elements that MUST be in the deck for this type.
426
+ */
427
+ getRequiredElements(type) {
428
+ this.ensureLoaded();
429
+ const typeConfig = this.kb.presentation_types?.[type];
430
+ return typeConfig?.required_elements ?? [];
431
+ }
432
+ /**
433
+ * Get anti-patterns to avoid for this presentation type.
434
+ */
435
+ getAntiPatternsForType(type) {
436
+ this.ensureLoaded();
437
+ const typeConfig = this.kb.presentation_types?.[type];
438
+ return typeConfig?.anti_patterns ?? [];
439
+ }
440
+ /**
441
+ * Get typography specifications for this presentation type.
442
+ */
443
+ getTypographyForType(type) {
444
+ this.ensureLoaded();
445
+ const typeConfig = this.kb.presentation_types?.[type];
446
+ const typo = typeConfig?.typography;
447
+ if (!typo) {
448
+ const mode = this.getModeForType(type);
449
+ const modeTypo = this.getTypography(mode);
450
+ const maxFontsValue = modeTypo.max_fonts;
451
+ return {
452
+ titles: modeTypo.titles ?? "48px, Bold",
453
+ body: modeTypo.body_text ?? "24px",
454
+ maxFonts: typeof maxFontsValue === "number" ? maxFontsValue : 2
455
+ };
456
+ }
457
+ return {
458
+ titles: typo.titles ?? "48px, Bold",
459
+ body: typo.body ?? "24px",
460
+ maxFonts: typeof typo.max_fonts === "number" ? typo.max_fonts : 2,
461
+ actionTitle: typo.action_title,
462
+ sectionHeaders: typo.section_headers,
463
+ dataLabels: typo.data_labels,
464
+ code: typo.code
465
+ };
466
+ }
467
+ /**
468
+ * Get CSS variables recipe for this presentation type.
469
+ */
470
+ getCSSVariablesForType(type) {
471
+ this.ensureLoaded();
472
+ const recipes = this.kb.type_visual_recipes;
473
+ if (recipes?.[type]?.css_variables) {
474
+ return recipes[type].css_variables;
475
+ }
476
+ return `:root {
477
+ --color-background: #FAFAF9;
478
+ --color-primary: #0F172A;
479
+ --color-secondary: #475569;
480
+ --color-accent: #0369A1;
481
+ --color-text: #18181B;
482
+ --font-display: 'Inter', sans-serif;
483
+ --font-body: 'Inter', sans-serif;
484
+ }`;
485
+ }
486
+ /**
487
+ * Get scoring weights for QA evaluation for this type.
488
+ */
489
+ getScoringWeights(type) {
490
+ this.ensureLoaded();
491
+ const typeConfig = this.kb.presentation_types?.[type];
492
+ const weights = typeConfig?.scoring_weights;
493
+ if (!weights) {
494
+ return {
495
+ visualQuality: 30,
496
+ contentQuality: 30,
497
+ expertCompliance: 30,
498
+ accessibility: 10
499
+ };
500
+ }
501
+ return {
502
+ visualQuality: weights.visual_quality ?? 30,
503
+ contentQuality: weights.content_quality ?? 30,
504
+ expertCompliance: weights.expert_compliance ?? 30,
505
+ accessibility: weights.accessibility ?? 10
506
+ };
507
+ }
508
+ /**
509
+ * Get story structure framework for this presentation type.
510
+ */
511
+ getStoryStructure(type) {
512
+ this.ensureLoaded();
513
+ const typeConfig = this.kb.presentation_types?.[type];
514
+ const mode = this.getModeForType(type);
515
+ if (mode === "business") {
516
+ return {
517
+ framework: "scqa",
518
+ requiredElements: ["situation", "complication", "answer"],
519
+ optionalElements: ["question", "evidence", "recommendation"]
520
+ };
521
+ }
522
+ return {
523
+ framework: "sparkline",
524
+ requiredElements: ["what_is", "what_could_be", "call_to_action"],
525
+ optionalElements: ["star_moment", "hook"]
526
+ };
527
+ }
528
+ /**
529
+ * Map a content pattern to the best allowed slide type.
530
+ * CRITICAL: This is how SlideFactory decides which slide type to use.
531
+ */
532
+ mapContentPatternToSlideType(pattern, allowedTypes) {
533
+ const patternToTypes = {
534
+ big_number: ["big_number", "big-number", "data_insight", "metrics_grid", "metrics-grid"],
535
+ comparison: ["comparison", "options_comparison", "two_column", "two-column"],
536
+ timeline: ["timeline", "process_timeline", "process", "roadmap"],
537
+ process: ["process", "process_timeline", "timeline", "three_column", "three-column"],
538
+ metrics: ["metrics_grid", "metrics-grid", "data_insight", "big_number", "big-number"],
539
+ quote: ["quote", "testimonial", "social_proof", "social-proof"],
540
+ code: ["code_snippet", "technical", "two_column", "two-column"],
541
+ bullets: ["bullet_points", "bullet-points", "detailed_findings", "two_column", "two-column"],
542
+ prose: ["two_column", "two-column", "bullet_points", "bullet-points", "single_statement"]
543
+ };
544
+ const preferredTypes = patternToTypes[pattern.primaryPattern] || patternToTypes.prose || [];
545
+ for (const preferred of preferredTypes) {
546
+ const underscoreVersion = preferred.replace(/-/g, "_");
547
+ const dashVersion = preferred.replace(/_/g, "-");
548
+ if (allowedTypes.includes(preferred)) return preferred;
549
+ if (allowedTypes.includes(underscoreVersion)) return underscoreVersion;
550
+ if (allowedTypes.includes(dashVersion)) return dashVersion;
551
+ }
552
+ const contentTypes = [
553
+ "bullet_points",
554
+ "bullet-points",
555
+ "two_column",
556
+ "two-column",
557
+ "three_column",
558
+ "three-column",
559
+ "data_insight"
560
+ ];
561
+ for (const ct of contentTypes) {
562
+ if (allowedTypes.includes(ct)) return ct;
563
+ }
564
+ return allowedTypes[0] ?? "bullet-points";
565
+ }
566
+ /**
567
+ * Get a specific slide template by name from the KB.
568
+ */
569
+ getSlideTemplateByName(name) {
570
+ this.ensureLoaded();
571
+ const keynoteTemplates = this.kb.slide_templates?.keynote_mode ?? [];
572
+ const businessTemplates = this.kb.slide_templates?.business_mode ?? [];
573
+ const normalizedName = name.toLowerCase().replace(/[-_]/g, " ");
574
+ for (const template of [...keynoteTemplates, ...businessTemplates]) {
575
+ const templateName = template.name.toLowerCase().replace(/[-_]/g, " ");
576
+ if (templateName.includes(normalizedName) || normalizedName.includes(templateName)) {
577
+ return {
578
+ name: template.name,
579
+ purpose: template.purpose,
580
+ elements: template.elements,
581
+ components: template.components,
582
+ wordLimit: template.word_limit ?? 60
583
+ };
584
+ }
585
+ }
586
+ return void 0;
587
+ }
588
+ // ==========================================================================
589
+ // SLIDE DEFAULTS & LABELS (v7.1.0 - Replace ALL Hardcoded Values)
590
+ // ==========================================================================
591
+ /**
592
+ * Get slide defaults for structural slides (titles, messages, labels).
593
+ * CRITICAL: SlideFactory must use these instead of hardcoded strings.
594
+ */
595
+ getSlideDefaults(type) {
596
+ this.ensureLoaded();
597
+ const mode = this.getModeForType(type);
598
+ const rules = this.getValidationRules(type);
599
+ const maxWords = rules.wordsPerSlide.max;
600
+ return {
601
+ agenda: { title: mode === "business" ? "Agenda" : "What We'll Cover" },
602
+ thankYou: {
603
+ title: mode === "business" ? "Thank You" : "Thank You",
604
+ subtitle: mode === "business" ? "Questions?" : "Let's Connect"
605
+ },
606
+ cta: {
607
+ title: mode === "business" ? "Next Steps" : "Take Action",
608
+ message: mode === "business" ? "Recommended Actions" : "Ready to Begin?",
609
+ fallback: mode === "business" ? "Contact us to learn more" : "Take the next step"
610
+ },
611
+ metricsGrid: {
612
+ title: mode === "business" ? "Key Metrics" : "The Numbers",
613
+ maxMetrics: 4
614
+ },
615
+ code: {
616
+ label: "Code Example",
617
+ maxChars: 500
618
+ },
619
+ comparison: {
620
+ leftLabel: mode === "business" ? "Current State" : "Before",
621
+ rightLabel: mode === "business" ? "Future State" : "After",
622
+ optionLabels: ["Option A", "Option B", "Option C", "Option D"]
623
+ },
624
+ column: { labelTemplate: "Point {n}" },
625
+ subtitle: { maxWords: Math.min(15, Math.floor(maxWords / 3)) },
626
+ context: { maxWords: Math.min(30, Math.floor(maxWords / 2)) },
627
+ step: { maxWords: Math.min(20, Math.floor(maxWords / 4)) },
628
+ columnContent: { maxWords: Math.min(25, Math.floor(maxWords / 3)) }
629
+ };
630
+ }
631
+ /**
632
+ * Get SCQA framework titles from KB (for business mode).
633
+ */
634
+ getSCQATitles(type) {
635
+ this.ensureLoaded();
636
+ const mode = this.getModeForType(type);
637
+ if (mode === "business") {
638
+ return {
639
+ situation: "Current Situation",
640
+ complication: "The Challenge",
641
+ question: "The Critical Question",
642
+ answer: "Our Recommendation"
643
+ };
644
+ }
645
+ return {
646
+ situation: "Where We Are",
647
+ complication: "What's At Stake",
648
+ question: "The Question We Face",
649
+ answer: "The Path Forward"
650
+ };
651
+ }
652
+ /**
653
+ * Get Sparkline framework titles from KB (for keynote mode).
654
+ */
655
+ getSparklineTitles(type) {
656
+ this.ensureLoaded();
657
+ return {
658
+ whatIs: "Where We Are Today",
659
+ whatCouldBe: "What Could Be",
660
+ callToAdventure: "The Call to Adventure",
661
+ newBliss: "The New World"
662
+ };
663
+ }
664
+ /**
665
+ * Get insight markers for detecting action titles.
666
+ * These are words/phrases that indicate an insight vs a topic.
667
+ */
668
+ getInsightMarkers() {
669
+ this.ensureLoaded();
670
+ return [
671
+ // Trend indicators
672
+ "increase",
673
+ "decrease",
674
+ "grew",
675
+ "declined",
676
+ "achieve",
677
+ "exceed",
678
+ "improve",
679
+ "reduce",
680
+ "save",
681
+ "gain",
682
+ "lost",
683
+ "doubled",
684
+ "tripled",
685
+ "outperform",
686
+ "underperform",
687
+ "accelerate",
688
+ "decelerate",
689
+ // Quantitative markers
690
+ "%",
691
+ "percent",
692
+ "million",
693
+ "billion",
694
+ "thousand",
695
+ "$",
696
+ "\u20AC",
697
+ "\xA3",
698
+ "ROI",
699
+ "revenue",
700
+ "cost",
701
+ "margin",
702
+ "growth",
703
+ "decline",
704
+ // Causality markers
705
+ "due to",
706
+ "because",
707
+ "resulting in",
708
+ "leading to",
709
+ "enabling",
710
+ "driving",
711
+ "caused by",
712
+ "attributed to",
713
+ "as a result of",
714
+ // Action verbs (Minto Pyramid style)
715
+ "should",
716
+ "must",
717
+ "need to",
718
+ "recommend",
719
+ "propose",
720
+ "suggest",
721
+ "requires",
722
+ "demands",
723
+ "enables",
724
+ "prevents",
725
+ "ensures"
726
+ ];
727
+ }
728
+ /**
729
+ * Get word limit for a specific slide element type.
730
+ */
731
+ getWordLimitForElement(type, element) {
732
+ const rules = this.getValidationRules(type);
733
+ const maxWords = rules.wordsPerSlide.max;
734
+ const bulletsMax = rules.bulletsPerSlide.max;
735
+ switch (element) {
736
+ case "title":
737
+ return Math.min(15, Math.floor(maxWords / 4));
738
+ case "subtitle":
739
+ return Math.min(15, Math.floor(maxWords / 5));
740
+ case "bullet":
741
+ return Math.floor(maxWords / (bulletsMax || 5));
742
+ case "body":
743
+ return maxWords;
744
+ case "quote":
745
+ return Math.min(40, maxWords);
746
+ case "step":
747
+ return Math.min(20, Math.floor(maxWords / 4));
748
+ default:
749
+ return maxWords;
750
+ }
751
+ }
752
+ /**
753
+ * Validate a slide against KB rules for the given presentation type.
754
+ */
755
+ validateSlideAgainstKB(slide, type) {
756
+ const rules = this.getValidationRules(type);
757
+ const allowedTypes = this.getAllowedSlideTypes(type);
758
+ const violations = [];
759
+ const warnings = [];
760
+ const normalizedType = slide.type.replace(/-/g, "_");
761
+ const isAllowed = allowedTypes.some(
762
+ (t) => t === slide.type || t === normalizedType || t.replace(/_/g, "-") === slide.type
763
+ );
764
+ if (!isAllowed) {
765
+ violations.push(`Slide type '${slide.type}' not allowed for ${type}. Allowed: ${allowedTypes.join(", ")}`);
766
+ }
767
+ if (slide.wordCount > rules.wordsPerSlide.max) {
768
+ violations.push(`Word count ${slide.wordCount} exceeds max ${rules.wordsPerSlide.max}`);
769
+ } else if (slide.wordCount < rules.wordsPerSlide.min) {
770
+ warnings.push(`Word count ${slide.wordCount} below min ${rules.wordsPerSlide.min}`);
771
+ }
772
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
773
+ violations.push(`Bullet count ${slide.bulletCount} exceeds max ${rules.bulletsPerSlide.max}`);
774
+ }
775
+ if (rules.actionTitlesRequired && !slide.hasActionTitle) {
776
+ violations.push(`Action title required for ${type} but not present`);
777
+ }
778
+ if (rules.sourcesRequired && !slide.hasSource) {
779
+ warnings.push(`Source citation recommended for ${type}`);
780
+ }
781
+ const fixes = {};
782
+ if (slide.wordCount > rules.wordsPerSlide.max) {
783
+ fixes.wordCount = rules.wordsPerSlide.max;
784
+ }
785
+ if (slide.bulletCount > rules.bulletsPerSlide.max) {
786
+ fixes.bulletCount = rules.bulletsPerSlide.max;
787
+ }
788
+ return {
789
+ valid: violations.length === 0,
790
+ violations,
791
+ warnings,
792
+ fixes
793
+ };
794
+ }
353
795
  };
354
796
  var gatewayInstance = null;
355
797
  async function getKnowledgeGateway() {
@@ -487,12 +929,22 @@ var ContentAnalyzer = class {
487
929
  }
488
930
  /**
489
931
  * Parse content based on type
932
+ * CRITICAL: Strip code blocks FIRST to prevent code from becoming slides
490
933
  */
491
934
  parseContent(content, contentType) {
492
- if (contentType === "markdown" || content.includes("#") || content.includes("- ")) {
493
- return content;
494
- }
495
- return content;
935
+ let text = content;
936
+ text = text.replace(/```[\s\S]*?```/g, "");
937
+ text = text.replace(/`[^`]{50,}`/g, "");
938
+ text = text.split("\n").filter((line) => {
939
+ const trimmed = line.trim();
940
+ if (/^(import|export|const|let|var|function|class|interface|type|async|await|return|if|for|while)\s/.test(trimmed)) return false;
941
+ if (/^(\/\/|\/\*|\*|#!)/.test(trimmed)) return false;
942
+ if (/^\s*[{}\[\]();]/.test(trimmed)) return false;
943
+ if (/^[\w.]+\s*\(.*\)\s*;?\s*$/.test(trimmed)) return false;
944
+ if (/^[\w.]+\s*=\s*/.test(trimmed) && /[{(\[]/.test(trimmed)) return false;
945
+ return true;
946
+ }).join("\n");
947
+ return text;
496
948
  }
497
949
  /**
498
950
  * Extract the main title from content
@@ -610,6 +1062,7 @@ var ContentAnalyzer = class {
610
1062
  }
611
1063
  /**
612
1064
  * Extract SCQA structure (Barbara Minto)
1065
+ * CRITICAL: Answer must be clean prose, NOT bullet points
613
1066
  */
614
1067
  extractSCQA(text) {
615
1068
  const paragraphs = text.split(/\n\n+/).filter((p) => p.trim());
@@ -617,34 +1070,45 @@ var ContentAnalyzer = class {
617
1070
  let complication = "";
618
1071
  let question = "";
619
1072
  let answer = "";
620
- for (const para of paragraphs.slice(0, 3)) {
1073
+ for (const para of paragraphs.slice(0, 5)) {
1074
+ if (para.startsWith("-") || para.startsWith("#") || para.startsWith("|")) continue;
621
1075
  if (this.containsSignals(para.toLowerCase(), this.situationSignals)) {
622
1076
  situation = this.extractFirstSentence(para);
623
1077
  break;
624
1078
  }
625
1079
  }
626
1080
  for (const para of paragraphs) {
1081
+ if (para.startsWith("-") || para.startsWith("|")) continue;
627
1082
  if (this.containsSignals(para.toLowerCase(), this.complicationSignals)) {
628
1083
  complication = this.extractFirstSentence(para);
629
1084
  break;
630
1085
  }
631
1086
  }
632
- const middleStart = Math.floor(paragraphs.length * 0.2);
633
- const middleEnd = Math.floor(paragraphs.length * 0.8);
634
- for (const para of paragraphs.slice(middleStart, middleEnd)) {
1087
+ for (const para of paragraphs) {
1088
+ if (para.startsWith("-") || para.includes("\n-") || para.startsWith("|")) continue;
1089
+ if (para.startsWith("*") && !para.startsWith("**")) continue;
635
1090
  const lowerPara = para.toLowerCase();
636
1091
  if (this.containsSignals(lowerPara, this.answerSignals) && !this.containsSignals(lowerPara, this.ctaSignals)) {
637
- answer = this.extractFirstSentence(para);
638
- break;
1092
+ const sentence = this.extractFirstSentence(para);
1093
+ if (sentence.split(/\s+/).length >= 8 && /\b(is|are|will|can|provides?|delivers?|enables?)\b/i.test(sentence)) {
1094
+ answer = sentence;
1095
+ break;
1096
+ }
639
1097
  }
640
1098
  }
641
- if (!situation && paragraphs.length > 0 && paragraphs[0]) {
642
- situation = this.extractFirstSentence(paragraphs[0]);
1099
+ if (!situation && paragraphs.length > 0) {
1100
+ for (const para of paragraphs.slice(0, 5)) {
1101
+ if (!para.startsWith("-") && !para.startsWith("#") && para.length > 50) {
1102
+ situation = this.extractFirstSentence(para);
1103
+ break;
1104
+ }
1105
+ }
643
1106
  }
644
- if (!answer && paragraphs.length > 2) {
645
- for (const para of paragraphs.slice(1, Math.floor(paragraphs.length * 0.5))) {
1107
+ if (!answer) {
1108
+ for (const para of paragraphs) {
1109
+ if (para.startsWith("-") || para.includes("\n-")) continue;
646
1110
  const lowerPara = para.toLowerCase();
647
- if (lowerPara.includes("recommend") || lowerPara.includes("strategy") || lowerPara.includes("solution") || lowerPara.includes("approach")) {
1111
+ if (lowerPara.includes("bottom line") || lowerPara.includes("delivers both") || lowerPara.includes("the result") || lowerPara.includes("in summary")) {
648
1112
  answer = this.extractFirstSentence(para);
649
1113
  break;
650
1114
  }
@@ -803,56 +1267,123 @@ var ContentAnalyzer = class {
803
1267
  }
804
1268
  /**
805
1269
  * Extract data points (metrics with values)
806
- * IMPROVED: Smarter label extraction that understands markdown tables
1270
+ * REWRITTEN: Proper markdown table parsing and meaningful label extraction
807
1271
  */
808
1272
  extractDataPoints(text) {
809
1273
  const dataPoints = [];
810
1274
  const usedValues = /* @__PURE__ */ new Set();
811
- const tableLines = text.split("\n").filter((line) => line.includes("|"));
812
- if (tableLines.length >= 3) {
813
- const dataRows = tableLines.filter((line) => !line.match(/^[\s|:-]+$/));
814
- if (dataRows.length >= 2) {
815
- for (const row of dataRows.slice(1)) {
816
- const cells = row.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
817
- if (cells.length >= 2) {
818
- const valueCell = cells.find((c) => /^\$?[\d,]+\.?\d*[MBK%]?$/.test(c.replace(/[,$]/g, "")));
819
- const labelCell = cells[0];
820
- if (valueCell && labelCell && !usedValues.has(valueCell)) {
821
- usedValues.add(valueCell);
822
- dataPoints.push({ value: valueCell, label: labelCell.slice(0, 40) });
823
- }
1275
+ const bulletPatterns = text.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?(?:\s*\([^)]+\))?)/gm);
1276
+ if (bulletPatterns) {
1277
+ for (const match of bulletPatterns) {
1278
+ const parsed = match.match(/^[-*]\s+([^:]+):\s*(\$?[\d,.]+[MBK%]?)/);
1279
+ if (parsed && parsed[1] && parsed[2]) {
1280
+ const label = parsed[1].trim();
1281
+ const value = parsed[2].trim();
1282
+ if (!usedValues.has(value) && label.length >= 5) {
1283
+ usedValues.add(value);
1284
+ dataPoints.push({ value, label: this.cleanMetricLabel(label) });
824
1285
  }
825
1286
  }
826
1287
  }
827
1288
  }
828
1289
  const lines = text.split("\n");
1290
+ let inTable = false;
1291
+ let tableRows = [];
1292
+ let headerRow = [];
829
1293
  for (const line of lines) {
830
- if (line.includes("|")) continue;
831
- const percentMatch = line.match(/(\w+(?:\s+\w+){0,4})\s+(?:by\s+)?(\d+(?:\.\d+)?%)/i);
832
- if (percentMatch && percentMatch[2] && percentMatch[1] && !usedValues.has(percentMatch[2])) {
833
- usedValues.add(percentMatch[2]);
834
- dataPoints.push({ value: percentMatch[2], label: percentMatch[1].slice(0, 40) });
1294
+ if (line.includes("|") && line.trim().length > 3) {
1295
+ const cells = line.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
1296
+ if (cells.every((c) => /^[-:]+$/.test(c))) continue;
1297
+ if (!inTable) {
1298
+ headerRow = cells;
1299
+ inTable = true;
1300
+ } else {
1301
+ tableRows.push(cells);
1302
+ }
1303
+ } else if (inTable && tableRows.length > 0) {
1304
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1305
+ inTable = false;
1306
+ tableRows = [];
1307
+ headerRow = [];
835
1308
  }
836
- const dollarMatch = line.match(/\$(\d+(?:\.\d+)?)\s*(million|billion|M|B|K)?/i);
837
- if (dollarMatch && dataPoints.length < 6) {
838
- const fullValue = "$" + dollarMatch[1] + (dollarMatch[2] ? " " + dollarMatch[2] : "");
839
- if (!usedValues.has(fullValue)) {
840
- usedValues.add(fullValue);
841
- const label = this.extractLabelFromLine(line, fullValue);
842
- dataPoints.push({ value: fullValue, label });
1309
+ }
1310
+ if (tableRows.length > 0) {
1311
+ this.extractMetricsFromTable(headerRow, tableRows, dataPoints, usedValues);
1312
+ }
1313
+ for (const line of lines) {
1314
+ if (line.includes("|")) continue;
1315
+ if (/```/.test(line)) continue;
1316
+ const percentPattern = line.match(/(\d+(?:\.\d+)?%)\s+(reduction|increase|improvement|growth|decrease)\s+(?:in\s+)?([^.]+)/i);
1317
+ if (percentPattern && percentPattern[1] && percentPattern[3]) {
1318
+ const value = percentPattern[1];
1319
+ if (!usedValues.has(value)) {
1320
+ usedValues.add(value);
1321
+ const action = percentPattern[2] || "change";
1322
+ const subject = percentPattern[3].slice(0, 30).trim();
1323
+ dataPoints.push({ value, label: `${action} in ${subject}` });
843
1324
  }
844
1325
  }
845
1326
  }
846
1327
  return dataPoints.slice(0, 4);
847
1328
  }
1329
+ /**
1330
+ * Extract metrics from a parsed markdown table
1331
+ */
1332
+ extractMetricsFromTable(headers, rows, dataPoints, usedValues) {
1333
+ for (const row of rows) {
1334
+ const label = row[0];
1335
+ if (!label || label.length < 3) continue;
1336
+ for (let i = 1; i < row.length; i++) {
1337
+ const cell = row[i];
1338
+ if (!cell) continue;
1339
+ if (/^\$?[\d,]+\.?\d*[%MBK]?$/.test(cell.replace(/[,\s]/g, "")) || /^\d+\s*(seconds?|minutes?|hours?|ms|gb|mb)$/i.test(cell)) {
1340
+ if (!usedValues.has(cell)) {
1341
+ usedValues.add(cell);
1342
+ const colHeader = headers[i] || "";
1343
+ const fullLabel = colHeader && colHeader !== label ? `${label} (${colHeader})` : label;
1344
+ dataPoints.push({
1345
+ value: cell,
1346
+ label: this.cleanMetricLabel(fullLabel)
1347
+ });
1348
+ break;
1349
+ }
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Clean a metric label to ensure it's complete and meaningful
1356
+ */
1357
+ cleanMetricLabel(raw) {
1358
+ let label = raw.replace(/\*\*/g, "").replace(/\|/g, " ").replace(/[:\-–—]+$/, "").replace(/\s+/g, " ").trim();
1359
+ if (label.length > 0) {
1360
+ label = label.charAt(0).toUpperCase() + label.slice(1);
1361
+ }
1362
+ if (label.length > 40) {
1363
+ const words = label.split(/\s+/);
1364
+ label = "";
1365
+ for (const word of words) {
1366
+ if ((label + " " + word).length <= 40) {
1367
+ label = label ? label + " " + word : word;
1368
+ } else {
1369
+ break;
1370
+ }
1371
+ }
1372
+ }
1373
+ return label || "Value";
1374
+ }
848
1375
  /**
849
1376
  * Extract a meaningful label from a line containing a metric
850
1377
  */
851
1378
  extractLabelFromLine(line, value) {
1379
+ const colonMatch = line.match(/([^:]+):\s*\$?[\d]/);
1380
+ if (colonMatch && colonMatch[1]) {
1381
+ return this.cleanMetricLabel(colonMatch[1]);
1382
+ }
852
1383
  const cleaned = line.replace(/\*\*/g, "").replace(/\|/g, " ").trim();
853
1384
  const beforeValue = cleaned.split(value)[0] || "";
854
1385
  const words = beforeValue.split(/\s+/).filter((w) => w.length > 2);
855
- return words.slice(-4).join(" ").slice(0, 40) || "Value";
1386
+ return this.cleanMetricLabel(words.slice(-4).join(" "));
856
1387
  }
857
1388
  /**
858
1389
  * Check if text contains any of the signals
@@ -879,531 +1410,897 @@ var ContentAnalyzer = class {
879
1410
  }
880
1411
  };
881
1412
 
882
- // src/core/SlideFactory.ts
883
- var SlideFactory = class {
884
- templates;
885
- usedContent = /* @__PURE__ */ new Set();
886
- constructor() {
887
- this.templates = this.initializeTemplates();
888
- }
1413
+ // src/core/ContentPatternClassifier.ts
1414
+ var ContentPatternClassifier = class {
1415
+ // Regex patterns for content detection
1416
+ patterns = {
1417
+ // Big numbers: $4.88M, 23%, 10x, 500+, etc.
1418
+ bigNumber: /(\$[\d,.]+[MBK]?|\d+(?:\.\d+)?%|\d+[xX]|\d{2,}(?:\+|,\d{3})+)/,
1419
+ // Comparison language
1420
+ 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,
1421
+ // Timeline markers
1422
+ 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,
1423
+ // Process/steps language
1424
+ process: /\b(step\s*\d|stage\s*\d|pillar|phase|first|second|third|finally|then|next|workflow|process)\b/i,
1425
+ // Quote patterns
1426
+ quote: /^[""]|[""]\s*[-—–]\s*|^\s*>\s*|[""]$/,
1427
+ // Code patterns
1428
+ code: /```|`[^`]+`|function\s*\(|const\s+\w+\s*=|import\s+{|class\s+\w+|=>\s*{/,
1429
+ // Metric patterns (for detecting multiple metrics)
1430
+ metric: /(\d+(?:\.\d+)?(?:%|[xX]|[MBK])?)\s*[-–:]\s*|\b(?:increased|decreased|grew|improved|reduced)\s+(?:by\s+)?(\d+)/gi
1431
+ };
889
1432
  /**
890
- * Check if content has already been used (deduplication)
1433
+ * Classify a content section to determine its primary pattern.
1434
+ * Returns a ContentPattern object used by SlideFactory to select slide type.
891
1435
  */
892
- isContentUsed(content) {
893
- if (!content) return true;
894
- const normalized = content.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 50);
895
- if (this.usedContent.has(normalized)) {
896
- return true;
1436
+ classify(section) {
1437
+ const fullText = `${section.header} ${section.content} ${section.bullets.join(" ")}`;
1438
+ const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
1439
+ const hasBigNumber = this.patterns.bigNumber.test(fullText);
1440
+ const bigNumberMatch = fullText.match(this.patterns.bigNumber);
1441
+ const hasComparison = this.patterns.comparison.test(fullText);
1442
+ const hasTimeline = this.patterns.timeline.test(fullText);
1443
+ const hasProcess = this.patterns.process.test(section.header) || section.bullets.length >= 3 && this.hasNumberedItems(section.bullets);
1444
+ const hasQuote = this.patterns.quote.test(section.content);
1445
+ const hasCode = this.patterns.code.test(section.content);
1446
+ const metricMatches = fullText.match(this.patterns.metric);
1447
+ const hasMetrics = section.metrics.length >= 3 || metricMatches && metricMatches.length >= 3;
1448
+ let primaryPattern = "prose";
1449
+ if (hasQuote && section.content.length > 20) {
1450
+ primaryPattern = "quote";
1451
+ } else if (hasCode) {
1452
+ primaryPattern = "code";
1453
+ } else if (hasBigNumber && wordCount < 30) {
1454
+ primaryPattern = "big_number";
1455
+ } else if (hasMetrics) {
1456
+ primaryPattern = "metrics";
1457
+ } else if (hasComparison) {
1458
+ primaryPattern = "comparison";
1459
+ } else if (hasTimeline) {
1460
+ primaryPattern = "timeline";
1461
+ } else if (hasProcess && section.bullets.length >= 3) {
1462
+ primaryPattern = "process";
1463
+ } else if (section.bullets.length >= 2) {
1464
+ primaryPattern = "bullets";
1465
+ } else {
1466
+ primaryPattern = "prose";
897
1467
  }
898
- this.usedContent.add(normalized);
899
- return false;
1468
+ const result = {
1469
+ hasBigNumber,
1470
+ hasComparison,
1471
+ hasTimeline,
1472
+ hasProcess,
1473
+ hasMetrics: !!hasMetrics,
1474
+ hasQuote,
1475
+ hasCode,
1476
+ bulletCount: section.bullets.length,
1477
+ wordCount,
1478
+ primaryPattern
1479
+ };
1480
+ if (bigNumberMatch && bigNumberMatch[1]) {
1481
+ result.bigNumberValue = bigNumberMatch[1];
1482
+ }
1483
+ return result;
900
1484
  }
901
1485
  /**
902
- * Create slides from analyzed content.
903
- *
904
- * ARCHITECTURE (per KB expert methodologies):
905
- * 1. Title slide - always first
906
- * 2. Agenda slide - business mode only with 3+ sections
907
- * 3. SCQA slides - Situation, Complication (per Minto)
908
- * 4. Content slides - from sections with bullets/content
909
- * 5. Metrics slide - if data points exist
910
- * 6. Solution slide - SCQA answer
911
- * 7. STAR moments - only if high-quality (per Duarte)
912
- * 8. CTA slide - if call to action exists
913
- * 9. Thank you slide - always last
914
- */
915
- async createSlides(analysis, mode) {
916
- const slides = [];
917
- let slideIndex = 0;
918
- this.usedContent.clear();
919
- slides.push(this.createTitleSlide(slideIndex++, analysis));
920
- this.isContentUsed(analysis.titles[0] ?? "");
921
- const substantiveSections = analysis.sections.filter(
922
- (s) => s.level === 2 && (s.bullets.length > 0 || s.content.length > 50)
923
- );
924
- if (mode === "business" && substantiveSections.length >= 3) {
925
- slides.push(this.createAgendaSlide(slideIndex++, analysis));
926
- }
927
- if (analysis.scqa.situation && analysis.scqa.situation.length > 30 && !this.isContentUsed(analysis.scqa.situation)) {
928
- slides.push(this.createContextSlide(slideIndex++, analysis, mode));
929
- }
930
- if (analysis.scqa.complication && analysis.scqa.complication.length > 30 && !this.isContentUsed(analysis.scqa.complication)) {
931
- slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
932
- }
933
- for (const section of substantiveSections.slice(0, 4)) {
934
- const headerUsed = this.isContentUsed(section.header);
935
- const contentUsed = section.content.length > 30 && this.isContentUsed(section.content.slice(0, 80));
936
- if (!headerUsed && !contentUsed) {
937
- if (section.bullets.length > 0) {
938
- slides.push(this.createSectionBulletSlide(slideIndex++, section, mode));
939
- } else if (section.content.length > 50) {
940
- slides.push(this.createSectionContentSlide(slideIndex++, section, mode));
941
- }
1486
+ * Check if bullets appear to be numbered/ordered items
1487
+ */
1488
+ hasNumberedItems(bullets) {
1489
+ let numberedCount = 0;
1490
+ for (const bullet of bullets) {
1491
+ if (/^\d+[.)]\s|^[a-z][.)]\s|^step\s*\d/i.test(bullet.trim())) {
1492
+ numberedCount++;
942
1493
  }
943
1494
  }
944
- for (const message of analysis.keyMessages) {
945
- const wordCount = message.split(/\s+/).length;
946
- if (wordCount >= 6 && !/^(The |Our |Your |Overview|Introduction|Conclusion)/i.test(message) && !this.isContentUsed(message)) {
947
- slides.push(this.createMessageSlide(slideIndex++, message, mode));
1495
+ return numberedCount >= 2;
1496
+ }
1497
+ /**
1498
+ * Extract the big number value from content for display
1499
+ */
1500
+ extractBigNumber(content) {
1501
+ const match = content.match(this.patterns.bigNumber);
1502
+ if (!match || !match[1]) return null;
1503
+ const value = match[1];
1504
+ const afterNumber = content.slice(content.indexOf(value) + value.length);
1505
+ const context = afterNumber.trim().slice(0, 60).split(/[.!?]/)[0]?.trim() ?? "";
1506
+ return { value, context };
1507
+ }
1508
+ /**
1509
+ * Extract comparison elements from content
1510
+ */
1511
+ extractComparison(section) {
1512
+ const text = section.content;
1513
+ const vsMatch = text.match(/(.{10,50})\s+(?:vs\.?|versus)\s+(.{10,50})/i);
1514
+ if (vsMatch && vsMatch[1] && vsMatch[2]) {
1515
+ return { left: vsMatch[1].trim(), right: vsMatch[2].trim() };
1516
+ }
1517
+ const beforeAfterMatch = text.match(/before[:\s]+(.{10,100})(?:after[:\s]+(.{10,100}))?/i);
1518
+ if (beforeAfterMatch && beforeAfterMatch[1]) {
1519
+ const left = beforeAfterMatch[1].trim();
1520
+ const right = beforeAfterMatch[2] ? beforeAfterMatch[2].trim() : "After";
1521
+ return { left, right };
1522
+ }
1523
+ if (section.bullets.length === 2) {
1524
+ const left = section.bullets[0];
1525
+ const right = section.bullets[1];
1526
+ if (left && right) {
1527
+ return { left, right };
948
1528
  }
949
1529
  }
950
- if (analysis.dataPoints.length >= 2) {
951
- slides.push(this.createMetricsSlide(slideIndex++, analysis.dataPoints));
952
- }
953
- if (analysis.scqa.answer && analysis.scqa.answer.length > 30 && !this.isContentUsed(analysis.scqa.answer)) {
954
- slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
955
- }
956
- const verbPattern = /\b(is|are|was|were|will|can|should|must|has|have|provides?|enables?|allows?|achieves?|exceeds?|results?|generates?|delivers?|creates?)\b/i;
957
- for (const starMoment of analysis.starMoments.slice(0, 2)) {
958
- const wordCount = starMoment.split(/\s+/).length;
959
- const hasVerb = verbPattern.test(starMoment);
960
- if (wordCount >= 6 && starMoment.length >= 40 && hasVerb && !this.isContentUsed(starMoment)) {
961
- slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
1530
+ return null;
1531
+ }
1532
+ /**
1533
+ * Extract timeline/process steps from content
1534
+ */
1535
+ extractSteps(section) {
1536
+ const steps = [];
1537
+ for (const bullet of section.bullets) {
1538
+ const stepMatch = bullet.match(/^((?:step|phase|stage)\s*\d+|[1-9]\.|\d+\))\s*[-:.]?\s*(.+)/i);
1539
+ if (stepMatch && stepMatch[1] && stepMatch[2]) {
1540
+ steps.push({
1541
+ label: stepMatch[1].trim(),
1542
+ description: stepMatch[2].trim()
1543
+ });
1544
+ } else {
1545
+ steps.push({
1546
+ label: `${steps.length + 1}`,
1547
+ description: bullet
1548
+ });
962
1549
  }
963
1550
  }
964
- if (analysis.sparkline.callToAdventure && analysis.sparkline.callToAdventure.length > 20 && !this.isContentUsed(analysis.sparkline.callToAdventure)) {
965
- slides.push(this.createCTASlide(slideIndex++, analysis, mode));
966
- }
967
- slides.push(this.createThankYouSlide(slideIndex++));
968
- return slides;
1551
+ return steps;
969
1552
  }
970
1553
  /**
971
- * Create a slide from a section with bullets
1554
+ * Determine if content is suitable for a three-column layout
1555
+ * (3 pillars, 3 key points, etc.)
972
1556
  */
973
- createSectionBulletSlide(index, section, mode) {
974
- const bullets = section.bullets.slice(0, mode === "keynote" ? 3 : 5);
975
- return {
976
- index,
977
- type: "bullet-points",
978
- data: {
979
- title: this.truncate(section.header, 60),
980
- bullets: bullets.map((b) => this.cleanText(b).slice(0, 80))
981
- },
982
- classes: ["slide-bullet-points"]
983
- };
1557
+ isSuitableForThreeColumn(section) {
1558
+ if (section.bullets.length === 3) return true;
1559
+ if (/three|3\s*(pillar|point|key|step)/i.test(section.header)) return true;
1560
+ const threePartPattern = /first[,.].*second[,.].*third/i;
1561
+ if (threePartPattern.test(section.content)) return true;
1562
+ return false;
984
1563
  }
985
1564
  /**
986
- * Create a slide from a section with body content
1565
+ * Check if content should be displayed as a quote slide
987
1566
  */
988
- createSectionContentSlide(index, section, mode) {
989
- if (mode === "keynote") {
990
- return {
991
- index,
992
- type: "single-statement",
993
- data: {
994
- title: this.truncate(this.extractFirstSentence(section.content), 80),
995
- keyMessage: section.header
996
- },
997
- classes: ["slide-single-statement"]
998
- };
1567
+ isQuoteContent(section) {
1568
+ const content = section.content.trim();
1569
+ if (!this.patterns.quote.test(content)) {
1570
+ return { isQuote: false };
999
1571
  }
1000
- return {
1001
- index,
1002
- type: "two-column",
1003
- data: {
1004
- title: this.truncate(section.header, 60),
1005
- body: this.truncate(section.content, 200)
1006
- },
1007
- classes: ["slide-two-column"]
1008
- };
1572
+ const attrMatch = content.match(/[-—–]\s*(.+)$/);
1573
+ if (attrMatch && attrMatch[1]) {
1574
+ return { isQuote: true, attribution: attrMatch[1].trim() };
1575
+ }
1576
+ return { isQuote: true };
1009
1577
  }
1010
- /**
1011
- * Extract first sentence from text
1012
- */
1013
- extractFirstSentence(text) {
1014
- const cleaned = this.cleanText(text);
1015
- const match = cleaned.match(/^[^.!?]+[.!?]/);
1016
- return match ? match[0].trim() : cleaned.slice(0, 100);
1578
+ };
1579
+
1580
+ // src/core/SlideFactory.ts
1581
+ var SlideFactory = class {
1582
+ kb;
1583
+ presentationType;
1584
+ classifier;
1585
+ config;
1586
+ usedContent;
1587
+ usedTitles;
1588
+ constructor(kb, type) {
1589
+ this.kb = kb;
1590
+ this.presentationType = type;
1591
+ this.classifier = new ContentPatternClassifier();
1592
+ this.usedContent = /* @__PURE__ */ new Set();
1593
+ this.usedTitles = /* @__PURE__ */ new Set();
1594
+ this.config = this.loadKBConfig(type);
1595
+ console.log(` \u2713 SlideFactory v7.1.0 initialized for ${type} (${this.config.mode} mode)`);
1596
+ console.log(` \u2713 KB Config loaded: ${this.config.allowedTypes.length} allowed types`);
1017
1597
  }
1018
1598
  /**
1019
- * Create a metrics slide from data points
1599
+ * Load ALL configuration from KB - ZERO hardcoded values after this.
1020
1600
  */
1021
- createMetricsSlide(index, dataPoints) {
1022
- const metrics = dataPoints.slice(0, 4).map((dp) => ({
1023
- value: dp.value,
1024
- label: this.cleanText(dp.label).slice(0, 40)
1025
- }));
1601
+ loadKBConfig(type) {
1026
1602
  return {
1027
- index,
1028
- type: "metrics-grid",
1029
- data: {
1030
- title: "Key Metrics",
1031
- metrics
1032
- },
1033
- classes: ["slide-metrics-grid"]
1603
+ defaults: this.kb.getSlideDefaults(type),
1604
+ scqaTitles: this.kb.getSCQATitles(type),
1605
+ sparklineTitles: this.kb.getSparklineTitles(type),
1606
+ rules: this.kb.getValidationRules(type),
1607
+ allowedTypes: this.kb.getAllowedSlideTypes(type),
1608
+ mode: this.kb.getModeForType(type),
1609
+ insightMarkers: this.kb.getInsightMarkers(),
1610
+ glanceTest: this.kb.getDuarteGlanceTest(),
1611
+ millersLaw: this.kb.getMillersLaw(),
1612
+ typography: this.kb.getTypographyForType(type),
1613
+ antiPatterns: this.kb.getAntiPatternsForType(type),
1614
+ requiredElements: this.kb.getRequiredElements(type)
1034
1615
  };
1035
1616
  }
1036
1617
  /**
1037
- * Create a title slide.
1618
+ * Create all slides from content analysis.
1619
+ * This is the main entry point - orchestrates the entire slide creation process.
1038
1620
  */
1621
+ async createSlides(analysis) {
1622
+ const slides = [];
1623
+ const storyStructure = this.kb.getStoryStructure(this.presentationType);
1624
+ console.log(` \u2713 Using ${storyStructure.framework} story framework`);
1625
+ slides.push(this.createTitleSlide(0, analysis));
1626
+ const minSectionsForAgenda = Math.max(3, this.config.rules.bulletsPerSlide.max - 2);
1627
+ if (this.config.mode === "business" && analysis.sections.length >= minSectionsForAgenda) {
1628
+ slides.push(this.createAgendaSlide(slides.length, analysis));
1629
+ }
1630
+ if (storyStructure.framework === "scqa") {
1631
+ this.addSCQASlides(slides, analysis);
1632
+ } else if (storyStructure.framework === "sparkline") {
1633
+ this.addSparklineSlides(slides, analysis);
1634
+ }
1635
+ for (const section of analysis.sections) {
1636
+ const contentKey = this.normalizeKey(section.header);
1637
+ if (this.usedContent.has(contentKey)) continue;
1638
+ this.usedContent.add(contentKey);
1639
+ const pattern = this.classifier.classify(section);
1640
+ const slideType = this.kb.mapContentPatternToSlideType(pattern, this.config.allowedTypes);
1641
+ const slide = this.createSlideByType(slides.length, slideType, section, pattern);
1642
+ if (slide) {
1643
+ slides.push(slide);
1644
+ }
1645
+ }
1646
+ const minDataPointsForMetrics = 2;
1647
+ if (analysis.dataPoints.length >= minDataPointsForMetrics) {
1648
+ const hasMetricsSlide = slides.some(
1649
+ (s) => s.type === "metrics-grid" || s.type === "big-number"
1650
+ );
1651
+ if (!hasMetricsSlide) {
1652
+ slides.push(this.createMetricsGridSlide(slides.length, analysis.dataPoints));
1653
+ }
1654
+ }
1655
+ if (analysis.sparkline?.callToAdventure || analysis.scqa?.answer) {
1656
+ slides.push(this.createCTASlide(slides.length, analysis));
1657
+ }
1658
+ slides.push(this.createThankYouSlide(slides.length));
1659
+ const validatedSlides = this.validateAndFixSlides(slides);
1660
+ this.checkRequiredElements(validatedSlides);
1661
+ this.checkAntiPatterns(validatedSlides);
1662
+ console.log(` \u2713 Created ${validatedSlides.length} validated slides`);
1663
+ return validatedSlides;
1664
+ }
1665
+ // ===========================================================================
1666
+ // SLIDE TYPE ROUTER - Routes to appropriate creator based on KB-selected type
1667
+ // ===========================================================================
1668
+ createSlideByType(index, type, section, pattern) {
1669
+ const normalizedType = type.toLowerCase().replace(/_/g, "-");
1670
+ switch (normalizedType) {
1671
+ case "big-number":
1672
+ case "data-insight":
1673
+ return this.createBigNumberSlide(index, section, pattern);
1674
+ case "comparison":
1675
+ case "options-comparison":
1676
+ return this.createComparisonSlide(index, section);
1677
+ case "timeline":
1678
+ case "process-timeline":
1679
+ case "roadmap":
1680
+ return this.createTimelineSlide(index, section);
1681
+ case "process":
1682
+ return this.createProcessSlide(index, section);
1683
+ case "three-column":
1684
+ return this.createThreeColumnSlide(index, section);
1685
+ case "two-column":
1686
+ return this.createTwoColumnSlide(index, section);
1687
+ case "quote":
1688
+ case "testimonial":
1689
+ case "social-proof":
1690
+ return this.createQuoteSlide(index, section);
1691
+ case "metrics-grid":
1692
+ return this.createMetricsGridSlide(index, section.metrics);
1693
+ case "bullet-points":
1694
+ case "detailed-findings":
1695
+ return this.createBulletSlide(index, section);
1696
+ case "single-statement":
1697
+ case "big-idea":
1698
+ return this.createSingleStatementSlide(index, section);
1699
+ case "code-snippet":
1700
+ case "technical":
1701
+ return this.createCodeSlide(index, section);
1702
+ default:
1703
+ console.log(` \u26A0 Unknown slide type '${type}', using bullet-points`);
1704
+ return this.createBulletSlide(index, section);
1705
+ }
1706
+ }
1707
+ // ===========================================================================
1708
+ // STRUCTURAL SLIDES (title, agenda, thank you) - ALL from KB
1709
+ // ===========================================================================
1039
1710
  createTitleSlide(index, analysis) {
1040
- let subtitle = "";
1041
- if (analysis.scqa.answer && analysis.scqa.answer.length > 10) {
1042
- subtitle = analysis.scqa.answer;
1043
- } else if (analysis.starMoments.length > 0 && analysis.starMoments[0]) {
1044
- subtitle = analysis.starMoments[0];
1711
+ const data = {
1712
+ title: this.truncateText(analysis.title, this.config.rules.wordsPerSlide.max)
1713
+ };
1714
+ if (analysis.keyMessages[0]) {
1715
+ data.subtitle = this.truncateText(
1716
+ analysis.keyMessages[0],
1717
+ this.config.defaults.subtitle.maxWords
1718
+ // FROM KB
1719
+ );
1045
1720
  }
1046
1721
  return {
1047
1722
  index,
1048
1723
  type: "title",
1049
- data: {
1050
- title: analysis.titles[0] ?? "Presentation",
1051
- subtitle: this.truncate(subtitle, 80),
1052
- keyMessage: analysis.scqa.answer
1053
- },
1054
- classes: ["slide-title"]
1724
+ data,
1725
+ classes: ["title-slide"]
1055
1726
  };
1056
1727
  }
1057
- /**
1058
- * Create an agenda slide.
1059
- */
1060
1728
  createAgendaSlide(index, analysis) {
1729
+ const agendaItems = analysis.sections.filter((s) => s.level <= 2).slice(0, this.config.rules.bulletsPerSlide.max).map((s) => this.cleanText(s.header));
1061
1730
  return {
1062
1731
  index,
1063
1732
  type: "agenda",
1064
1733
  data: {
1065
- title: "Agenda",
1066
- bullets: analysis.keyMessages.map((msg, i) => `${i + 1}. ${this.truncate(msg, 50)}`)
1734
+ title: this.config.defaults.agenda.title,
1735
+ // FROM KB - not hardcoded 'Agenda'
1736
+ bullets: agendaItems
1067
1737
  },
1068
- classes: ["slide-agenda"]
1738
+ classes: ["agenda-slide"]
1069
1739
  };
1070
1740
  }
1071
- /**
1072
- * Create a context/situation slide.
1073
- */
1074
- createContextSlide(index, analysis, mode) {
1075
- if (mode === "keynote") {
1076
- return {
1077
- index,
1741
+ createThankYouSlide(index) {
1742
+ return {
1743
+ index,
1744
+ type: "thank-you",
1745
+ data: {
1746
+ title: this.config.defaults.thankYou.title,
1747
+ // FROM KB - not hardcoded 'Thank You'
1748
+ subtitle: this.config.defaults.thankYou.subtitle
1749
+ // FROM KB - not hardcoded 'Questions?'
1750
+ },
1751
+ classes: ["thank-you-slide"]
1752
+ };
1753
+ }
1754
+ // ===========================================================================
1755
+ // STORY FRAMEWORK SLIDES (SCQA, Sparkline) - ALL titles from KB
1756
+ // ===========================================================================
1757
+ addSCQASlides(slides, analysis) {
1758
+ const scqa = analysis.scqa;
1759
+ const titles = this.config.scqaTitles;
1760
+ if (scqa?.situation && !this.usedContent.has("scqa-situation")) {
1761
+ this.usedContent.add("scqa-situation");
1762
+ slides.push({
1763
+ index: slides.length,
1078
1764
  type: "single-statement",
1079
1765
  data: {
1080
- title: this.truncate(analysis.scqa.situation, 80),
1081
- keyMessage: "The current state"
1766
+ title: titles.situation,
1767
+ // FROM KB - not hardcoded 'Current Situation'
1768
+ body: this.truncateText(scqa.situation, this.config.rules.wordsPerSlide.max)
1082
1769
  },
1083
- classes: ["slide-single-statement"]
1084
- };
1770
+ classes: ["situation-slide"]
1771
+ });
1772
+ }
1773
+ if (scqa?.complication && !this.usedContent.has("scqa-complication")) {
1774
+ this.usedContent.add("scqa-complication");
1775
+ slides.push({
1776
+ index: slides.length,
1777
+ type: "single-statement",
1778
+ data: {
1779
+ title: titles.complication,
1780
+ // FROM KB - not hardcoded 'The Challenge'
1781
+ body: this.truncateText(scqa.complication, this.config.rules.wordsPerSlide.max)
1782
+ },
1783
+ classes: ["complication-slide"]
1784
+ });
1785
+ }
1786
+ if (scqa?.question && !this.usedContent.has("scqa-question")) {
1787
+ this.usedContent.add("scqa-question");
1788
+ slides.push({
1789
+ index: slides.length,
1790
+ type: "single-statement",
1791
+ data: {
1792
+ title: titles.question,
1793
+ // FROM KB - not hardcoded 'The Question'
1794
+ body: this.truncateText(scqa.question, this.config.rules.wordsPerSlide.max)
1795
+ },
1796
+ classes: ["question-slide"]
1797
+ });
1798
+ }
1799
+ }
1800
+ addSparklineSlides(slides, analysis) {
1801
+ const spark = analysis.sparkline;
1802
+ const titles = this.config.sparklineTitles;
1803
+ const whatIsFirst = spark?.whatIs?.[0];
1804
+ if (whatIsFirst && !this.usedContent.has("spark-what-is")) {
1805
+ this.usedContent.add("spark-what-is");
1806
+ slides.push({
1807
+ index: slides.length,
1808
+ type: "single-statement",
1809
+ data: {
1810
+ title: titles.whatIs,
1811
+ // FROM KB - not hardcoded 'Where We Are Today'
1812
+ body: this.truncateText(whatIsFirst, this.config.rules.wordsPerSlide.max)
1813
+ },
1814
+ classes: ["what-is-slide"]
1815
+ });
1816
+ }
1817
+ const whatCouldBeFirst = spark?.whatCouldBe?.[0];
1818
+ if (whatCouldBeFirst && !this.usedContent.has("spark-could-be")) {
1819
+ this.usedContent.add("spark-could-be");
1820
+ slides.push({
1821
+ index: slides.length,
1822
+ type: "single-statement",
1823
+ data: {
1824
+ title: titles.whatCouldBe,
1825
+ // FROM KB - not hardcoded 'What Could Be'
1826
+ body: this.truncateText(whatCouldBeFirst, this.config.rules.wordsPerSlide.max)
1827
+ },
1828
+ classes: ["what-could-be-slide"]
1829
+ });
1085
1830
  }
1831
+ }
1832
+ // ===========================================================================
1833
+ // CONTENT SLIDES - ALL values from KB
1834
+ // ===========================================================================
1835
+ createBigNumberSlide(index, section, pattern) {
1836
+ const bigNumber = this.classifier.extractBigNumber(
1837
+ `${section.header} ${section.content} ${section.bullets.join(" ")}`
1838
+ );
1086
1839
  return {
1087
1840
  index,
1088
- type: "two-column",
1841
+ type: "big-number",
1089
1842
  data: {
1090
- title: "Current Situation",
1091
- body: analysis.scqa.situation,
1092
- bullets: analysis.sparkline.whatIs.slice(0, 3)
1843
+ title: this.createTitle(section.header, section),
1844
+ keyMessage: bigNumber?.value || pattern.bigNumberValue || "0",
1845
+ body: bigNumber?.context || this.truncateText(
1846
+ section.content,
1847
+ this.config.defaults.context.maxWords
1848
+ // FROM KB - not hardcoded 30
1849
+ )
1093
1850
  },
1094
- classes: ["slide-two-column"]
1851
+ classes: ["big-number-slide"]
1095
1852
  };
1096
1853
  }
1097
- /**
1098
- * Create a problem/complication slide.
1099
- */
1100
- createProblemSlide(index, analysis, mode) {
1101
- if (mode === "keynote") {
1102
- return {
1103
- index,
1104
- type: "big-idea",
1105
- data: {
1106
- title: this.truncate(analysis.scqa.complication, 60),
1107
- keyMessage: "The challenge we face"
1108
- },
1109
- classes: ["slide-big-idea"]
1110
- };
1111
- }
1854
+ createComparisonSlide(index, section) {
1855
+ const comparison = this.classifier.extractComparison(section);
1856
+ const labels = this.config.defaults.comparison;
1857
+ const leftFallback = labels.optionLabels[0] ?? "Option A";
1858
+ const rightFallback = labels.optionLabels[1] ?? "Option B";
1859
+ const leftColumn = comparison?.left || section.bullets[0] || leftFallback;
1860
+ const rightColumn = comparison?.right || section.bullets[1] || rightFallback;
1112
1861
  return {
1113
1862
  index,
1114
- type: "bullet-points",
1863
+ type: "comparison",
1115
1864
  data: {
1116
- title: "The Challenge",
1117
- body: analysis.scqa.complication,
1118
- bullets: this.extractBullets(analysis.scqa.complication)
1865
+ title: this.createTitle(section.header, section),
1866
+ columns: [
1867
+ {
1868
+ title: labels.leftLabel,
1869
+ // FROM KB - not hardcoded 'Before'
1870
+ content: this.truncateText(leftColumn, this.config.rules.wordsPerSlide.max)
1871
+ },
1872
+ {
1873
+ title: labels.rightLabel,
1874
+ // FROM KB - not hardcoded 'After'
1875
+ content: this.truncateText(rightColumn, this.config.rules.wordsPerSlide.max)
1876
+ }
1877
+ ]
1119
1878
  },
1120
- classes: ["slide-bullet-points"]
1879
+ classes: ["comparison-slide"]
1121
1880
  };
1122
1881
  }
1123
- /**
1124
- * Create a key message slide.
1125
- */
1126
- createMessageSlide(index, message, mode) {
1127
- if (mode === "keynote") {
1128
- return {
1129
- index,
1130
- type: "single-statement",
1131
- data: {
1132
- title: this.truncate(message, 60),
1133
- keyMessage: message
1134
- },
1135
- classes: ["slide-single-statement"]
1136
- };
1137
- }
1882
+ createTimelineSlide(index, section) {
1883
+ const steps = this.classifier.extractSteps(section);
1884
+ const maxSteps = Math.min(
1885
+ steps.length,
1886
+ this.config.rules.bulletsPerSlide.max,
1887
+ this.config.millersLaw.maxItems
1888
+ // FROM KB - 7±2 rule
1889
+ );
1138
1890
  return {
1139
1891
  index,
1140
- type: "bullet-points",
1892
+ type: "timeline",
1141
1893
  data: {
1142
- title: this.extractActionTitle(message),
1143
- body: message,
1144
- bullets: this.extractBullets(message)
1894
+ title: this.createTitle(section.header, section),
1895
+ steps: steps.slice(0, maxSteps).map((step) => ({
1896
+ label: step.label,
1897
+ description: this.truncateText(
1898
+ step.description,
1899
+ this.config.defaults.step.maxWords
1900
+ // FROM KB - not hardcoded 20
1901
+ )
1902
+ }))
1145
1903
  },
1146
- classes: ["slide-bullet-points"]
1904
+ classes: ["timeline-slide"]
1147
1905
  };
1148
1906
  }
1149
- /**
1150
- * Create a STAR moment slide.
1151
- */
1152
- createStarMomentSlide(index, starMoment, mode) {
1153
- const statMatch = starMoment.match(/(\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion))?)/);
1154
- if (statMatch && statMatch[1]) {
1155
- const stat = statMatch[1];
1156
- return {
1157
- index,
1158
- type: "big-number",
1159
- data: {
1160
- title: stat,
1161
- subtitle: this.removeStatistic(starMoment, stat),
1162
- keyMessage: starMoment
1163
- },
1164
- classes: ["slide-big-number"]
1165
- };
1907
+ createProcessSlide(index, section) {
1908
+ const steps = this.classifier.extractSteps(section);
1909
+ const maxSteps = Math.min(steps.length, this.config.rules.bulletsPerSlide.max);
1910
+ return {
1911
+ index,
1912
+ type: "process",
1913
+ data: {
1914
+ title: this.createTitle(section.header, section),
1915
+ steps: steps.slice(0, maxSteps).map((step, i) => ({
1916
+ number: i + 1,
1917
+ title: step.label,
1918
+ description: this.truncateText(
1919
+ step.description,
1920
+ this.config.defaults.step.maxWords
1921
+ // FROM KB - not hardcoded 15
1922
+ )
1923
+ }))
1924
+ },
1925
+ classes: ["process-slide"]
1926
+ };
1927
+ }
1928
+ createThreeColumnSlide(index, section) {
1929
+ const columns = section.bullets.slice(0, 3);
1930
+ while (columns.length < 3) {
1931
+ columns.push("");
1166
1932
  }
1167
- if (mode === "keynote") {
1168
- return {
1169
- index,
1170
- type: "big-idea",
1171
- data: {
1172
- title: this.truncate(starMoment, 80),
1173
- keyMessage: "A key insight"
1933
+ const labelTemplate = this.config.defaults.column.labelTemplate;
1934
+ return {
1935
+ index,
1936
+ type: "three-column",
1937
+ data: {
1938
+ title: this.createTitle(section.header, section),
1939
+ columns: columns.map((content, i) => ({
1940
+ title: labelTemplate.replace("{n}", String(i + 1)),
1941
+ // FROM KB - not hardcoded 'Point ${i+1}'
1942
+ content: this.truncateText(
1943
+ content,
1944
+ this.config.defaults.columnContent.maxWords
1945
+ // FROM KB - not hardcoded 25
1946
+ )
1947
+ }))
1948
+ },
1949
+ classes: ["three-column-slide"]
1950
+ };
1951
+ }
1952
+ createTwoColumnSlide(index, section) {
1953
+ const midpoint = Math.ceil(section.bullets.length / 2);
1954
+ const leftBullets = section.bullets.slice(0, midpoint);
1955
+ const rightBullets = section.bullets.slice(midpoint);
1956
+ const wordsPerBullet = Math.floor(
1957
+ this.config.rules.wordsPerSlide.max / this.config.rules.bulletsPerSlide.max
1958
+ );
1959
+ return {
1960
+ index,
1961
+ type: "two-column",
1962
+ data: {
1963
+ title: this.createTitle(section.header, section),
1964
+ leftColumn: {
1965
+ bullets: leftBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
1174
1966
  },
1175
- classes: ["slide-big-idea"]
1176
- };
1967
+ rightColumn: {
1968
+ bullets: rightBullets.map((b) => this.truncateText(this.cleanText(b), wordsPerBullet))
1969
+ }
1970
+ },
1971
+ classes: ["two-column-slide"]
1972
+ };
1973
+ }
1974
+ createQuoteSlide(index, section) {
1975
+ const quoteInfo = this.classifier.isQuoteContent(section);
1976
+ const quoteText = section.content.replace(/^[""]|[""]$/g, "").trim();
1977
+ const quoteWordLimit = Math.min(
1978
+ this.config.glanceTest.wordLimit * 2,
1979
+ // FROM KB - not hardcoded
1980
+ this.config.rules.wordsPerSlide.max
1981
+ );
1982
+ const data = {
1983
+ title: this.createTitle(section.header, section),
1984
+ quote: this.truncateText(quoteText, quoteWordLimit)
1985
+ };
1986
+ if (quoteInfo.attribution) {
1987
+ data.attribution = quoteInfo.attribution;
1177
1988
  }
1178
1989
  return {
1179
1990
  index,
1180
1991
  type: "quote",
1992
+ data,
1993
+ classes: ["quote-slide"]
1994
+ };
1995
+ }
1996
+ createMetricsGridSlide(index, metrics) {
1997
+ const maxMetrics = this.config.defaults.metricsGrid.maxMetrics;
1998
+ const cleanedMetrics = metrics.slice(0, maxMetrics).map((m) => ({
1999
+ value: this.cleanText(m.value),
2000
+ label: this.cleanMetricLabel(m.label)
2001
+ }));
2002
+ return {
2003
+ index,
2004
+ type: "metrics-grid",
1181
2005
  data: {
1182
- quote: starMoment,
1183
- attribution: "Key Insight"
2006
+ title: this.config.defaults.metricsGrid.title,
2007
+ // FROM KB - not hardcoded 'Key Metrics'
2008
+ metrics: cleanedMetrics
1184
2009
  },
1185
- classes: ["slide-quote"]
2010
+ classes: ["metrics-grid-slide"]
1186
2011
  };
1187
2012
  }
1188
- /**
1189
- * Create a solution/answer slide.
1190
- */
1191
- createSolutionSlide(index, analysis, mode) {
1192
- if (mode === "keynote") {
1193
- return {
1194
- index,
1195
- type: "big-idea",
1196
- data: {
1197
- title: this.truncate(analysis.scqa.answer, 60),
1198
- keyMessage: "Our answer"
1199
- },
1200
- classes: ["slide-big-idea"]
1201
- };
2013
+ createBulletSlide(index, section) {
2014
+ const maxBullets = this.config.rules.bulletsPerSlide.max;
2015
+ const wordsPerBullet = Math.floor(this.config.rules.wordsPerSlide.max / maxBullets);
2016
+ const cleanedBullets = section.bullets.slice(0, maxBullets).map((b) => this.truncateText(this.cleanText(b), wordsPerBullet)).filter((b) => b.length > 0);
2017
+ if (cleanedBullets.length === 0 && section.content) {
2018
+ return this.createSingleStatementSlide(index, section);
1202
2019
  }
1203
2020
  return {
1204
2021
  index,
1205
- type: "two-column",
2022
+ type: "bullet-points",
1206
2023
  data: {
1207
- title: "The Solution",
1208
- body: analysis.scqa.answer,
1209
- bullets: analysis.sparkline.whatCouldBe.slice(0, 4)
2024
+ title: this.createTitle(section.header, section),
2025
+ bullets: cleanedBullets
1210
2026
  },
1211
- classes: ["slide-two-column"]
2027
+ classes: ["bullet-points-slide"]
1212
2028
  };
1213
2029
  }
1214
- /**
1215
- * Create a call-to-action slide.
1216
- */
1217
- createCTASlide(index, analysis, mode) {
2030
+ createSingleStatementSlide(index, section) {
1218
2031
  return {
1219
2032
  index,
1220
- type: "cta",
2033
+ type: "single-statement",
1221
2034
  data: {
1222
- title: mode === "keynote" ? "Take Action" : "Next Steps",
1223
- body: analysis.sparkline.callToAdventure,
1224
- keyMessage: "What we need from you"
2035
+ title: this.createTitle(section.header, section),
2036
+ body: this.truncateText(
2037
+ section.content || section.bullets[0] || "",
2038
+ this.config.rules.wordsPerSlide.max
2039
+ // FROM KB
2040
+ )
1225
2041
  },
1226
- classes: ["slide-cta"]
2042
+ classes: ["single-statement-slide"]
1227
2043
  };
1228
2044
  }
1229
- /**
1230
- * Create a thank you slide.
1231
- */
1232
- createThankYouSlide(index) {
2045
+ createCodeSlide(index, section) {
2046
+ const codeMatch = section.content.match(/```[\s\S]*?```|`[^`]+`/);
2047
+ const code = codeMatch ? codeMatch[0].replace(/```/g, "").trim() : section.content;
1233
2048
  return {
1234
2049
  index,
1235
- type: "thank-you",
2050
+ type: "two-column",
2051
+ // Code slides use two-column layout
1236
2052
  data: {
1237
- title: "Thank You",
1238
- subtitle: "Questions?"
2053
+ title: this.createTitle(section.header, section),
2054
+ leftColumn: {
2055
+ body: this.config.defaults.code.label
2056
+ // FROM KB - not hardcoded 'Code Example'
2057
+ },
2058
+ rightColumn: {
2059
+ body: code.slice(0, this.config.defaults.code.maxChars)
2060
+ // FROM KB - not hardcoded 500
2061
+ }
1239
2062
  },
1240
- classes: ["slide-thank-you"]
2063
+ classes: ["code-slide"]
1241
2064
  };
1242
2065
  }
1243
- /**
1244
- * Initialize slide templates with constraints.
1245
- */
1246
- initializeTemplates() {
1247
- const templates = /* @__PURE__ */ new Map();
1248
- templates.set("title", {
1249
- type: "title",
1250
- requiredFields: ["title"],
1251
- optionalFields: ["subtitle", "author", "date"],
1252
- keynoteSuitable: true,
1253
- businessSuitable: true,
1254
- maxWords: 15
1255
- });
1256
- templates.set("big-idea", {
1257
- type: "big-idea",
1258
- requiredFields: ["title"],
1259
- optionalFields: ["keyMessage"],
1260
- keynoteSuitable: true,
1261
- businessSuitable: false,
1262
- maxWords: 10
1263
- });
1264
- templates.set("single-statement", {
1265
- type: "single-statement",
1266
- requiredFields: ["title"],
1267
- optionalFields: ["keyMessage"],
1268
- keynoteSuitable: true,
1269
- businessSuitable: false,
1270
- maxWords: 15
1271
- });
1272
- templates.set("big-number", {
1273
- type: "big-number",
1274
- requiredFields: ["title"],
1275
- optionalFields: ["subtitle", "source"],
1276
- keynoteSuitable: true,
1277
- businessSuitable: true,
1278
- maxWords: 10
1279
- });
1280
- templates.set("quote", {
1281
- type: "quote",
1282
- requiredFields: ["quote"],
1283
- optionalFields: ["attribution", "source"],
1284
- keynoteSuitable: true,
1285
- businessSuitable: true,
1286
- maxWords: 30
1287
- });
1288
- templates.set("bullet-points", {
1289
- type: "bullet-points",
1290
- requiredFields: ["title", "bullets"],
1291
- optionalFields: ["body"],
1292
- keynoteSuitable: false,
1293
- businessSuitable: true,
1294
- maxWords: 80
1295
- });
1296
- templates.set("two-column", {
1297
- type: "two-column",
1298
- requiredFields: ["title"],
1299
- optionalFields: ["body", "bullets", "images"],
1300
- keynoteSuitable: false,
1301
- businessSuitable: true,
1302
- maxWords: 100
1303
- });
1304
- templates.set("agenda", {
1305
- type: "agenda",
1306
- requiredFields: ["title", "bullets"],
1307
- optionalFields: [],
1308
- keynoteSuitable: false,
1309
- businessSuitable: true,
1310
- maxWords: 50
1311
- });
1312
- templates.set("cta", {
2066
+ createCTASlide(index, analysis) {
2067
+ const ctaDefaults = this.config.defaults.cta;
2068
+ const ctaText = analysis.sparkline?.callToAdventure || analysis.scqa?.answer || ctaDefaults.fallback;
2069
+ return {
2070
+ index,
1313
2071
  type: "cta",
1314
- requiredFields: ["title"],
1315
- optionalFields: ["body", "keyMessage"],
1316
- keynoteSuitable: true,
1317
- businessSuitable: true,
1318
- maxWords: 30
1319
- });
1320
- templates.set("thank-you", {
1321
- type: "thank-you",
1322
- requiredFields: ["title"],
1323
- optionalFields: ["subtitle"],
1324
- keynoteSuitable: true,
1325
- businessSuitable: true,
1326
- maxWords: 10
1327
- });
1328
- return templates;
2072
+ data: {
2073
+ title: ctaDefaults.title,
2074
+ // FROM KB - not hardcoded 'Next Steps'
2075
+ body: this.truncateText(ctaText, this.config.rules.wordsPerSlide.max),
2076
+ keyMessage: ctaDefaults.message
2077
+ // FROM KB - not hardcoded 'Ready to Begin?'
2078
+ },
2079
+ classes: ["cta-slide"]
2080
+ };
2081
+ }
2082
+ // ===========================================================================
2083
+ // VALIDATION - Ensure all slides comply with KB rules AND FIX ISSUES
2084
+ // ===========================================================================
2085
+ validateAndFixSlides(slides) {
2086
+ const validatedSlides = [];
2087
+ for (const slide of slides) {
2088
+ const wordCount = this.countWords(slide);
2089
+ const bulletCount = slide.data.bullets?.length || 0;
2090
+ const validation = this.kb.validateSlideAgainstKB(
2091
+ {
2092
+ type: slide.type,
2093
+ wordCount,
2094
+ bulletCount,
2095
+ hasActionTitle: this.isActionTitle(slide.data.title),
2096
+ hasSource: !!slide.data.source
2097
+ },
2098
+ this.presentationType
2099
+ );
2100
+ if (validation.fixes.wordCount) {
2101
+ if (slide.data.body) {
2102
+ slide.data.body = this.truncateText(slide.data.body, validation.fixes.wordCount);
2103
+ }
2104
+ if (slide.data.bullets) {
2105
+ const wordsPerBullet = Math.floor(validation.fixes.wordCount / slide.data.bullets.length);
2106
+ slide.data.bullets = slide.data.bullets.map((b) => this.truncateText(b, wordsPerBullet));
2107
+ }
2108
+ }
2109
+ if (validation.fixes.bulletCount && slide.data.bullets) {
2110
+ slide.data.bullets = slide.data.bullets.slice(0, validation.fixes.bulletCount);
2111
+ }
2112
+ if (validation.violations.length > 0) {
2113
+ console.log(` \u26A0 Slide ${slide.index} (${slide.type}): Fixed ${Object.keys(validation.fixes).length} issues, ${validation.violations.length} remaining`);
2114
+ }
2115
+ if (validation.warnings.length > 0) {
2116
+ slide.notes = (slide.notes || "") + "\nWarnings: " + validation.warnings.join(", ");
2117
+ }
2118
+ validatedSlides.push(slide);
2119
+ }
2120
+ return validatedSlides;
1329
2121
  }
1330
- // === Helper Methods ===
1331
2122
  /**
1332
- * Clean text by removing all markdown and content markers.
1333
- * CRITICAL: Must strip all formatting to prevent garbage in slides
2123
+ * Check that required elements exist in the deck (from KB)
1334
2124
  */
1335
- cleanText(text) {
1336
- if (!text) return "";
1337
- return text.replace(/^#+\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\[HEADER\]\s*/g, "").replace(/\[BULLET\]\s*/g, "").replace(/\[NUMBERED\]\s*/g, "").replace(/\[EMPHASIS\]/g, "").replace(/\[\/EMPHASIS\]/g, "").replace(/\[CODE BLOCK\]/g, "").replace(/\[IMAGE\]/g, "").replace(/\n{2,}/g, " ").replace(/\s+/g, " ").trim();
2125
+ checkRequiredElements(slides) {
2126
+ const required = this.config.requiredElements;
2127
+ if (required.length === 0) return;
2128
+ const slideTypes = slides.map((s) => s.type);
2129
+ const missingElements = [];
2130
+ for (const element of required) {
2131
+ const lowerElement = element.toLowerCase();
2132
+ if (lowerElement.includes("hook") || lowerElement.includes("opening")) {
2133
+ const titleSlide = slides.find((s) => s.type === "title");
2134
+ if (!titleSlide || !titleSlide.data.subtitle) {
2135
+ missingElements.push(element);
2136
+ }
2137
+ }
2138
+ if (lowerElement.includes("call to action") || lowerElement.includes("cta")) {
2139
+ if (!slideTypes.includes("cta")) {
2140
+ missingElements.push(element);
2141
+ }
2142
+ }
2143
+ if (lowerElement.includes("star moment") || lowerElement.includes("memorable")) {
2144
+ const hasStarMoment = slideTypes.some(
2145
+ (t) => t === "big-number" || t === "quote" || t === "single-statement"
2146
+ );
2147
+ if (!hasStarMoment) {
2148
+ missingElements.push(element);
2149
+ }
2150
+ }
2151
+ }
2152
+ if (missingElements.length > 0) {
2153
+ console.log(` \u26A0 Missing required elements: ${missingElements.join(", ")}`);
2154
+ }
1338
2155
  }
1339
2156
  /**
1340
- * Truncate text to max length at sentence boundary when possible.
1341
- * CRITICAL: Never cut mid-number (99.5% should not become 99.)
2157
+ * Check for anti-patterns from KB
1342
2158
  */
1343
- truncate(text, maxLength) {
1344
- const cleanedText = this.cleanText(text);
1345
- if (!cleanedText || cleanedText.length <= maxLength) {
1346
- return cleanedText;
1347
- }
1348
- const sentences = cleanedText.match(/[^.!?]+[.!?]/g);
1349
- if (sentences) {
1350
- let result = "";
1351
- for (const sentence of sentences) {
1352
- if ((result + sentence).length <= maxLength) {
1353
- result += sentence;
1354
- } else {
1355
- break;
2159
+ checkAntiPatterns(slides) {
2160
+ const antiPatterns = this.config.antiPatterns;
2161
+ if (antiPatterns.length === 0) return;
2162
+ const violations = [];
2163
+ for (const pattern of antiPatterns) {
2164
+ const lowerPattern = pattern.toLowerCase();
2165
+ if (lowerPattern.includes("bullet") && this.config.mode === "keynote") {
2166
+ const bulletSlides = slides.filter(
2167
+ (s) => s.type === "bullet-points" || s.data.bullets && s.data.bullets.length > 3
2168
+ );
2169
+ if (bulletSlides.length > 0) {
2170
+ violations.push(`${pattern} (${bulletSlides.length} slides)`);
1356
2171
  }
1357
2172
  }
1358
- if (result.length > maxLength * 0.5) {
1359
- return result.trim();
2173
+ if (lowerPattern.includes("text") || lowerPattern.includes("overload")) {
2174
+ const overloadedSlides = slides.filter((s) => {
2175
+ const wordCount = this.countWords(s);
2176
+ return wordCount > this.config.glanceTest.wordLimit;
2177
+ });
2178
+ if (overloadedSlides.length > 0) {
2179
+ violations.push(`${pattern} (${overloadedSlides.length} slides exceed glance test)`);
2180
+ }
1360
2181
  }
1361
2182
  }
1362
- const truncated = cleanedText.slice(0, maxLength);
1363
- let lastSpace = truncated.lastIndexOf(" ");
1364
- const afterCut = cleanedText.slice(lastSpace + 1, maxLength + 10);
1365
- if (/^[\d.,%$]+/.test(afterCut)) {
1366
- lastSpace = truncated.slice(0, lastSpace).lastIndexOf(" ");
2183
+ if (violations.length > 0) {
2184
+ console.log(` \u26A0 Anti-pattern violations: ${violations.join(", ")}`);
2185
+ }
2186
+ }
2187
+ // ===========================================================================
2188
+ // HELPER METHODS - All use KB configuration
2189
+ // ===========================================================================
2190
+ /**
2191
+ * Create a title for a slide - uses action titles for business mode per KB
2192
+ */
2193
+ createTitle(header, section) {
2194
+ const cleanHeader = this.cleanText(header);
2195
+ if (this.usedTitles.has(cleanHeader.toLowerCase())) {
2196
+ return `${cleanHeader} (continued)`;
1367
2197
  }
1368
- if (lastSpace > maxLength * 0.5) {
1369
- return truncated.slice(0, lastSpace) + "...";
2198
+ this.usedTitles.add(cleanHeader.toLowerCase());
2199
+ if (this.config.rules.actionTitlesRequired) {
2200
+ return this.createActionTitle(cleanHeader, section);
1370
2201
  }
1371
- return truncated + "...";
2202
+ return cleanHeader;
1372
2203
  }
1373
2204
  /**
1374
- * Extract an action title from a message.
2205
+ * Create an action title (Minto/McKinsey style)
2206
+ * Title should communicate the conclusion, not the topic
1375
2207
  */
1376
- extractActionTitle(message) {
1377
- const cleanedMessage = this.cleanText(message);
1378
- const firstSentence = cleanedMessage.split(/[.!?]/)[0];
1379
- if (firstSentence && firstSentence.length <= 50) {
1380
- return firstSentence;
2208
+ createActionTitle(header, section) {
2209
+ const titleWordLimit = this.kb.getWordLimitForElement(this.presentationType, "title");
2210
+ const firstBullet = section.bullets[0];
2211
+ if (firstBullet && this.isInsightful(firstBullet)) {
2212
+ return this.truncateText(this.cleanText(firstBullet), titleWordLimit);
2213
+ }
2214
+ if (section.content && this.isInsightful(section.content)) {
2215
+ const sentences = section.content.split(/[.!?]/);
2216
+ const firstSentence = sentences[0];
2217
+ if (firstSentence) {
2218
+ return this.truncateText(this.cleanText(firstSentence), titleWordLimit);
2219
+ }
1381
2220
  }
1382
- const words = cleanedMessage.split(/\s+/).slice(0, 6);
1383
- return words.join(" ");
2221
+ return header;
2222
+ }
2223
+ /**
2224
+ * Check if text contains an insight (not just a topic) - uses KB markers
2225
+ */
2226
+ isInsightful(text) {
2227
+ const lowerText = text.toLowerCase();
2228
+ return this.config.insightMarkers.some(
2229
+ (marker) => (
2230
+ // FROM KB - not hardcoded array
2231
+ lowerText.includes(marker.toLowerCase())
2232
+ )
2233
+ );
2234
+ }
2235
+ /**
2236
+ * Check if a title is an action title
2237
+ */
2238
+ isActionTitle(title) {
2239
+ if (!title) return false;
2240
+ return this.isInsightful(title);
2241
+ }
2242
+ /**
2243
+ * Count words in a slide
2244
+ */
2245
+ countWords(slide) {
2246
+ let text = "";
2247
+ const data = slide.data;
2248
+ if (data.title) text += data.title + " ";
2249
+ if (data.subtitle) text += data.subtitle + " ";
2250
+ if (data.body) text += data.body + " ";
2251
+ if (data.keyMessage) text += data.keyMessage + " ";
2252
+ if (data.bullets) text += data.bullets.join(" ") + " ";
2253
+ if (data.quote) text += data.quote + " ";
2254
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
1384
2255
  }
1385
2256
  /**
1386
- * Extract bullet points from text.
2257
+ * Truncate text to word limit at sentence boundaries
1387
2258
  */
1388
- extractBullets(text) {
1389
- if (!text) return [];
1390
- const bulletMatches = text.match(/\[BULLET\]\s*(.+)/g);
1391
- if (bulletMatches && bulletMatches.length > 0) {
1392
- return bulletMatches.map((b) => this.cleanText(b)).slice(0, 5);
2259
+ truncateText(text, maxWords) {
2260
+ const cleaned = this.cleanText(text);
2261
+ const words = cleaned.split(/\s+/);
2262
+ if (words.length <= maxWords) {
2263
+ return cleaned;
2264
+ }
2265
+ const sentences = cleaned.match(/[^.!?]+[.!?]?/g) || [cleaned];
2266
+ let result = "";
2267
+ for (const sentence of sentences) {
2268
+ const testResult = result + sentence;
2269
+ if (testResult.split(/\s+/).length <= maxWords) {
2270
+ result = testResult;
2271
+ } else {
2272
+ break;
2273
+ }
1393
2274
  }
1394
- const cleanedText = this.cleanText(text);
1395
- const sentences = cleanedText.split(/[.!?]+/).filter((s) => s.trim().length > 10);
1396
- return sentences.slice(0, 5).map((s) => s.trim());
2275
+ if (result.trim().length > 0) {
2276
+ return result.trim();
2277
+ }
2278
+ return words.slice(0, maxWords).join(" ") + "...";
2279
+ }
2280
+ /**
2281
+ * Clean text by removing content markers and normalizing
2282
+ */
2283
+ cleanText(text) {
2284
+ if (!text) return "";
2285
+ 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();
2286
+ }
2287
+ /**
2288
+ * Clean metric labels (strip table syntax)
2289
+ */
2290
+ cleanMetricLabel(label) {
2291
+ if (!label) return "";
2292
+ return label.replace(/\|/g, " ").replace(/-{3,}/g, "").replace(/\s{2,}/g, " ").replace(/^\s*\d+\.\s*/, "").trim();
1397
2293
  }
1398
2294
  /**
1399
- * Remove a statistic from text and clean thoroughly.
2295
+ * Normalize a key for deduplication
1400
2296
  */
1401
- removeStatistic(text, stat) {
1402
- const cleaned = this.cleanText(text).replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
1403
- const firstSentence = cleaned.match(/^[^.!?]+[.!?]?/);
1404
- return firstSentence ? firstSentence[0].slice(0, 80) : cleaned.slice(0, 80);
2297
+ normalizeKey(text) {
2298
+ return text.toLowerCase().replace(/[^a-z0-9]/g, "");
1405
2299
  }
1406
2300
  };
2301
+ function createSlideFactory(kb, type) {
2302
+ return new SlideFactory(kb, type);
2303
+ }
1407
2304
 
1408
2305
  // src/core/TemplateEngine.ts
1409
2306
  var import_handlebars = __toESM(require("handlebars"));
@@ -2814,99 +3711,1044 @@ var QAEngine = class {
2814
3711
  score -= Math.round((1 - signalPass / slideCount) * 30);
2815
3712
  return Math.max(0, score);
2816
3713
  }
2817
- calculateExpertScore(results) {
2818
- const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
2819
- const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
2820
- return totalScore / experts.length;
3714
+ calculateExpertScore(results) {
3715
+ const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
3716
+ const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
3717
+ return totalScore / experts.length;
3718
+ }
3719
+ calculateA11yScore(results) {
3720
+ let score = 100;
3721
+ score -= Math.min(40, results.fontSizeIssues.length * 10);
3722
+ score -= Math.min(40, results.contrastIssues.length * 10);
3723
+ if (results.focusCoverage < 1) score -= 20;
3724
+ return Math.max(0, score);
3725
+ }
3726
+ // ===========================================================================
3727
+ // ISSUE COLLECTION
3728
+ // ===========================================================================
3729
+ collectIssues(visual, content, expert, accessibility) {
3730
+ const issues = [];
3731
+ visual.perSlide.forEach((slide) => {
3732
+ slide.issues.forEach((issue) => {
3733
+ issues.push({
3734
+ severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
3735
+ category: "visual",
3736
+ slideIndex: slide.slideIndex,
3737
+ message: issue
3738
+ });
3739
+ });
3740
+ });
3741
+ content.perSlide.forEach((slide) => {
3742
+ slide.issues.forEach((issue) => {
3743
+ issues.push({
3744
+ severity: "warning",
3745
+ category: "content",
3746
+ slideIndex: slide.slideIndex,
3747
+ message: issue
3748
+ });
3749
+ });
3750
+ });
3751
+ content.glanceTest.filter((g) => !g.passed).forEach((g) => {
3752
+ const issue = {
3753
+ severity: "warning",
3754
+ category: "content",
3755
+ slideIndex: g.slideIndex,
3756
+ message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
3757
+ };
3758
+ if (g.recommendation) {
3759
+ issue.suggestion = g.recommendation;
3760
+ }
3761
+ issues.push(issue);
3762
+ });
3763
+ [expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
3764
+ e.violations.forEach((v) => {
3765
+ issues.push({
3766
+ severity: "warning",
3767
+ category: "expert",
3768
+ message: `${e.expertName}: ${v}`
3769
+ });
3770
+ });
3771
+ });
3772
+ accessibility.fontSizeIssues.forEach((issue) => {
3773
+ issues.push({
3774
+ severity: "error",
3775
+ category: "accessibility",
3776
+ slideIndex: issue.slideIndex,
3777
+ message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
3778
+ suggestion: `Increase font size to at least ${issue.minimumSize}px`
3779
+ });
3780
+ });
3781
+ accessibility.contrastIssues.forEach((issue) => {
3782
+ issues.push({
3783
+ severity: "error",
3784
+ category: "accessibility",
3785
+ slideIndex: issue.slideIndex,
3786
+ message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
3787
+ suggestion: "Increase contrast between text and background"
3788
+ });
3789
+ });
3790
+ return issues;
3791
+ }
3792
+ // ===========================================================================
3793
+ // BROWSER MANAGEMENT
3794
+ // ===========================================================================
3795
+ async initBrowser() {
3796
+ if (!this.browser) {
3797
+ this.browser = await import_playwright.chromium.launch({ headless: true });
3798
+ }
3799
+ }
3800
+ async closeBrowser() {
3801
+ if (this.browser) {
3802
+ await this.browser.close();
3803
+ this.browser = null;
3804
+ }
3805
+ }
3806
+ };
3807
+
3808
+ // src/qa/SevenDimensionScorer.ts
3809
+ var DIMENSION_WEIGHTS = {
3810
+ layout: 0.15,
3811
+ contrast: 0.15,
3812
+ graphics: 0.1,
3813
+ content: 0.2,
3814
+ clarity: 0.15,
3815
+ effectiveness: 0.15,
3816
+ consistency: 0.1
3817
+ };
3818
+ var SevenDimensionScorer = class {
3819
+ kb;
3820
+ mode;
3821
+ presentationType;
3822
+ constructor(mode, presentationType) {
3823
+ this.mode = mode;
3824
+ this.presentationType = presentationType;
3825
+ }
3826
+ /**
3827
+ * Score a presentation across all 7 dimensions.
3828
+ */
3829
+ async score(slides, html, threshold = 95) {
3830
+ this.kb = await getKnowledgeGateway();
3831
+ const layout = await this.scoreLayout(slides, html);
3832
+ const contrast = await this.scoreContrast(html);
3833
+ const graphics = await this.scoreGraphics(slides);
3834
+ const content = await this.scoreContent(slides);
3835
+ const clarity = await this.scoreClarity(slides);
3836
+ const effectiveness = await this.scoreEffectiveness(slides);
3837
+ const consistency = await this.scoreConsistency(slides, html);
3838
+ const overallScore = Math.round(
3839
+ 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
3840
+ );
3841
+ const issues = [
3842
+ ...layout.issues,
3843
+ ...contrast.issues,
3844
+ ...graphics.issues,
3845
+ ...content.issues,
3846
+ ...clarity.issues,
3847
+ ...effectiveness.issues,
3848
+ ...consistency.issues
3849
+ ];
3850
+ return {
3851
+ overallScore,
3852
+ dimensions: {
3853
+ layout,
3854
+ contrast,
3855
+ graphics,
3856
+ content,
3857
+ clarity,
3858
+ effectiveness,
3859
+ consistency
3860
+ },
3861
+ issues,
3862
+ passed: overallScore >= threshold,
3863
+ threshold
3864
+ };
3865
+ }
3866
+ /**
3867
+ * Score layout dimension (whitespace, visual balance, structure)
3868
+ */
3869
+ async scoreLayout(slides, html) {
3870
+ const issues = [];
3871
+ let totalScore = 0;
3872
+ let checks = 0;
3873
+ const minWhitespace = this.mode === "keynote" ? 0.45 : 0.35;
3874
+ const maxWhitespace = 0.6;
3875
+ const slideSections = html.match(/<section[^>]*>[\s\S]*?<\/section>/gi) || [];
3876
+ for (let i = 0; i < slideSections.length; i++) {
3877
+ const section = slideSections[i];
3878
+ if (!section) continue;
3879
+ const textContent = section.replace(/<[^>]+>/g, "").trim();
3880
+ const totalArea = 1920 * 1080;
3881
+ const estimatedTextArea = textContent.length * 100;
3882
+ const whitespaceRatio = 1 - Math.min(estimatedTextArea / totalArea, 1);
3883
+ if (whitespaceRatio < minWhitespace) {
3884
+ issues.push({
3885
+ slideIndex: i,
3886
+ dimension: "layout",
3887
+ severity: "error",
3888
+ message: `Slide ${i + 1}: Insufficient whitespace (${Math.round(whitespaceRatio * 100)}% < ${Math.round(minWhitespace * 100)}%)`,
3889
+ currentValue: whitespaceRatio,
3890
+ expectedValue: minWhitespace,
3891
+ autoFixable: true,
3892
+ fixSuggestion: "Reduce content or increase margins"
3893
+ });
3894
+ totalScore += 50;
3895
+ } else if (whitespaceRatio > maxWhitespace) {
3896
+ issues.push({
3897
+ slideIndex: i,
3898
+ dimension: "layout",
3899
+ severity: "warning",
3900
+ message: `Slide ${i + 1}: Too much whitespace (${Math.round(whitespaceRatio * 100)}% > ${Math.round(maxWhitespace * 100)}%)`,
3901
+ currentValue: whitespaceRatio,
3902
+ expectedValue: maxWhitespace,
3903
+ autoFixable: false,
3904
+ fixSuggestion: "Add more content or reduce margins"
3905
+ });
3906
+ totalScore += 80;
3907
+ } else {
3908
+ totalScore += 100;
3909
+ }
3910
+ checks++;
3911
+ }
3912
+ for (let i = 0; i < slides.length; i++) {
3913
+ const slide = slides[i];
3914
+ if (!slide) continue;
3915
+ if (!["thank-you", "section-divider"].includes(slide.type)) {
3916
+ if (!slide.data.title || slide.data.title.trim().length === 0) {
3917
+ issues.push({
3918
+ slideIndex: i,
3919
+ dimension: "layout",
3920
+ severity: "warning",
3921
+ message: `Slide ${i + 1}: Missing title`,
3922
+ autoFixable: false,
3923
+ fixSuggestion: "Add a clear slide title"
3924
+ });
3925
+ totalScore += 70;
3926
+ } else {
3927
+ totalScore += 100;
3928
+ }
3929
+ checks++;
3930
+ }
3931
+ }
3932
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
3933
+ return {
3934
+ name: "Layout",
3935
+ score,
3936
+ weight: DIMENSION_WEIGHTS.layout,
3937
+ issues,
3938
+ details: {
3939
+ slidesAnalyzed: slides.length,
3940
+ whitespaceTarget: `${Math.round(minWhitespace * 100)}-${Math.round(maxWhitespace * 100)}%`
3941
+ }
3942
+ };
3943
+ }
3944
+ /**
3945
+ * Score contrast dimension (WCAG compliance, readability)
3946
+ */
3947
+ async scoreContrast(html) {
3948
+ const issues = [];
3949
+ let score = 100;
3950
+ const minContrastRatio = 4.5;
3951
+ const colorMatches = html.match(/color:\s*([^;]+);/gi) || [];
3952
+ const bgColorMatches = html.match(/background(-color)?:\s*([^;]+);/gi) || [];
3953
+ const hasGoodContrast = html.includes("color: #fff") || html.includes("color: white") || html.includes("color: #18181B") || html.includes("color: #F5F5F4");
3954
+ const hasDarkBackground = html.includes("background-color: #18181B") || html.includes("background: #18181B") || html.includes("background-color: rgb(24, 24, 27)");
3955
+ if (html.includes("color: gray") || html.includes("color: #999") || html.includes("color: #888")) {
3956
+ issues.push({
3957
+ slideIndex: -1,
3958
+ dimension: "contrast",
3959
+ severity: "error",
3960
+ message: "Low contrast text color detected (gray text)",
3961
+ currentValue: "gray",
3962
+ expectedValue: "High contrast color",
3963
+ autoFixable: true,
3964
+ fixSuggestion: "Use white (#fff) or dark (#18181B) text depending on background"
3965
+ });
3966
+ score -= 20;
3967
+ }
3968
+ if (!hasGoodContrast && !hasDarkBackground) {
3969
+ issues.push({
3970
+ slideIndex: -1,
3971
+ dimension: "contrast",
3972
+ severity: "warning",
3973
+ message: "Could not verify WCAG-compliant contrast ratios",
3974
+ autoFixable: false,
3975
+ fixSuggestion: "Ensure text has 4.5:1 contrast ratio against background"
3976
+ });
3977
+ score -= 10;
3978
+ }
3979
+ const hasSmallFont = html.match(/font-size:\s*(1[0-3]|[0-9])px/i) !== null;
3980
+ if (hasSmallFont) {
3981
+ issues.push({
3982
+ slideIndex: -1,
3983
+ dimension: "contrast",
3984
+ severity: "error",
3985
+ message: "Font size too small for presentation (< 14px)",
3986
+ currentValue: "Small font",
3987
+ expectedValue: "18px minimum for body text",
3988
+ autoFixable: true,
3989
+ fixSuggestion: "Increase font size to minimum 18px"
3990
+ });
3991
+ score -= 15;
3992
+ }
3993
+ return {
3994
+ name: "Contrast",
3995
+ score: Math.max(0, score),
3996
+ weight: DIMENSION_WEIGHTS.contrast,
3997
+ issues,
3998
+ details: {
3999
+ wcagLevel: "AA",
4000
+ minContrastRatio,
4001
+ colorDefinitions: colorMatches.length,
4002
+ backgroundDefinitions: bgColorMatches.length
4003
+ }
4004
+ };
4005
+ }
4006
+ /**
4007
+ * Score graphics dimension (images, placement, relevance)
4008
+ */
4009
+ async scoreGraphics(slides) {
4010
+ const issues = [];
4011
+ let score = 100;
4012
+ let slidesWithImages = 0;
4013
+ let totalImageSlides = 0;
4014
+ for (let i = 0; i < slides.length; i++) {
4015
+ const slide = slides[i];
4016
+ if (!slide) continue;
4017
+ const shouldHaveImage = ["hero", "image", "feature"].includes(slide.type);
4018
+ if (shouldHaveImage) {
4019
+ totalImageSlides++;
4020
+ if (slide.data.image || slide.data.backgroundImage) {
4021
+ slidesWithImages++;
4022
+ } else {
4023
+ issues.push({
4024
+ slideIndex: i,
4025
+ dimension: "graphics",
4026
+ severity: "warning",
4027
+ message: `Slide ${i + 1} (${slide.type}): Missing expected image`,
4028
+ autoFixable: false,
4029
+ fixSuggestion: "Add a relevant image or change slide type"
4030
+ });
4031
+ score -= 5;
4032
+ }
4033
+ }
4034
+ }
4035
+ return {
4036
+ name: "Graphics",
4037
+ score: Math.max(0, score),
4038
+ weight: DIMENSION_WEIGHTS.graphics,
4039
+ issues,
4040
+ details: {
4041
+ slidesWithImages,
4042
+ expectedImageSlides: totalImageSlides,
4043
+ imageRatio: totalImageSlides > 0 ? slidesWithImages / totalImageSlides : 1
4044
+ }
4045
+ };
4046
+ }
4047
+ /**
4048
+ * Score content dimension (word limits, bullet counts, structure)
4049
+ * This is critical - must use KB rules exactly.
4050
+ */
4051
+ async scoreContent(slides) {
4052
+ const issues = [];
4053
+ let totalScore = 0;
4054
+ let checks = 0;
4055
+ const wordLimits = this.kb.getWordLimits(this.mode);
4056
+ const bulletLimits = this.kb.getBulletLimits(this.mode);
4057
+ const defaultMaxWords = this.mode === "keynote" ? 25 : 80;
4058
+ const defaultMinWords = this.mode === "keynote" ? 3 : 15;
4059
+ const defaultMaxBullets = 5;
4060
+ for (let i = 0; i < slides.length; i++) {
4061
+ const slide = slides[i];
4062
+ if (!slide) continue;
4063
+ const wordCount = this.countWords(slide);
4064
+ const slideType = slide.type;
4065
+ const maxWords = wordLimits[slideType] ?? defaultMaxWords;
4066
+ const minWords = defaultMinWords;
4067
+ if (wordCount > maxWords) {
4068
+ const severity = wordCount > maxWords * 1.5 ? "error" : "warning";
4069
+ issues.push({
4070
+ slideIndex: i,
4071
+ dimension: "content",
4072
+ severity,
4073
+ message: `Slide ${i + 1}: Word count ${wordCount} exceeds limit of ${maxWords} for ${this.mode} mode`,
4074
+ currentValue: wordCount,
4075
+ expectedValue: maxWords,
4076
+ autoFixable: true,
4077
+ fixSuggestion: "Condense text to key points only"
4078
+ });
4079
+ totalScore += severity === "error" ? 40 : 70;
4080
+ } else if (wordCount < minWords && !["title", "section-divider", "thank-you"].includes(slide.type)) {
4081
+ issues.push({
4082
+ slideIndex: i,
4083
+ dimension: "content",
4084
+ severity: "warning",
4085
+ message: `Slide ${i + 1}: Word count ${wordCount} may be too sparse (min: ${minWords})`,
4086
+ currentValue: wordCount,
4087
+ expectedValue: minWords,
4088
+ autoFixable: false,
4089
+ fixSuggestion: "Add supporting content"
4090
+ });
4091
+ totalScore += 80;
4092
+ } else {
4093
+ totalScore += 100;
4094
+ }
4095
+ checks++;
4096
+ if (slide.data.bullets && Array.isArray(slide.data.bullets)) {
4097
+ const bulletCount = slide.data.bullets.length;
4098
+ const maxBullets = bulletLimits[slideType] ?? defaultMaxBullets;
4099
+ if (bulletCount > maxBullets) {
4100
+ issues.push({
4101
+ slideIndex: i,
4102
+ dimension: "content",
4103
+ severity: "error",
4104
+ message: `Slide ${i + 1}: ${bulletCount} bullets exceeds limit of ${maxBullets}`,
4105
+ currentValue: bulletCount,
4106
+ expectedValue: maxBullets,
4107
+ autoFixable: true,
4108
+ fixSuggestion: "Reduce to top 3-5 key points"
4109
+ });
4110
+ totalScore += 50;
4111
+ } else {
4112
+ totalScore += 100;
4113
+ }
4114
+ checks++;
4115
+ }
4116
+ }
4117
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4118
+ return {
4119
+ name: "Content",
4120
+ score,
4121
+ weight: DIMENSION_WEIGHTS.content,
4122
+ issues,
4123
+ details: {
4124
+ mode: this.mode,
4125
+ wordLimits,
4126
+ bulletLimits,
4127
+ totalSlides: slides.length
4128
+ }
4129
+ };
4130
+ }
4131
+ /**
4132
+ * Score clarity dimension (message focus, information density)
4133
+ */
4134
+ async scoreClarity(slides) {
4135
+ const issues = [];
4136
+ let totalScore = 0;
4137
+ let checks = 0;
4138
+ for (let i = 0; i < slides.length; i++) {
4139
+ const slide = slides[i];
4140
+ if (!slide) continue;
4141
+ if (slide.data.keyMessage) {
4142
+ const keyMessageWords = slide.data.keyMessage.split(/\s+/).length;
4143
+ const maxKeyMessageWords = this.mode === "keynote" ? 15 : 25;
4144
+ if (keyMessageWords > maxKeyMessageWords) {
4145
+ issues.push({
4146
+ slideIndex: i,
4147
+ dimension: "clarity",
4148
+ severity: "warning",
4149
+ message: `Slide ${i + 1}: Key message too long (${keyMessageWords} words > ${maxKeyMessageWords})`,
4150
+ currentValue: keyMessageWords,
4151
+ expectedValue: maxKeyMessageWords,
4152
+ autoFixable: true,
4153
+ fixSuggestion: "Shorten key message to one impactful sentence"
4154
+ });
4155
+ totalScore += 70;
4156
+ } else {
4157
+ totalScore += 100;
4158
+ }
4159
+ checks++;
4160
+ }
4161
+ if (slide.data.title) {
4162
+ const title = slide.data.title;
4163
+ const titleWords = title.split(/\s+/).length;
4164
+ if (titleWords > 10) {
4165
+ issues.push({
4166
+ slideIndex: i,
4167
+ dimension: "clarity",
4168
+ severity: "warning",
4169
+ message: `Slide ${i + 1}: Title too long (${titleWords} words)`,
4170
+ currentValue: titleWords,
4171
+ expectedValue: "2-8 words",
4172
+ autoFixable: true,
4173
+ fixSuggestion: "Use action-oriented, concise title"
4174
+ });
4175
+ totalScore += 75;
4176
+ } else {
4177
+ totalScore += 100;
4178
+ }
4179
+ checks++;
4180
+ }
4181
+ 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);
4182
+ const maxElements = this.mode === "keynote" ? 4 : 6;
4183
+ if (elementCount > maxElements) {
4184
+ issues.push({
4185
+ slideIndex: i,
4186
+ dimension: "clarity",
4187
+ severity: "warning",
4188
+ message: `Slide ${i + 1}: Too many elements (${elementCount} > ${maxElements})`,
4189
+ currentValue: elementCount,
4190
+ expectedValue: maxElements,
4191
+ autoFixable: true,
4192
+ fixSuggestion: "Split into multiple slides for clarity"
4193
+ });
4194
+ totalScore += 70;
4195
+ } else {
4196
+ totalScore += 100;
4197
+ }
4198
+ checks++;
4199
+ }
4200
+ const score = checks > 0 ? Math.round(totalScore / checks) : 100;
4201
+ return {
4202
+ name: "Clarity",
4203
+ score,
4204
+ weight: DIMENSION_WEIGHTS.clarity,
4205
+ issues,
4206
+ details: {
4207
+ slidesAnalyzed: slides.length,
4208
+ mode: this.mode
4209
+ }
4210
+ };
4211
+ }
4212
+ /**
4213
+ * Score effectiveness dimension (expert methodology compliance)
4214
+ */
4215
+ async scoreEffectiveness(slides) {
4216
+ const issues = [];
4217
+ let score = 100;
4218
+ if (slides.length >= 3) {
4219
+ const firstSlide = slides[0];
4220
+ const lastSlide = slides[slides.length - 1];
4221
+ if (firstSlide && !["title", "hero"].includes(firstSlide.type)) {
4222
+ issues.push({
4223
+ slideIndex: 0,
4224
+ dimension: "effectiveness",
4225
+ severity: "warning",
4226
+ message: "Presentation should start with a title or hero slide",
4227
+ currentValue: firstSlide.type,
4228
+ expectedValue: "title or hero",
4229
+ autoFixable: false,
4230
+ fixSuggestion: "Add a compelling opening slide"
4231
+ });
4232
+ score -= 10;
4233
+ }
4234
+ if (lastSlide && !["thank-you", "cta", "closing"].includes(lastSlide.type)) {
4235
+ issues.push({
4236
+ slideIndex: slides.length - 1,
4237
+ dimension: "effectiveness",
4238
+ severity: "warning",
4239
+ message: "Presentation should end with a closing or CTA slide",
4240
+ currentValue: lastSlide.type,
4241
+ expectedValue: "thank-you, cta, or closing",
4242
+ autoFixable: false,
4243
+ fixSuggestion: "Add a clear call-to-action or closing"
4244
+ });
4245
+ score -= 10;
4246
+ }
4247
+ }
4248
+ const keyMessages = slides.filter((s) => s.data.keyMessage);
4249
+ if (keyMessages.length > 0 && keyMessages.length !== 3 && keyMessages.length > 4) {
4250
+ issues.push({
4251
+ slideIndex: -1,
4252
+ dimension: "effectiveness",
4253
+ severity: "info",
4254
+ message: `Consider using Rule of Three: ${keyMessages.length} key messages found`,
4255
+ currentValue: keyMessages.length,
4256
+ expectedValue: 3,
4257
+ autoFixable: false,
4258
+ fixSuggestion: "Group messages into 3 main themes"
4259
+ });
4260
+ score -= 5;
4261
+ }
4262
+ const hasScqaElements = slides.some(
4263
+ (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")
4264
+ );
4265
+ if (!hasScqaElements && this.presentationType === "consulting_deck") {
4266
+ issues.push({
4267
+ slideIndex: -1,
4268
+ dimension: "effectiveness",
4269
+ severity: "warning",
4270
+ message: "Consulting deck should follow SCQA structure (Situation, Complication, Question, Answer)",
4271
+ autoFixable: false,
4272
+ fixSuggestion: "Organize content using Barbara Minto Pyramid Principle"
4273
+ });
4274
+ score -= 10;
4275
+ }
4276
+ const firstSlideType = slides[0]?.type;
4277
+ const lastSlideType = slides[slides.length - 1]?.type;
4278
+ return {
4279
+ name: "Effectiveness",
4280
+ score: Math.max(0, score),
4281
+ weight: DIMENSION_WEIGHTS.effectiveness,
4282
+ issues,
4283
+ details: {
4284
+ presentationType: this.presentationType,
4285
+ slideCount: slides.length,
4286
+ hasOpeningSlide: firstSlideType ? ["title", "hero"].includes(firstSlideType) : false,
4287
+ hasClosingSlide: lastSlideType ? ["thank-you", "cta", "closing"].includes(lastSlideType) : false
4288
+ }
4289
+ };
4290
+ }
4291
+ /**
4292
+ * Score consistency dimension (style uniformity, design coherence)
4293
+ */
4294
+ async scoreConsistency(slides, html) {
4295
+ const issues = [];
4296
+ let score = 100;
4297
+ const hasCssVariables = html.includes("var(--") || html.includes(":root");
4298
+ if (!hasCssVariables) {
4299
+ issues.push({
4300
+ slideIndex: -1,
4301
+ dimension: "consistency",
4302
+ severity: "warning",
4303
+ message: "Presentation lacks CSS variables for consistent styling",
4304
+ autoFixable: true,
4305
+ fixSuggestion: "Use CSS variables for colors, fonts, and spacing"
4306
+ });
4307
+ score -= 10;
4308
+ }
4309
+ const titlePatterns = /* @__PURE__ */ new Set();
4310
+ for (const slide of slides) {
4311
+ if (slide.data.title) {
4312
+ const isUpperCase = slide.data.title === slide.data.title.toUpperCase();
4313
+ const words = slide.data.title.split(" ").filter((w) => w.length > 0);
4314
+ const isTitleCase = words.length > 0 && words.every(
4315
+ (w) => w.length > 0 && w[0] === w[0]?.toUpperCase()
4316
+ );
4317
+ titlePatterns.add(isUpperCase ? "UPPER" : isTitleCase ? "Title" : "sentence");
4318
+ }
4319
+ }
4320
+ if (titlePatterns.size > 1) {
4321
+ issues.push({
4322
+ slideIndex: -1,
4323
+ dimension: "consistency",
4324
+ severity: "warning",
4325
+ message: `Inconsistent title casing: ${Array.from(titlePatterns).join(", ")}`,
4326
+ autoFixable: true,
4327
+ fixSuggestion: "Use consistent title case throughout"
4328
+ });
4329
+ score -= 10;
4330
+ }
4331
+ const fontMatches = html.match(/font-family:\s*([^;]+);/gi) || [];
4332
+ const uniqueFonts = new Set(fontMatches.map((f) => f.toLowerCase()));
4333
+ if (uniqueFonts.size > 3) {
4334
+ issues.push({
4335
+ slideIndex: -1,
4336
+ dimension: "consistency",
4337
+ severity: "warning",
4338
+ message: `Too many font families (${uniqueFonts.size} > 3)`,
4339
+ autoFixable: true,
4340
+ fixSuggestion: "Use 2-3 complementary fonts max"
4341
+ });
4342
+ score -= 10;
4343
+ }
4344
+ return {
4345
+ name: "Consistency",
4346
+ score: Math.max(0, score),
4347
+ weight: DIMENSION_WEIGHTS.consistency,
4348
+ issues,
4349
+ details: {
4350
+ hasCssVariables,
4351
+ titlePatterns: Array.from(titlePatterns),
4352
+ fontFamilyCount: uniqueFonts.size
4353
+ }
4354
+ };
4355
+ }
4356
+ /**
4357
+ * Count words in a slide.
4358
+ */
4359
+ countWords(slide) {
4360
+ let text = "";
4361
+ if (slide.data.title) text += slide.data.title + " ";
4362
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4363
+ if (slide.data.body) text += slide.data.body + " ";
4364
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4365
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4366
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4367
+ }
4368
+ /**
4369
+ * Get a formatted report of the scoring results.
4370
+ */
4371
+ formatReport(result) {
4372
+ const lines = [];
4373
+ 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");
4374
+ lines.push("\u2551 7-DIMENSION QUALITY ASSESSMENT \u2551");
4375
+ 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");
4376
+ lines.push("");
4377
+ lines.push(`Overall Score: ${result.overallScore}/100 ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
4378
+ lines.push(`Threshold: ${result.threshold}/100`);
4379
+ lines.push("");
4380
+ lines.push("Dimension Breakdown:");
4381
+ 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");
4382
+ const dims = result.dimensions;
4383
+ const formatDim = (name, d) => {
4384
+ const bar = "\u2588".repeat(Math.floor(d.score / 10)) + "\u2591".repeat(10 - Math.floor(d.score / 10));
4385
+ const status = d.score >= 95 ? "\u2705" : d.score >= 80 ? "\u26A0\uFE0F" : "\u274C";
4386
+ return `${status} ${name.padEnd(14)} ${bar} ${d.score.toString().padStart(3)}/100 (${(d.weight * 100).toFixed(0)}%)`;
4387
+ };
4388
+ lines.push(formatDim("Layout", dims.layout));
4389
+ lines.push(formatDim("Contrast", dims.contrast));
4390
+ lines.push(formatDim("Graphics", dims.graphics));
4391
+ lines.push(formatDim("Content", dims.content));
4392
+ lines.push(formatDim("Clarity", dims.clarity));
4393
+ lines.push(formatDim("Effectiveness", dims.effectiveness));
4394
+ lines.push(formatDim("Consistency", dims.consistency));
4395
+ lines.push("");
4396
+ const errors = result.issues.filter((i) => i.severity === "error");
4397
+ const warnings = result.issues.filter((i) => i.severity === "warning");
4398
+ if (errors.length > 0) {
4399
+ lines.push("\u274C Errors:");
4400
+ errors.forEach((e) => lines.push(` \u2022 ${e.message}`));
4401
+ lines.push("");
4402
+ }
4403
+ if (warnings.length > 0) {
4404
+ lines.push("\u26A0\uFE0F Warnings:");
4405
+ warnings.slice(0, 10).forEach((w) => lines.push(` \u2022 ${w.message}`));
4406
+ if (warnings.length > 10) {
4407
+ lines.push(` ... and ${warnings.length - 10} more warnings`);
4408
+ }
4409
+ lines.push("");
4410
+ }
4411
+ const autoFixable = result.issues.filter((i) => i.autoFixable);
4412
+ if (autoFixable.length > 0) {
4413
+ lines.push(`\u{1F527} ${autoFixable.length} issues can be auto-fixed`);
4414
+ }
4415
+ return lines.join("\n");
4416
+ }
4417
+ };
4418
+
4419
+ // src/qa/AutoFixEngine.ts
4420
+ var AutoFixEngine = class {
4421
+ kb;
4422
+ mode;
4423
+ presentationType;
4424
+ constructor(mode, presentationType) {
4425
+ this.mode = mode;
4426
+ this.presentationType = presentationType;
4427
+ }
4428
+ /**
4429
+ * Apply automatic fixes to slides based on scoring results.
4430
+ */
4431
+ async fix(slides, scoringResult) {
4432
+ this.kb = await getKnowledgeGateway();
4433
+ const slidesFixed = JSON.parse(JSON.stringify(slides));
4434
+ const fixesApplied = [];
4435
+ const fixesSkipped = [];
4436
+ const autoFixableIssues = scoringResult.issues.filter((i) => i.autoFixable);
4437
+ for (const issue of autoFixableIssues) {
4438
+ const result = await this.applyFix(slidesFixed, issue);
4439
+ if (result.applied) {
4440
+ fixesApplied.push(result);
4441
+ } else {
4442
+ fixesSkipped.push(result);
4443
+ }
4444
+ }
4445
+ const summary = this.generateSummary(fixesApplied, fixesSkipped);
4446
+ return {
4447
+ slidesFixed,
4448
+ fixesApplied,
4449
+ fixesSkipped,
4450
+ summary
4451
+ };
4452
+ }
4453
+ /**
4454
+ * Apply a single fix based on the issue.
4455
+ */
4456
+ async applyFix(slides, issue) {
4457
+ const result = {
4458
+ slideIndex: issue.slideIndex,
4459
+ dimension: issue.dimension,
4460
+ originalValue: issue.currentValue,
4461
+ newValue: null,
4462
+ description: issue.message,
4463
+ applied: false
4464
+ };
4465
+ switch (issue.dimension) {
4466
+ case "content":
4467
+ return this.fixContentIssue(slides, issue, result);
4468
+ case "clarity":
4469
+ return this.fixClarityIssue(slides, issue, result);
4470
+ case "layout":
4471
+ return this.fixLayoutIssue(slides, issue, result);
4472
+ case "consistency":
4473
+ return this.fixConsistencyIssue(slides, issue, result);
4474
+ case "contrast":
4475
+ return this.fixContrastIssue(slides, issue, result);
4476
+ default:
4477
+ result.description = `No auto-fix available for ${issue.dimension}`;
4478
+ return result;
4479
+ }
4480
+ }
4481
+ /**
4482
+ * Fix content-related issues (word count, bullet count).
4483
+ */
4484
+ fixContentIssue(slides, issue, result) {
4485
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4486
+ return result;
4487
+ }
4488
+ const slide = slides[issue.slideIndex];
4489
+ if (!slide) return result;
4490
+ if (issue.message.includes("Word count") && issue.message.includes("exceeds")) {
4491
+ const maxWords = issue.expectedValue;
4492
+ result.originalValue = this.countWords(slide);
4493
+ if (slide.data.body) {
4494
+ slide.data.body = this.condenseText(slide.data.body, maxWords / 2);
4495
+ }
4496
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4497
+ const bulletCount = slide.data.bullets.length;
4498
+ slide.data.bullets = slide.data.bullets.map(
4499
+ (bullet) => this.condenseText(bullet, Math.floor(maxWords / bulletCount))
4500
+ );
4501
+ }
4502
+ if (slide.data.subtitle) {
4503
+ slide.data.subtitle = this.condenseText(slide.data.subtitle, 10);
4504
+ }
4505
+ result.newValue = this.countWords(slide);
4506
+ result.applied = result.newValue <= maxWords;
4507
+ result.description = `Condensed content from ${result.originalValue} to ${result.newValue} words`;
4508
+ }
4509
+ if (issue.message.includes("bullets exceeds")) {
4510
+ const maxBullets = issue.expectedValue;
4511
+ if (slide.data.bullets) {
4512
+ result.originalValue = slide.data.bullets.length;
4513
+ slide.data.bullets = slide.data.bullets.slice(0, maxBullets);
4514
+ result.newValue = slide.data.bullets.length;
4515
+ result.applied = true;
4516
+ result.description = `Reduced bullets from ${result.originalValue} to ${result.newValue}`;
4517
+ }
4518
+ }
4519
+ return result;
4520
+ }
4521
+ /**
4522
+ * Fix clarity-related issues (key message length, title length).
4523
+ */
4524
+ fixClarityIssue(slides, issue, result) {
4525
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4526
+ return result;
4527
+ }
4528
+ const slide = slides[issue.slideIndex];
4529
+ if (!slide) return result;
4530
+ if (issue.message.includes("Key message too long")) {
4531
+ if (slide.data.keyMessage) {
4532
+ result.originalValue = slide.data.keyMessage;
4533
+ const maxWords = issue.expectedValue;
4534
+ slide.data.keyMessage = this.condenseText(slide.data.keyMessage, maxWords);
4535
+ result.newValue = slide.data.keyMessage;
4536
+ result.applied = true;
4537
+ result.description = `Shortened key message to ${maxWords} words`;
4538
+ }
4539
+ }
4540
+ if (issue.message.includes("Title too long")) {
4541
+ if (slide.data.title) {
4542
+ result.originalValue = slide.data.title;
4543
+ const words = slide.data.title.split(/\s+/);
4544
+ const originalLength = words.length;
4545
+ if (words.length > 6) {
4546
+ slide.data.title = words.slice(0, 6).join(" ");
4547
+ }
4548
+ result.newValue = slide.data.title;
4549
+ result.applied = true;
4550
+ result.description = `Shortened title from ${originalLength} to ${slide.data.title.split(/\s+/).length} words`;
4551
+ }
4552
+ }
4553
+ if (issue.message.includes("Too many elements")) {
4554
+ result.originalValue = issue.currentValue;
4555
+ if (slide.data.subtitle && slide.data.body) {
4556
+ delete slide.data.subtitle;
4557
+ result.applied = true;
4558
+ result.description = "Removed subtitle to reduce element count";
4559
+ } else if (slide.data.body && slide.data.bullets && slide.data.bullets.length > 0) {
4560
+ delete slide.data.body;
4561
+ result.applied = true;
4562
+ result.description = "Removed body text, keeping bullets";
4563
+ }
4564
+ result.newValue = this.countElements(slide);
4565
+ }
4566
+ return result;
2821
4567
  }
2822
- calculateA11yScore(results) {
2823
- let score = 100;
2824
- score -= Math.min(40, results.fontSizeIssues.length * 10);
2825
- score -= Math.min(40, results.contrastIssues.length * 10);
2826
- if (results.focusCoverage < 1) score -= 20;
2827
- return Math.max(0, score);
4568
+ /**
4569
+ * Fix layout-related issues.
4570
+ */
4571
+ fixLayoutIssue(slides, issue, result) {
4572
+ if (issue.slideIndex < 0 || issue.slideIndex >= slides.length) {
4573
+ return result;
4574
+ }
4575
+ const slide = slides[issue.slideIndex];
4576
+ if (!slide) return result;
4577
+ if (issue.message.includes("Insufficient whitespace")) {
4578
+ const currentWordCount = this.countWords(slide);
4579
+ const targetReduction = 0.3;
4580
+ const targetWords = Math.floor(currentWordCount * (1 - targetReduction));
4581
+ result.originalValue = currentWordCount;
4582
+ if (slide.data.body) {
4583
+ slide.data.body = this.condenseText(slide.data.body, Math.floor(targetWords * 0.5));
4584
+ }
4585
+ if (slide.data.bullets && slide.data.bullets.length > 0) {
4586
+ const wordsPerBullet = Math.floor(targetWords / (slide.data.bullets.length * 2));
4587
+ slide.data.bullets = slide.data.bullets.map((b) => this.condenseText(b, wordsPerBullet));
4588
+ }
4589
+ result.newValue = this.countWords(slide);
4590
+ result.applied = true;
4591
+ result.description = `Reduced content from ${result.originalValue} to ${result.newValue} words for better whitespace`;
4592
+ }
4593
+ return result;
2828
4594
  }
2829
- // ===========================================================================
2830
- // ISSUE COLLECTION
2831
- // ===========================================================================
2832
- collectIssues(visual, content, expert, accessibility) {
2833
- const issues = [];
2834
- visual.perSlide.forEach((slide) => {
2835
- slide.issues.forEach((issue) => {
2836
- issues.push({
2837
- severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
2838
- category: "visual",
2839
- slideIndex: slide.slideIndex,
2840
- message: issue
2841
- });
2842
- });
2843
- });
2844
- content.perSlide.forEach((slide) => {
2845
- slide.issues.forEach((issue) => {
2846
- issues.push({
2847
- severity: "warning",
2848
- category: "content",
2849
- slideIndex: slide.slideIndex,
2850
- message: issue
2851
- });
2852
- });
2853
- });
2854
- content.glanceTest.filter((g) => !g.passed).forEach((g) => {
2855
- const issue = {
2856
- severity: "warning",
2857
- category: "content",
2858
- slideIndex: g.slideIndex,
2859
- message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
2860
- };
2861
- if (g.recommendation) {
2862
- issue.suggestion = g.recommendation;
4595
+ /**
4596
+ * Fix consistency-related issues.
4597
+ */
4598
+ fixConsistencyIssue(slides, issue, result) {
4599
+ if (issue.message.includes("Inconsistent title casing")) {
4600
+ let fixedCount = 0;
4601
+ for (const slide of slides) {
4602
+ if (slide.data.title) {
4603
+ const original = slide.data.title;
4604
+ slide.data.title = this.toTitleCase(slide.data.title);
4605
+ if (slide.data.title !== original) fixedCount++;
4606
+ }
2863
4607
  }
2864
- issues.push(issue);
2865
- });
2866
- [expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
2867
- e.violations.forEach((v) => {
2868
- issues.push({
2869
- severity: "warning",
2870
- category: "expert",
2871
- message: `${e.expertName}: ${v}`
2872
- });
2873
- });
2874
- });
2875
- accessibility.fontSizeIssues.forEach((issue) => {
2876
- issues.push({
2877
- severity: "error",
2878
- category: "accessibility",
2879
- slideIndex: issue.slideIndex,
2880
- message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
2881
- suggestion: `Increase font size to at least ${issue.minimumSize}px`
2882
- });
2883
- });
2884
- accessibility.contrastIssues.forEach((issue) => {
2885
- issues.push({
2886
- severity: "error",
2887
- category: "accessibility",
2888
- slideIndex: issue.slideIndex,
2889
- message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
2890
- suggestion: "Increase contrast between text and background"
2891
- });
2892
- });
2893
- return issues;
4608
+ result.originalValue = "Mixed casing";
4609
+ result.newValue = "Title Case";
4610
+ result.applied = fixedCount > 0;
4611
+ result.description = `Applied Title Case to ${fixedCount} slide titles`;
4612
+ }
4613
+ return result;
2894
4614
  }
2895
- // ===========================================================================
2896
- // BROWSER MANAGEMENT
2897
- // ===========================================================================
2898
- async initBrowser() {
2899
- if (!this.browser) {
2900
- this.browser = await import_playwright.chromium.launch({ headless: true });
4615
+ /**
4616
+ * Fix contrast-related issues.
4617
+ * Note: These are CSS fixes, typically handled at generation time.
4618
+ */
4619
+ fixContrastIssue(slides, issue, result) {
4620
+ result.description = "Contrast issues flagged for CSS regeneration";
4621
+ result.applied = false;
4622
+ return result;
4623
+ }
4624
+ /**
4625
+ * Condense text to approximately maxWords.
4626
+ * Uses smart truncation that preserves meaning.
4627
+ */
4628
+ condenseText(text, maxWords) {
4629
+ const words = text.split(/\s+/);
4630
+ if (words.length <= maxWords) {
4631
+ return text;
4632
+ }
4633
+ const fillerWords = /* @__PURE__ */ new Set([
4634
+ "very",
4635
+ "really",
4636
+ "actually",
4637
+ "basically",
4638
+ "literally",
4639
+ "obviously",
4640
+ "clearly",
4641
+ "simply",
4642
+ "just",
4643
+ "that",
4644
+ "which",
4645
+ "would",
4646
+ "could",
4647
+ "should",
4648
+ "might"
4649
+ ]);
4650
+ let filtered = words.filter((w) => !fillerWords.has(w.toLowerCase()));
4651
+ if (filtered.length <= maxWords) {
4652
+ return filtered.join(" ");
4653
+ }
4654
+ const punctuation = [".", ",", ";", ":", "-"];
4655
+ let breakPoint = maxWords;
4656
+ for (let i = maxWords - 1; i >= maxWords - 5 && i >= 0; i--) {
4657
+ const word = filtered[i];
4658
+ if (word && punctuation.some((p) => word.endsWith(p))) {
4659
+ breakPoint = i + 1;
4660
+ break;
4661
+ }
2901
4662
  }
4663
+ filtered = filtered.slice(0, breakPoint);
4664
+ let result = filtered.join(" ");
4665
+ if (!result.endsWith(".") && !result.endsWith("!") && !result.endsWith("?")) {
4666
+ result = result.replace(/[,;:]$/, "") + "...";
4667
+ }
4668
+ return result;
2902
4669
  }
2903
- async closeBrowser() {
2904
- if (this.browser) {
2905
- await this.browser.close();
2906
- this.browser = null;
4670
+ /**
4671
+ * Convert text to Title Case.
4672
+ */
4673
+ toTitleCase(text) {
4674
+ const minorWords = /* @__PURE__ */ new Set([
4675
+ "a",
4676
+ "an",
4677
+ "the",
4678
+ "and",
4679
+ "but",
4680
+ "or",
4681
+ "for",
4682
+ "nor",
4683
+ "on",
4684
+ "at",
4685
+ "to",
4686
+ "by",
4687
+ "of",
4688
+ "in",
4689
+ "with"
4690
+ ]);
4691
+ return text.split(" ").map((word, index) => {
4692
+ if (index === 0) {
4693
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4694
+ }
4695
+ if (minorWords.has(word.toLowerCase())) {
4696
+ return word.toLowerCase();
4697
+ }
4698
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
4699
+ }).join(" ");
4700
+ }
4701
+ /**
4702
+ * Count words in a slide.
4703
+ */
4704
+ countWords(slide) {
4705
+ let text = "";
4706
+ if (slide.data.title) text += slide.data.title + " ";
4707
+ if (slide.data.subtitle) text += slide.data.subtitle + " ";
4708
+ if (slide.data.body) text += slide.data.body + " ";
4709
+ if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
4710
+ if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
4711
+ return text.split(/\s+/).filter((w) => w.length > 0).length;
4712
+ }
4713
+ /**
4714
+ * Count elements in a slide.
4715
+ */
4716
+ countElements(slide) {
4717
+ 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);
4718
+ }
4719
+ /**
4720
+ * Generate a summary of fixes applied.
4721
+ */
4722
+ generateSummary(applied, skipped) {
4723
+ const lines = [];
4724
+ lines.push(`
4725
+ \u{1F527} Auto-Fix Summary`);
4726
+ 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`);
4727
+ lines.push(`Fixes Applied: ${applied.length}`);
4728
+ lines.push(`Fixes Skipped: ${skipped.length}`);
4729
+ lines.push("");
4730
+ if (applied.length > 0) {
4731
+ lines.push("\u2705 Applied Fixes:");
4732
+ for (const fix of applied) {
4733
+ lines.push(` \u2022 ${fix.description}`);
4734
+ }
4735
+ lines.push("");
4736
+ }
4737
+ if (skipped.length > 0) {
4738
+ lines.push("\u26A0\uFE0F Skipped Fixes (require manual attention):");
4739
+ for (const fix of skipped.slice(0, 5)) {
4740
+ lines.push(` \u2022 ${fix.description}`);
4741
+ }
4742
+ if (skipped.length > 5) {
4743
+ lines.push(` ... and ${skipped.length - 5} more`);
4744
+ }
2907
4745
  }
4746
+ return lines.join("\n");
2908
4747
  }
2909
4748
  };
4749
+ function createAutoFixEngine(mode, presentationType) {
4750
+ return new AutoFixEngine(mode, presentationType);
4751
+ }
2910
4752
 
2911
4753
  // src/generators/html/RevealJsGenerator.ts
2912
4754
  var RevealJsGenerator = class {
@@ -3502,6 +5344,182 @@ ${slides}
3502
5344
  }
3503
5345
  };
3504
5346
 
5347
+ // src/qa/IterativeQAEngine.ts
5348
+ var DEFAULT_OPTIONS = {
5349
+ minScore: 95,
5350
+ maxIterations: 5,
5351
+ verbose: true
5352
+ };
5353
+ var IterativeQAEngine = class {
5354
+ kb;
5355
+ scorer;
5356
+ fixer;
5357
+ generator;
5358
+ mode;
5359
+ presentationType;
5360
+ config;
5361
+ constructor(mode, presentationType, config) {
5362
+ this.mode = mode;
5363
+ this.presentationType = presentationType;
5364
+ this.config = config;
5365
+ this.scorer = new SevenDimensionScorer(mode, presentationType);
5366
+ this.fixer = new AutoFixEngine(mode, presentationType);
5367
+ this.generator = new RevealJsGenerator();
5368
+ }
5369
+ /**
5370
+ * Run the iterative QA process.
5371
+ */
5372
+ async run(initialSlides, initialHtml, options = {}) {
5373
+ const opts = { ...DEFAULT_OPTIONS, ...options };
5374
+ this.kb = await getKnowledgeGateway();
5375
+ const iterations = [];
5376
+ let currentSlides = initialSlides;
5377
+ let currentHtml = initialHtml;
5378
+ let scoringResult;
5379
+ let autoFixSummary = "";
5380
+ let totalFixesApplied = 0;
5381
+ if (opts.verbose) {
5382
+ console.log("\n\u{1F504} Starting Iterative QA Process");
5383
+ console.log(` Target Score: ${opts.minScore}/100`);
5384
+ console.log(` Max Iterations: ${opts.maxIterations}`);
5385
+ console.log("");
5386
+ }
5387
+ for (let i = 0; i < opts.maxIterations; i++) {
5388
+ const iterationNum = i + 1;
5389
+ if (opts.verbose) {
5390
+ console.log(`\u{1F4CA} Iteration ${iterationNum}/${opts.maxIterations}...`);
5391
+ }
5392
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5393
+ iterations.push({
5394
+ iteration: iterationNum,
5395
+ score: scoringResult.overallScore,
5396
+ dimensionScores: {
5397
+ layout: scoringResult.dimensions.layout.score,
5398
+ contrast: scoringResult.dimensions.contrast.score,
5399
+ graphics: scoringResult.dimensions.graphics.score,
5400
+ content: scoringResult.dimensions.content.score,
5401
+ clarity: scoringResult.dimensions.clarity.score,
5402
+ effectiveness: scoringResult.dimensions.effectiveness.score,
5403
+ consistency: scoringResult.dimensions.consistency.score
5404
+ },
5405
+ fixesApplied: 0,
5406
+ timestamp: /* @__PURE__ */ new Date()
5407
+ });
5408
+ if (opts.verbose) {
5409
+ console.log(` Score: ${scoringResult.overallScore}/100`);
5410
+ }
5411
+ if (scoringResult.passed) {
5412
+ if (opts.verbose) {
5413
+ console.log(` \u2705 PASSED - Score meets threshold (${opts.minScore})`);
5414
+ }
5415
+ break;
5416
+ }
5417
+ if (i < opts.maxIterations - 1) {
5418
+ if (opts.verbose) {
5419
+ console.log(` \u26A0\uFE0F Below threshold, applying auto-fixes...`);
5420
+ }
5421
+ const fixResult = await this.fixer.fix(currentSlides, scoringResult);
5422
+ if (fixResult.fixesApplied.length > 0) {
5423
+ currentSlides = fixResult.slidesFixed;
5424
+ currentHtml = await this.generator.generate(currentSlides, this.config);
5425
+ const lastIteration = iterations[iterations.length - 1];
5426
+ if (lastIteration) {
5427
+ lastIteration.fixesApplied = fixResult.fixesApplied.length;
5428
+ }
5429
+ totalFixesApplied += fixResult.fixesApplied.length;
5430
+ if (opts.verbose) {
5431
+ console.log(` \u{1F527} Applied ${fixResult.fixesApplied.length} fixes`);
5432
+ }
5433
+ autoFixSummary += fixResult.summary + "\n";
5434
+ } else {
5435
+ if (opts.verbose) {
5436
+ console.log(` \u26A0\uFE0F No auto-fixes available, manual review needed`);
5437
+ }
5438
+ break;
5439
+ }
5440
+ }
5441
+ }
5442
+ if (!scoringResult.passed) {
5443
+ scoringResult = await this.scorer.score(currentSlides, currentHtml, opts.minScore);
5444
+ }
5445
+ const report = this.generateReport(
5446
+ scoringResult,
5447
+ iterations,
5448
+ opts,
5449
+ totalFixesApplied
5450
+ );
5451
+ if (opts.verbose) {
5452
+ console.log(report);
5453
+ }
5454
+ return {
5455
+ finalScore: scoringResult.overallScore,
5456
+ passed: scoringResult.passed,
5457
+ threshold: opts.minScore,
5458
+ iterations,
5459
+ totalIterations: iterations.length,
5460
+ maxIterations: opts.maxIterations,
5461
+ slides: currentSlides,
5462
+ html: currentHtml,
5463
+ finalScoring: scoringResult,
5464
+ autoFixSummary,
5465
+ report
5466
+ };
5467
+ }
5468
+ /**
5469
+ * Generate a comprehensive report.
5470
+ */
5471
+ generateReport(finalScoring, iterations, options, totalFixesApplied) {
5472
+ const lines = [];
5473
+ lines.push("");
5474
+ 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");
5475
+ lines.push("\u2551 ITERATIVE QA FINAL REPORT \u2551");
5476
+ 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");
5477
+ lines.push("");
5478
+ const passStatus = finalScoring.passed ? "\u2705 PASSED" : "\u274C FAILED";
5479
+ lines.push(`Final Score: ${finalScoring.overallScore}/100 ${passStatus}`);
5480
+ lines.push(`Threshold: ${options.minScore}/100`);
5481
+ lines.push(`Iterations: ${iterations.length}/${options.maxIterations}`);
5482
+ lines.push(`Total Fixes Applied: ${totalFixesApplied}`);
5483
+ lines.push("");
5484
+ if (iterations.length > 1) {
5485
+ lines.push("Score Progression:");
5486
+ 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");
5487
+ for (const iter of iterations) {
5488
+ const bar = "\u2588".repeat(Math.floor(iter.score / 10)) + "\u2591".repeat(10 - Math.floor(iter.score / 10));
5489
+ lines.push(` Iter ${iter.iteration}: ${bar} ${iter.score}/100 (+${iter.fixesApplied} fixes)`);
5490
+ }
5491
+ lines.push("");
5492
+ }
5493
+ lines.push(this.scorer.formatReport(finalScoring));
5494
+ lines.push("");
5495
+ lines.push("\u{1F4DA} Knowledge Base Compliance:");
5496
+ 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");
5497
+ lines.push(` Mode: ${this.mode}`);
5498
+ lines.push(` Presentation Type: ${this.presentationType}`);
5499
+ lines.push(` Word Limits: ${this.mode === "keynote" ? "6-25" : "40-80"} per slide`);
5500
+ lines.push(` Expert Frameworks: Duarte, Reynolds, Gallo, Anderson`);
5501
+ lines.push("");
5502
+ if (!finalScoring.passed) {
5503
+ lines.push("\u{1F4CB} Recommendations for Manual Review:");
5504
+ 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");
5505
+ const manualIssues = finalScoring.issues.filter((i) => !i.autoFixable);
5506
+ for (const issue of manualIssues.slice(0, 5)) {
5507
+ lines.push(` \u2022 ${issue.message}`);
5508
+ if (issue.fixSuggestion) {
5509
+ lines.push(` \u2192 ${issue.fixSuggestion}`);
5510
+ }
5511
+ }
5512
+ if (manualIssues.length > 5) {
5513
+ lines.push(` ... and ${manualIssues.length - 5} more issues`);
5514
+ }
5515
+ }
5516
+ return lines.join("\n");
5517
+ }
5518
+ };
5519
+ function createIterativeQAEngine(mode, presentationType, config) {
5520
+ return new IterativeQAEngine(mode, presentationType, config);
5521
+ }
5522
+
3505
5523
  // src/generators/pptx/PowerPointGenerator.ts
3506
5524
  var import_pptxgenjs = __toESM(require("pptxgenjs"));
3507
5525
 
@@ -4236,15 +6254,14 @@ var PowerPointGenerator = class {
4236
6254
  // src/core/PresentationEngine.ts
4237
6255
  var PresentationEngine = class {
4238
6256
  contentAnalyzer;
4239
- slideFactory;
4240
6257
  templateEngine;
4241
6258
  scoreCalculator;
4242
6259
  qaEngine;
4243
6260
  htmlGenerator;
4244
6261
  pptxGenerator;
6262
+ kb;
4245
6263
  constructor() {
4246
6264
  this.contentAnalyzer = new ContentAnalyzer();
4247
- this.slideFactory = new SlideFactory();
4248
6265
  this.templateEngine = new TemplateEngine();
4249
6266
  this.scoreCalculator = new ScoreCalculator();
4250
6267
  this.qaEngine = new QAEngine();
@@ -4259,10 +6276,15 @@ var PresentationEngine = class {
4259
6276
  */
4260
6277
  async generate(config) {
4261
6278
  this.validateConfig(config);
6279
+ console.log("\u{1F4DA} Loading knowledge base...");
6280
+ this.kb = await getKnowledgeGateway();
4262
6281
  console.log("\u{1F4DD} Analyzing content...");
4263
6282
  const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
6283
+ const presentationType = config.presentationType || analysis.detectedType;
6284
+ console.log(` \u2713 Presentation type: ${presentationType}`);
6285
+ const slideFactory = createSlideFactory(this.kb, presentationType);
4264
6286
  console.log("\u{1F3A8} Creating slides...");
4265
- const slides = await this.slideFactory.createSlides(analysis, config.mode);
6287
+ const slides = await slideFactory.createSlides(analysis);
4266
6288
  console.log("\u2705 Validating structure...");
4267
6289
  const structureErrors = this.validateStructure(slides, config.mode);
4268
6290
  if (structureErrors.length > 0) {
@@ -4283,23 +6305,52 @@ var PresentationEngine = class {
4283
6305
  }
4284
6306
  let qaResults;
4285
6307
  let score = 100;
6308
+ let iterativeResult = null;
6309
+ let finalSlides = slides;
6310
+ let finalHtml = outputs.html;
4286
6311
  if (!config.skipQA && outputs.html) {
4287
- console.log("\u{1F50D} Running QA validation...");
4288
- qaResults = await this.qaEngine.validate(outputs.html, {
4289
- mode: config.mode,
4290
- strictMode: true
4291
- });
4292
- score = this.scoreCalculator.calculate(qaResults);
4293
- console.log(`\u{1F4CA} QA Score: ${score}/100`);
4294
6312
  const threshold = config.qaThreshold ?? 95;
4295
- if (score < threshold) {
4296
- throw new QAFailureError(score, threshold, qaResults);
6313
+ const maxIterations = config.maxIterations ?? 5;
6314
+ const useIterativeQA = config.useIterativeQA !== false;
6315
+ if (useIterativeQA) {
6316
+ console.log("\u{1F50D} Running Iterative QA (7-Dimension Scoring)...");
6317
+ const iterativeEngine = createIterativeQAEngine(
6318
+ config.mode,
6319
+ presentationType,
6320
+ config
6321
+ );
6322
+ iterativeResult = await iterativeEngine.run(slides, outputs.html, {
6323
+ minScore: threshold,
6324
+ maxIterations,
6325
+ verbose: true
6326
+ });
6327
+ score = iterativeResult.finalScore;
6328
+ finalSlides = iterativeResult.slides;
6329
+ finalHtml = iterativeResult.html;
6330
+ if (outputs.html) {
6331
+ outputs.html = finalHtml;
6332
+ }
6333
+ qaResults = this.buildQAResultsFrom7Dimension(iterativeResult);
6334
+ if (!iterativeResult.passed) {
6335
+ throw new QAFailureError(score, threshold, qaResults);
6336
+ }
6337
+ } else {
6338
+ console.log("\u{1F50D} Running QA validation (legacy mode)...");
6339
+ qaResults = await this.qaEngine.validate(outputs.html, {
6340
+ mode: config.mode,
6341
+ strictMode: true
6342
+ });
6343
+ score = this.scoreCalculator.calculate(qaResults);
6344
+ console.log(`\u{1F4CA} QA Score: ${score}/100`);
6345
+ if (score < threshold) {
6346
+ throw new QAFailureError(score, threshold, qaResults);
6347
+ }
4297
6348
  }
4298
6349
  } else {
4299
6350
  qaResults = this.qaEngine.createEmptyResults();
4300
6351
  console.log("\u26A0\uFE0F QA validation skipped (NOT RECOMMENDED)");
4301
6352
  }
4302
- const metadata = this.buildMetadata(config, analysis, slides);
6353
+ const metadata = this.buildMetadata(config, analysis, finalSlides, iterativeResult);
4303
6354
  return {
4304
6355
  outputs,
4305
6356
  qaResults,
@@ -4307,6 +6358,46 @@ var PresentationEngine = class {
4307
6358
  metadata
4308
6359
  };
4309
6360
  }
6361
+ /**
6362
+ * Build QA results structure from 7-dimension scoring.
6363
+ */
6364
+ buildQAResultsFrom7Dimension(iterativeResult) {
6365
+ const scoring = iterativeResult.finalScoring;
6366
+ return {
6367
+ passed: scoring.passed,
6368
+ score: scoring.overallScore,
6369
+ visual: {
6370
+ whitespacePercentage: scoring.dimensions.layout.score,
6371
+ layoutBalance: scoring.dimensions.layout.score / 100,
6372
+ colorContrast: scoring.dimensions.contrast.score / 100
6373
+ },
6374
+ content: {
6375
+ perSlide: [],
6376
+ issues: scoring.issues.filter((i) => i.dimension === "content")
6377
+ },
6378
+ accessibility: {
6379
+ wcagLevel: scoring.dimensions.contrast.score >= 95 ? "AAA" : "AA",
6380
+ issues: scoring.issues.filter((i) => i.dimension === "contrast")
6381
+ },
6382
+ issues: scoring.issues.map((issue) => ({
6383
+ severity: issue.severity,
6384
+ message: issue.message,
6385
+ slideIndex: issue.slideIndex,
6386
+ dimension: issue.dimension
6387
+ })),
6388
+ dimensions: {
6389
+ layout: scoring.dimensions.layout.score,
6390
+ contrast: scoring.dimensions.contrast.score,
6391
+ graphics: scoring.dimensions.graphics.score,
6392
+ content: scoring.dimensions.content.score,
6393
+ clarity: scoring.dimensions.clarity.score,
6394
+ effectiveness: scoring.dimensions.effectiveness.score,
6395
+ consistency: scoring.dimensions.consistency.score
6396
+ },
6397
+ iterations: iterativeResult.iterations,
6398
+ report: iterativeResult.report
6399
+ };
6400
+ }
4310
6401
  /**
4311
6402
  * Validate presentation configuration.
4312
6403
  */
@@ -4377,13 +6468,13 @@ var PresentationEngine = class {
4377
6468
  /**
4378
6469
  * Build presentation metadata.
4379
6470
  */
4380
- buildMetadata(config, analysis, slides) {
6471
+ buildMetadata(config, analysis, slides, iterativeResult) {
4381
6472
  const wordCounts = slides.map((s) => this.countWords(s));
4382
6473
  const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
4383
6474
  const avgWordsPerSlide = Math.round(totalWords / slides.length);
4384
6475
  const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
4385
6476
  const estimatedDuration = Math.round(slides.length * minutesPerSlide);
4386
- return {
6477
+ const metadata = {
4387
6478
  title: config.title,
4388
6479
  author: config.author ?? "Unknown",
4389
6480
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4392,8 +6483,15 @@ var PresentationEngine = class {
4392
6483
  wordCount: totalWords,
4393
6484
  avgWordsPerSlide,
4394
6485
  estimatedDuration,
4395
- frameworks: this.detectFrameworks(analysis)
6486
+ frameworks: this.detectFrameworks(analysis),
6487
+ presentationType: config.presentationType || analysis.detectedType
4396
6488
  };
6489
+ if (iterativeResult) {
6490
+ metadata.qaIterations = iterativeResult.totalIterations;
6491
+ metadata.qaMaxIterations = iterativeResult.maxIterations;
6492
+ metadata.dimensionScores = iterativeResult.finalScoring.dimensions;
6493
+ }
6494
+ return metadata;
4397
6495
  }
4398
6496
  /**
4399
6497
  * Detect which expert frameworks were applied.
@@ -4416,6 +6514,95 @@ var PresentationEngine = class {
4416
6514
  }
4417
6515
  };
4418
6516
 
6517
+ // src/core/SlideGenerator.ts
6518
+ var SlideGenerator = class {
6519
+ kb;
6520
+ factory;
6521
+ mode = "keynote";
6522
+ presentationType = "ted_keynote";
6523
+ async initialize() {
6524
+ this.kb = await getKnowledgeGateway();
6525
+ }
6526
+ /**
6527
+ * Generate slides from analyzed content using KB-driven SlideFactory.
6528
+ *
6529
+ * @version 7.0.0 - Now delegates ALL slide creation to SlideFactory
6530
+ * SlideFactory queries KB for every decision:
6531
+ * - Allowed slide types per presentation type
6532
+ * - Word limits per type
6533
+ * - Bullet limits per type
6534
+ * - Content pattern to slide type mapping
6535
+ * - Slide validation against KB rules
6536
+ */
6537
+ async generate(analysis, type) {
6538
+ await this.initialize();
6539
+ this.presentationType = type || analysis.detectedType;
6540
+ this.mode = this.kb.getModeForType(this.presentationType);
6541
+ console.log(` \u2713 Using ${this.mode} mode for ${this.presentationType}`);
6542
+ this.factory = createSlideFactory(this.kb, this.presentationType);
6543
+ const slides = await this.factory.createSlides(analysis);
6544
+ const legacySlides = this.convertToLegacyFormat(slides);
6545
+ console.log(` \u2713 Generated ${legacySlides.length} KB-validated slides`);
6546
+ return legacySlides;
6547
+ }
6548
+ /**
6549
+ * Convert new Slide format to legacy format for backwards compatibility
6550
+ */
6551
+ convertToLegacyFormat(slides) {
6552
+ return slides.map((slide) => ({
6553
+ index: slide.index,
6554
+ type: slide.type,
6555
+ title: slide.data.title || "",
6556
+ content: {
6557
+ subtitle: slide.data.subtitle,
6558
+ statement: slide.data.body,
6559
+ body: slide.data.body,
6560
+ bullets: slide.data.bullets,
6561
+ metrics: slide.data.metrics,
6562
+ quote: slide.data.quote ? { text: slide.data.quote, attribution: slide.data.attribution } : void 0,
6563
+ callToAction: slide.data.keyMessage
6564
+ },
6565
+ notes: slide.notes,
6566
+ template: this.mapTypeToTemplate(slide.type)
6567
+ }));
6568
+ }
6569
+ /**
6570
+ * Map slide type to template name for backwards compatibility
6571
+ */
6572
+ mapTypeToTemplate(type) {
6573
+ const templateMap = {
6574
+ "title": "Title Impact",
6575
+ "agenda": "Detailed Findings",
6576
+ "big-number": "Data Insight",
6577
+ "metrics-grid": "Data Insight",
6578
+ "bullet-points": "Detailed Findings",
6579
+ "two-column": "Two Column",
6580
+ "three-column": "Three Column",
6581
+ "comparison": "Comparison",
6582
+ "timeline": "Process Timeline",
6583
+ "process": "Process",
6584
+ "quote": "Quote",
6585
+ "single-statement": "Single Statement",
6586
+ "cta": "Call to Action",
6587
+ "thank-you": "Title Impact"
6588
+ };
6589
+ return templateMap[type] || "Detailed Findings";
6590
+ }
6591
+ // =========================================================================
6592
+ // All slide creation is now handled by SlideFactory
6593
+ // Old methods removed in v7.0.0 - KB-driven SlideFactory handles:
6594
+ // - createTitleSlide, createAgendaSlide, createSituationSlide, etc.
6595
+ // - Content pattern classification
6596
+ // - KB-based slide type selection
6597
+ // - Text truncation, cleaning, deduplication
6598
+ // =========================================================================
6599
+ };
6600
+ async function initSlideGenerator() {
6601
+ const generator = new SlideGenerator();
6602
+ await generator.initialize();
6603
+ return generator;
6604
+ }
6605
+
4419
6606
  // src/media/ImageProvider.ts
4420
6607
  var LocalImageProvider = class {
4421
6608
  name = "local";
@@ -4604,7 +6791,7 @@ async function validate(presentation, options) {
4604
6791
  score
4605
6792
  };
4606
6793
  }
4607
- var VERSION = "6.0.0";
6794
+ var VERSION = "7.1.0";
4608
6795
  var index_default = {
4609
6796
  generate,
4610
6797
  validate,
@@ -4614,10 +6801,13 @@ var index_default = {
4614
6801
  };
4615
6802
  // Annotate the CommonJS export names for ESM import in node:
4616
6803
  0 && (module.exports = {
6804
+ AutoFixEngine,
4617
6805
  ChartJsProvider,
4618
6806
  CompositeChartProvider,
4619
6807
  CompositeImageProvider,
4620
6808
  ContentAnalyzer,
6809
+ ContentPatternClassifier,
6810
+ IterativeQAEngine,
4621
6811
  KnowledgeGateway,
4622
6812
  LocalImageProvider,
4623
6813
  MermaidProvider,
@@ -4629,16 +6819,22 @@ var index_default = {
4629
6819
  QuickChartProvider,
4630
6820
  RevealJsGenerator,
4631
6821
  ScoreCalculator,
6822
+ SevenDimensionScorer,
4632
6823
  SlideFactory,
6824
+ SlideGenerator,
4633
6825
  TemplateEngine,
4634
6826
  TemplateNotFoundError,
4635
6827
  UnsplashImageProvider,
4636
6828
  VERSION,
4637
6829
  ValidationError,
6830
+ createAutoFixEngine,
4638
6831
  createDefaultChartProvider,
4639
6832
  createDefaultImageProvider,
6833
+ createIterativeQAEngine,
6834
+ createSlideFactory,
4640
6835
  generate,
4641
6836
  getKnowledgeGateway,
6837
+ initSlideGenerator,
4642
6838
  validate
4643
6839
  });
4644
6840
  /**