@qontinui/ui-bridge 0.3.0 → 0.3.1

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.
Files changed (127) hide show
  1. package/dist/ai/index.d.mts +312 -155
  2. package/dist/ai/index.d.ts +312 -155
  3. package/dist/ai/index.js +2363 -67
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/index.mjs +2328 -68
  6. package/dist/ai/index.mjs.map +1 -1
  7. package/dist/annotations/index.d.mts +218 -0
  8. package/dist/annotations/index.d.ts +218 -0
  9. package/dist/annotations/index.js +246 -0
  10. package/dist/annotations/index.js.map +1 -0
  11. package/dist/annotations/index.mjs +241 -0
  12. package/dist/annotations/index.mjs.map +1 -0
  13. package/dist/assertions-BSR3afVr.d.ts +161 -0
  14. package/dist/assertions-CTw1hfOx.d.mts +161 -0
  15. package/dist/babel-plugin/index.js +23 -34
  16. package/dist/babel-plugin/index.js.map +1 -1
  17. package/dist/babel-plugin/index.mjs +23 -34
  18. package/dist/babel-plugin/index.mjs.map +1 -1
  19. package/dist/browser-capture-Bms60T6f.d.mts +47 -0
  20. package/dist/browser-capture-CsTU29mb.d.ts +47 -0
  21. package/dist/control/index.d.mts +26 -7
  22. package/dist/control/index.d.ts +26 -7
  23. package/dist/control/index.js +276 -48
  24. package/dist/control/index.js.map +1 -1
  25. package/dist/control/index.mjs +276 -48
  26. package/dist/control/index.mjs.map +1 -1
  27. package/dist/core/index.d.mts +2 -2
  28. package/dist/core/index.d.ts +2 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs.map +1 -1
  31. package/dist/debug/index.d.mts +5 -3
  32. package/dist/debug/index.d.ts +5 -3
  33. package/dist/debug/index.js +925 -1
  34. package/dist/debug/index.js.map +1 -1
  35. package/dist/debug/index.mjs +924 -2
  36. package/dist/debug/index.mjs.map +1 -1
  37. package/dist/index.d.mts +12 -7
  38. package/dist/index.d.ts +12 -7
  39. package/dist/index.js +4720 -173
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +4656 -174
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  44. package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  45. package/dist/native/control/index.js +2 -7
  46. package/dist/native/control/index.js.map +1 -1
  47. package/dist/native/control/index.mjs +2 -7
  48. package/dist/native/control/index.mjs.map +1 -1
  49. package/dist/native/core/index.js.map +1 -1
  50. package/dist/native/core/index.mjs.map +1 -1
  51. package/dist/native/debug/index.js +23 -66
  52. package/dist/native/debug/index.js.map +1 -1
  53. package/dist/native/debug/index.mjs +23 -66
  54. package/dist/native/debug/index.mjs.map +1 -1
  55. package/dist/native/index.js +89 -131
  56. package/dist/native/index.js.map +1 -1
  57. package/dist/native/index.mjs +89 -131
  58. package/dist/native/index.mjs.map +1 -1
  59. package/dist/native/react/index.js +28 -52
  60. package/dist/native/react/index.js.map +1 -1
  61. package/dist/native/react/index.mjs +28 -52
  62. package/dist/native/react/index.mjs.map +1 -1
  63. package/dist/native/server/index.js +38 -13
  64. package/dist/native/server/index.js.map +1 -1
  65. package/dist/native/server/index.mjs +38 -13
  66. package/dist/native/server/index.mjs.map +1 -1
  67. package/dist/react/index.d.mts +107 -8
  68. package/dist/react/index.d.ts +107 -8
  69. package/dist/react/index.js +2194 -84
  70. package/dist/react/index.js.map +1 -1
  71. package/dist/react/index.mjs +2194 -85
  72. package/dist/react/index.mjs.map +1 -1
  73. package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
  74. package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
  75. package/dist/render-log/index.d.mts +1 -1
  76. package/dist/render-log/index.d.ts +1 -1
  77. package/dist/server/express.d.mts +5 -4
  78. package/dist/server/express.d.ts +5 -4
  79. package/dist/server/express.js +104 -2
  80. package/dist/server/express.js.map +1 -1
  81. package/dist/server/express.mjs +104 -2
  82. package/dist/server/express.mjs.map +1 -1
  83. package/dist/server/handlers.d.mts +36 -5
  84. package/dist/server/handlers.d.ts +36 -5
  85. package/dist/server/handlers.js +3129 -224
  86. package/dist/server/handlers.js.map +1 -1
  87. package/dist/server/handlers.mjs +3129 -224
  88. package/dist/server/handlers.mjs.map +1 -1
  89. package/dist/server/index.d.mts +7 -5
  90. package/dist/server/index.d.ts +7 -5
  91. package/dist/server/index.js +3215 -183
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/index.mjs +3215 -183
  94. package/dist/server/index.mjs.map +1 -1
  95. package/dist/server/nextjs.d.mts +6 -4
  96. package/dist/server/nextjs.d.ts +6 -4
  97. package/dist/server/nextjs.js +106 -3
  98. package/dist/server/nextjs.js.map +1 -1
  99. package/dist/server/nextjs.mjs +106 -3
  100. package/dist/server/nextjs.mjs.map +1 -1
  101. package/dist/server/standalone.d.mts +6 -5
  102. package/dist/server/standalone.d.ts +6 -5
  103. package/dist/server/standalone.js +131 -5
  104. package/dist/server/standalone.js.map +1 -1
  105. package/dist/server/standalone.mjs +131 -5
  106. package/dist/server/standalone.mjs.map +1 -1
  107. package/dist/specs/index.d.mts +365 -0
  108. package/dist/specs/index.d.ts +365 -0
  109. package/dist/specs/index.js +2809 -0
  110. package/dist/specs/index.js.map +1 -0
  111. package/dist/specs/index.mjs +2786 -0
  112. package/dist/specs/index.mjs.map +1 -0
  113. package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
  114. package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
  115. package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
  116. package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
  117. package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
  118. package/dist/types-CebMQj76.d.ts +1275 -0
  119. package/dist/types-D_ypYl3T.d.mts +1275 -0
  120. package/dist/types-UBtp7R0u.d.mts +132 -0
  121. package/dist/types-UBtp7R0u.d.ts +132 -0
  122. package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
  123. package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
  124. package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
  125. package/package.json +18 -2
  126. package/dist/types-B5Q0GVo0.d.mts +0 -646
  127. package/dist/types-DfPqwU-i.d.ts +0 -646
package/dist/ai/index.mjs CHANGED
@@ -449,17 +449,72 @@ function generateDescription(input) {
449
449
  }
450
450
  parts.push(`"${name}"`);
451
451
  }
452
- const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [input.elementType || "element"];
452
+ const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
453
+ input.elementType || "element"
454
+ ];
453
455
  parts.push(typeWords[0]);
454
456
  if (input.inputType && input.inputType !== "text") {
455
457
  parts.push(`(${input.inputType})`);
456
458
  }
457
459
  return parts.join(" ");
458
460
  }
461
+ var CONTENT_TYPES = /* @__PURE__ */ new Set([
462
+ "heading",
463
+ "paragraph",
464
+ "list-item",
465
+ "table-cell",
466
+ "table-header",
467
+ "label",
468
+ "caption",
469
+ "blockquote",
470
+ "code-block",
471
+ "badge",
472
+ "status-message",
473
+ "metric-value",
474
+ "description-text",
475
+ "nav-text",
476
+ "content-generic"
477
+ ]);
459
478
  function generatePurpose(input) {
460
479
  const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
461
480
  const type = input.elementType?.toLowerCase() || "";
462
481
  const inputType = input.inputType?.toLowerCase() || "";
482
+ if (CONTENT_TYPES.has(type)) {
483
+ switch (type) {
484
+ case "heading":
485
+ return "Section heading";
486
+ case "paragraph":
487
+ return "Body text content";
488
+ case "list-item":
489
+ return "List item";
490
+ case "table-cell":
491
+ return "Table data cell";
492
+ case "table-header":
493
+ return "Table column header";
494
+ case "label":
495
+ return "Field label or definition term";
496
+ case "caption":
497
+ return "Figure or table caption";
498
+ case "blockquote":
499
+ return "Quoted content";
500
+ case "code-block":
501
+ return "Code or preformatted text";
502
+ case "badge":
503
+ return "Status badge or tag";
504
+ case "status-message":
505
+ return "Dynamic status indicator";
506
+ case "metric-value":
507
+ return "Metric or statistic value";
508
+ case "description-text":
509
+ return "Description or definition";
510
+ case "nav-text":
511
+ return "Navigation label";
512
+ case "content-generic":
513
+ return "Text content";
514
+ default:
515
+ return "Static content";
516
+ }
517
+ }
463
518
  if (type === "button" || inputType === "submit") {
464
519
  if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
465
520
  return "Submits the form";
@@ -524,6 +579,10 @@ function generateSuggestedActions(input) {
524
579
  const inputType = input.inputType?.toLowerCase() || "";
525
580
  const text = (input.textContent || input.ariaLabel || "").toLowerCase();
526
581
  const actions = [];
582
+ if (CONTENT_TYPES.has(type)) {
583
+ actions.push("read text content", "verify text matches expected");
584
+ return actions;
585
+ }
527
586
  switch (type) {
528
587
  case "button":
529
588
  actions.push(`click "${text || "this button"}"`);
@@ -575,6 +634,241 @@ function areSynonyms(word1, word2) {
575
634
  return synonyms1.includes(w2) || synonyms2.includes(w1);
576
635
  }
577
636
 
637
+ // src/annotations/types.ts
638
+ var ANNOTATION_CONFIG_VERSION = "1.0.0";
639
+
640
+ // src/annotations/store.ts
641
+ var AnnotationStore = class {
642
+ constructor() {
643
+ this.store = /* @__PURE__ */ new Map();
644
+ this.listeners = /* @__PURE__ */ new Set();
645
+ }
646
+ /**
647
+ * Get an annotation by element ID.
648
+ */
649
+ get(elementId) {
650
+ return this.store.get(elementId);
651
+ }
652
+ /**
653
+ * Get all annotations as a record.
654
+ */
655
+ getAll() {
656
+ const result = {};
657
+ for (const [id, annotation] of this.store) {
658
+ result[id] = annotation;
659
+ }
660
+ return result;
661
+ }
662
+ /**
663
+ * Set an annotation for an element. Auto-sets `updatedAt`.
664
+ */
665
+ set(elementId, annotation) {
666
+ const updated = {
667
+ ...annotation,
668
+ updatedAt: Date.now()
669
+ };
670
+ this.store.set(elementId, updated);
671
+ this.emit({
672
+ type: "annotation:set",
673
+ elementId,
674
+ annotation: updated,
675
+ timestamp: Date.now()
676
+ });
677
+ }
678
+ /**
679
+ * Delete an annotation by element ID.
680
+ *
681
+ * @returns true if the annotation existed and was deleted
682
+ */
683
+ delete(elementId) {
684
+ const existed = this.store.delete(elementId);
685
+ if (existed) {
686
+ this.emit({
687
+ type: "annotation:deleted",
688
+ elementId,
689
+ timestamp: Date.now()
690
+ });
691
+ }
692
+ return existed;
693
+ }
694
+ /**
695
+ * Check if an annotation exists for an element.
696
+ */
697
+ has(elementId) {
698
+ return this.store.has(elementId);
699
+ }
700
+ /**
701
+ * Get the number of stored annotations.
702
+ */
703
+ get count() {
704
+ return this.store.size;
705
+ }
706
+ /**
707
+ * Clear all annotations.
708
+ */
709
+ clear() {
710
+ this.store.clear();
711
+ this.emit({
712
+ type: "annotation:cleared",
713
+ timestamp: Date.now()
714
+ });
715
+ }
716
+ /**
717
+ * Import annotations from a config object.
718
+ *
719
+ * Merges with existing annotations (new values overwrite per element ID).
720
+ *
721
+ * @returns Number of annotations imported
722
+ *
723
+ * @example
724
+ * ```ts
725
+ * const config: AnnotationConfig = {
726
+ * version: '1.0.0',
727
+ * annotations: {
728
+ * 'btn-1': { description: 'Submit button', tags: ['form'] },
729
+ * 'input-1': { description: 'Name field' },
730
+ * },
731
+ * };
732
+ * const count = store.importConfig(config); // 2
733
+ * ```
734
+ */
735
+ importConfig(config) {
736
+ let count = 0;
737
+ for (const [id, annotation] of Object.entries(config.annotations)) {
738
+ this.store.set(id, {
739
+ ...annotation,
740
+ updatedAt: annotation.updatedAt ?? Date.now()
741
+ });
742
+ count++;
743
+ }
744
+ this.emit({
745
+ type: "annotation:imported",
746
+ count,
747
+ timestamp: Date.now()
748
+ });
749
+ return count;
750
+ }
751
+ /**
752
+ * Export all annotations as a config object.
753
+ *
754
+ * The returned object can be serialized to JSON and saved to a file,
755
+ * then later re-imported with {@link importConfig}.
756
+ *
757
+ * @param metadata - Optional metadata to include (appName, description, etc.)
758
+ * @returns AnnotationConfig with all current annotations
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * const config = store.exportConfig({ appName: 'MyApp' });
763
+ * // config.version === '1.0.0'
764
+ * // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
765
+ * // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
766
+ *
767
+ * // Save to file
768
+ * fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
769
+ * ```
770
+ */
771
+ exportConfig(metadata) {
772
+ return {
773
+ version: ANNOTATION_CONFIG_VERSION,
774
+ annotations: this.getAll(),
775
+ metadata: {
776
+ ...metadata,
777
+ exportedAt: Date.now()
778
+ }
779
+ };
780
+ }
781
+ /**
782
+ * Compute annotation coverage against a set of known element IDs.
783
+ *
784
+ * Compares the store's annotations against the provided list of element IDs
785
+ * to determine what percentage of elements have been annotated.
786
+ *
787
+ * @param allElementIds - Array of all known element IDs in the UI
788
+ * @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
789
+ *
790
+ * @example
791
+ * ```ts
792
+ * store.set('btn-1', { description: 'Submit' });
793
+ * store.set('input-1', { description: 'Name' });
794
+ *
795
+ * const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
796
+ * // coverage.totalElements === 4
797
+ * // coverage.annotatedElements === 2
798
+ * // coverage.coveragePercent === 50
799
+ * // coverage.annotatedIds === ['btn-1', 'input-1']
800
+ * // coverage.unannotatedIds === ['input-2', 'link-1']
801
+ * ```
802
+ */
803
+ getCoverage(allElementIds) {
804
+ const annotatedIds = [];
805
+ const unannotatedIds = [];
806
+ for (const id of allElementIds) {
807
+ if (this.store.has(id)) {
808
+ annotatedIds.push(id);
809
+ } else {
810
+ unannotatedIds.push(id);
811
+ }
812
+ }
813
+ const total = allElementIds.length;
814
+ return {
815
+ totalElements: total,
816
+ annotatedElements: annotatedIds.length,
817
+ coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
818
+ annotatedIds,
819
+ unannotatedIds,
820
+ timestamp: Date.now()
821
+ };
822
+ }
823
+ /**
824
+ * Subscribe to annotation events.
825
+ *
826
+ * The listener is called whenever annotations are set, deleted, imported,
827
+ * or cleared. Returns an unsubscribe function to stop listening.
828
+ *
829
+ * @param listener - Callback function receiving {@link AnnotationEvent} objects
830
+ * @returns Unsubscribe function - call it to remove the listener
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * const unsubscribe = store.on((event) => {
835
+ * if (event.type === 'annotation:set') {
836
+ * console.log(`Element ${event.elementId} annotated:`, event.annotation);
837
+ * }
838
+ * });
839
+ *
840
+ * store.set('btn-1', { description: 'Submit' });
841
+ * // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
842
+ *
843
+ * unsubscribe(); // Stop listening
844
+ * ```
845
+ */
846
+ on(listener) {
847
+ this.listeners.add(listener);
848
+ return () => {
849
+ this.listeners.delete(listener);
850
+ };
851
+ }
852
+ /**
853
+ * Emit an event to all listeners.
854
+ */
855
+ emit(event) {
856
+ for (const listener of this.listeners) {
857
+ try {
858
+ listener(event);
859
+ } catch {
860
+ }
861
+ }
862
+ }
863
+ };
864
+ var globalStore = null;
865
+ function getGlobalAnnotationStore() {
866
+ if (!globalStore) {
867
+ globalStore = new AnnotationStore();
868
+ }
869
+ return globalStore;
870
+ }
871
+
578
872
  // src/ai/search-engine.ts
579
873
  var DEFAULT_SEARCH_CONFIG = {
580
874
  fuzzyThreshold: 0.7,
@@ -617,17 +911,40 @@ var SearchEngine = class {
617
911
  if ("getState" in element && typeof element.getState === "function") {
618
912
  state = getState ? getState(element) : element.getState();
619
913
  textContent = state.textContent || void 0;
620
- tagName = element.element.tagName.toLowerCase();
621
- role = element.element.getAttribute("role") || void 0;
622
- ariaLabel = element.element.getAttribute("aria-label") || void 0;
623
- placeholder = element.element.getAttribute("placeholder") || void 0;
624
- title = element.element.getAttribute("title") || void 0;
625
- if (element.element.id) {
626
- const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
627
- labelText = labelEl?.textContent?.trim() || void 0;
628
- }
629
- if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
630
- value = element.element.value || void 0;
914
+ try {
915
+ tagName = element.element.tagName.toLowerCase();
916
+ } catch {
917
+ tagName = element.type || "unknown";
918
+ }
919
+ try {
920
+ role = element.element.getAttribute("role") || void 0;
921
+ ariaLabel = element.element.getAttribute("aria-label") || void 0;
922
+ placeholder = element.element.getAttribute("placeholder") || void 0;
923
+ title = element.element.getAttribute("title") || void 0;
924
+ } catch {
925
+ }
926
+ if (!ariaLabel && element.label) {
927
+ ariaLabel = element.label;
928
+ }
929
+ try {
930
+ if (element.element.id) {
931
+ const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
932
+ labelText = labelEl?.textContent?.trim() || void 0;
933
+ }
934
+ } catch {
935
+ }
936
+ if (!labelText && element.label) {
937
+ labelText = element.label;
938
+ }
939
+ if (!textContent && element.label) {
940
+ textContent = element.label;
941
+ }
942
+ try {
943
+ if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
944
+ value = element.element.value || void 0;
945
+ }
946
+ } catch {
947
+ value = state.value || void 0;
631
948
  }
632
949
  } else {
633
950
  const discovered = element;
@@ -636,8 +953,11 @@ var SearchEngine = class {
636
953
  tagName = discovered.tagName;
637
954
  role = discovered.role || void 0;
638
955
  ariaLabel = discovered.accessibleName || void 0;
956
+ if (!labelText && element.label) {
957
+ labelText = element.label;
958
+ }
639
959
  }
640
- const aliases = generateAliases({
960
+ let aliases = generateAliases({
641
961
  textContent,
642
962
  ariaLabel,
643
963
  placeholder,
@@ -647,7 +967,14 @@ var SearchEngine = class {
647
967
  labelText,
648
968
  value
649
969
  });
650
- const description = generateDescription({
970
+ if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
971
+ const aliasSet = /* @__PURE__ */ new Set([
972
+ ...aliases,
973
+ ...element.aliases.map((a) => a.toLowerCase())
974
+ ]);
975
+ aliases = [...aliasSet];
976
+ }
977
+ let description = generateDescription({
651
978
  textContent,
652
979
  ariaLabel,
653
980
  placeholder,
@@ -656,6 +983,22 @@ var SearchEngine = class {
656
983
  id: element.id,
657
984
  labelText
658
985
  });
986
+ if (!description && "description" in element && element.description) {
987
+ description = element.description;
988
+ }
989
+ const annotation = getGlobalAnnotationStore().get(element.id);
990
+ if (annotation) {
991
+ if (annotation.description) {
992
+ description = annotation.description;
993
+ }
994
+ if (annotation.tags && annotation.tags.length > 0) {
995
+ const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
996
+ aliases = [...tagSet];
997
+ }
998
+ if (annotation.notes) {
999
+ aliases.push(annotation.notes.toLowerCase());
1000
+ }
1001
+ }
659
1002
  return {
660
1003
  id: element.id,
661
1004
  element,
@@ -758,7 +1101,12 @@ var SearchEngine = class {
758
1101
  threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
759
1102
  };
760
1103
  if (criteria.text) {
761
- const textScore = this.scoreTextMatch(searchable, criteria.text, criteria.fuzzy !== false, fuzzyConfig.threshold);
1104
+ const textScore = this.scoreTextMatch(
1105
+ searchable,
1106
+ criteria.text,
1107
+ criteria.fuzzy !== false,
1108
+ fuzzyConfig.threshold
1109
+ );
762
1110
  scores.text = textScore.score;
763
1111
  if (textScore.score > 0) {
764
1112
  matchReasons.push(...textScore.reasons);
@@ -766,8 +1114,37 @@ var SearchEngine = class {
766
1114
  weightedScore += textScore.score * this.config.textWeight;
767
1115
  totalWeight += this.config.textWeight;
768
1116
  }
1117
+ if (criteria.textContent && !criteria.text) {
1118
+ const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
1119
+ let bestScore = 0;
1120
+ let bestReasons = [];
1121
+ for (const alt of alternatives) {
1122
+ const exactScore = this.scoreTextMatch(
1123
+ searchable,
1124
+ alt,
1125
+ criteria.fuzzy !== false,
1126
+ fuzzyConfig.threshold
1127
+ );
1128
+ const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
1129
+ const altBest = Math.max(exactScore.score, containsScore.score);
1130
+ if (altBest > bestScore) {
1131
+ bestScore = altBest;
1132
+ bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
1133
+ }
1134
+ }
1135
+ scores.text = bestScore;
1136
+ if (bestScore > 0) {
1137
+ matchReasons.push(...bestReasons);
1138
+ }
1139
+ weightedScore += bestScore * this.config.textWeight;
1140
+ totalWeight += this.config.textWeight;
1141
+ }
769
1142
  if (criteria.textContains) {
770
- const containsScore = this.scoreContainsMatch(searchable, criteria.textContains, criteria.fuzzy !== false);
1143
+ const containsScore = this.scoreContainsMatch(
1144
+ searchable,
1145
+ criteria.textContains,
1146
+ criteria.fuzzy !== false
1147
+ );
771
1148
  scores.text = Math.max(scores.text || 0, containsScore.score);
772
1149
  if (containsScore.score > 0 && containsScore.reasons.length > 0) {
773
1150
  matchReasons.push(...containsScore.reasons);
@@ -816,7 +1193,11 @@ var SearchEngine = class {
816
1193
  totalWeight += this.config.spatialWeight;
817
1194
  }
818
1195
  if (criteria.placeholder && searchable.placeholder) {
819
- const placeholderResult = fuzzyMatch(searchable.placeholder, criteria.placeholder, fuzzyConfig);
1196
+ const placeholderResult = fuzzyMatch(
1197
+ searchable.placeholder,
1198
+ criteria.placeholder,
1199
+ fuzzyConfig
1200
+ );
820
1201
  if (placeholderResult.isMatch) {
821
1202
  matchReasons.push(`placeholder matches`);
822
1203
  weightedScore += placeholderResult.similarity * this.config.textWeight;
@@ -861,11 +1242,9 @@ var SearchEngine = class {
861
1242
  scoreTextMatch(searchable, text, fuzzy, threshold) {
862
1243
  const reasons = [];
863
1244
  let maxScore = 0;
864
- const textsToMatch = [
865
- searchable.textContent,
866
- searchable.labelText,
867
- searchable.value
868
- ].filter(Boolean);
1245
+ const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
1246
+ Boolean
1247
+ );
869
1248
  for (const targetText of textsToMatch) {
870
1249
  if (targetText.toLowerCase() === text.toLowerCase()) {
871
1250
  maxScore = Math.max(maxScore, 1);
@@ -961,7 +1340,9 @@ var SearchEngine = class {
961
1340
  heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
962
1341
  };
963
1342
  const inferredRoles = tagRoleMap[normalizedRole] || [];
964
- if (inferredRoles.some((r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole)) {
1343
+ if (inferredRoles.some(
1344
+ (r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
1345
+ )) {
965
1346
  return { score: 0.8, reasons: [`inferred role: ${role}`] };
966
1347
  }
967
1348
  return { score: 0, reasons };
@@ -1151,11 +1532,14 @@ function generatePageSummary(elements, pageContext, config = {}) {
1151
1532
  if (finalConfig.includeElementCounts) {
1152
1533
  const counts = countElementTypes(elements);
1153
1534
  const countParts = [];
1154
- if (counts.button > 0) countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1535
+ if (counts.button > 0)
1536
+ countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1155
1537
  if (counts.input > 0) countParts.push(`${counts.input} input${counts.input > 1 ? "s" : ""}`);
1156
1538
  if (counts.link > 0) countParts.push(`${counts.link} link${counts.link > 1 ? "s" : ""}`);
1157
- if (counts.select > 0) countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1158
- if (counts.checkbox > 0) countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1539
+ if (counts.select > 0)
1540
+ countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1541
+ if (counts.checkbox > 0)
1542
+ countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1159
1543
  if (countParts.length > 0) {
1160
1544
  lines.push(`Contains: ${countParts.join(", ")}`);
1161
1545
  }
@@ -1219,7 +1603,9 @@ function generateFormSummary(form, verbosity) {
1219
1603
  if (verbosity === "brief") {
1220
1604
  const fieldCount = form.fields.length;
1221
1605
  const filledCount = form.fields.filter((f) => f.value).length;
1222
- lines.push(` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`);
1606
+ lines.push(
1607
+ ` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`
1608
+ );
1223
1609
  } else {
1224
1610
  for (const field of form.fields) {
1225
1611
  let fieldLine = ` - ${field.label || field.id}`;
@@ -1315,7 +1701,9 @@ function generateDiffSummary(appeared, disappeared, modified) {
1315
1701
  if (modified.length > 0) {
1316
1702
  lines.push("Changed:");
1317
1703
  for (const mod of modified.slice(0, 5)) {
1318
- lines.push(` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`);
1704
+ lines.push(
1705
+ ` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`
1706
+ );
1319
1707
  }
1320
1708
  if (modified.length > 5) {
1321
1709
  lines.push(` ... and ${modified.length - 5} more changes`);
@@ -1765,7 +2153,9 @@ function parseNLInstruction(instruction) {
1765
2153
  }
1766
2154
  }
1767
2155
  if (pattern.action === "assert") {
1768
- const assertMatch = trimmed.match(/(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i);
2156
+ const assertMatch = trimmed.match(
2157
+ /(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i
2158
+ );
1769
2159
  if (assertMatch) {
1770
2160
  parsed.assertionType = ASSERTION_TYPE_MAP[assertMatch[1].toLowerCase()];
1771
2161
  }
@@ -2280,7 +2670,9 @@ function formatErrorContext(context) {
2280
2670
  lines.push("");
2281
2671
  if (context.searchResults.nearestMatch) {
2282
2672
  const match = context.searchResults.nearestMatch;
2283
- lines.push(`Nearest match: "${match.element.description}" (${(match.confidence * 100).toFixed(0)}% confidence)`);
2673
+ lines.push(
2674
+ `Nearest match: "${match.element.description}" (${(match.confidence * 100).toFixed(0)}% confidence)`
2675
+ );
2284
2676
  lines.push(`Why not used: ${match.whyNotSelected}`);
2285
2677
  lines.push("");
2286
2678
  }
@@ -2627,7 +3019,9 @@ var NLActionExecutor = class {
2627
3019
  switch (errorCode) {
2628
3020
  case "PARSE_ERROR":
2629
3021
  suggestions.push('Try using a simpler phrase like "click Submit button"');
2630
- suggestions.push('Ensure the instruction follows patterns like "click X" or "type Y into X"');
3022
+ suggestions.push(
3023
+ 'Ensure the instruction follows patterns like "click X" or "type Y into X"'
3024
+ );
2631
3025
  break;
2632
3026
  case "ELEMENT_NOT_FOUND":
2633
3027
  if (alternatives.length > 0) {
@@ -2698,9 +3092,15 @@ var AssertionExecutor = class {
2698
3092
  async assert(request) {
2699
3093
  const startTime = performance.now();
2700
3094
  const timeout = request.timeout ?? this.config.defaultTimeout;
2701
- const element = await this.findElement(request.target, request.fuzzy !== false);
3095
+ const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
3096
+ const element = searchResult?.element ?? null;
3097
+ const searchDetails = searchResult ? {
3098
+ confidence: searchResult.confidence,
3099
+ matchReasons: searchResult.matchReasons,
3100
+ candidateCount: this.elements.length
3101
+ } : void 0;
2702
3102
  if (!element && request.type !== "notExists") {
2703
- return this.createResult(
3103
+ const result2 = this.createResult(
2704
3104
  false,
2705
3105
  typeof request.target === "string" ? request.target : JSON.stringify(request.target),
2706
3106
  "element not found",
@@ -2710,8 +3110,16 @@ var AssertionExecutor = class {
2710
3110
  this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
2711
3111
  startTime
2712
3112
  );
3113
+ if (searchDetails) {
3114
+ result2.searchDetails = searchDetails;
3115
+ }
3116
+ return result2;
2713
3117
  }
2714
- return this.executeAssertion(request, element, timeout, startTime);
3118
+ const result = await this.executeAssertion(request, element, timeout, startTime);
3119
+ if (searchDetails) {
3120
+ result.searchDetails = searchDetails;
3121
+ }
3122
+ return result;
2715
3123
  }
2716
3124
  /**
2717
3125
  * Execute multiple assertions
@@ -2816,16 +3224,26 @@ var AssertionExecutor = class {
2816
3224
  return this.assert({ target, type: "count", expected: expectedCount, timeout });
2817
3225
  }
2818
3226
  /**
2819
- * Find element by target (string or criteria)
3227
+ * Find element by target with full search metadata.
3228
+ * Returns the SearchResult (including confidence, matchReasons, scores)
3229
+ * or null if no match above the fuzzy threshold.
2820
3230
  */
2821
- async findElement(target, fuzzy = true) {
3231
+ findElementDetailed(target, fuzzy = true) {
2822
3232
  const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
2823
- const searchResult = this.searchEngine.findBest(criteria);
3233
+ const searchResult = this.searchEngine.findBest(criteria, this.elements);
2824
3234
  if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
2825
- return searchResult.element;
3235
+ return searchResult;
2826
3236
  }
2827
3237
  return null;
2828
3238
  }
3239
+ /**
3240
+ * Find element by target (string or criteria).
3241
+ * Public for use by condition evaluation in SpecExecutor.
3242
+ */
3243
+ async findElement(target, fuzzy = true) {
3244
+ const result = this.findElementDetailed(target, fuzzy);
3245
+ return result?.element ?? null;
3246
+ }
2829
3247
  /**
2830
3248
  * Execute the actual assertion
2831
3249
  */
@@ -2834,19 +3252,55 @@ var AssertionExecutor = class {
2834
3252
  const elementDescription = element?.description || targetStr;
2835
3253
  switch (request.type) {
2836
3254
  case "visible":
2837
- return this.assertVisibility(element, true, elementDescription, request.message, startTime);
3255
+ return this.assertVisibility(
3256
+ element,
3257
+ true,
3258
+ elementDescription,
3259
+ request.message,
3260
+ startTime
3261
+ );
2838
3262
  case "hidden":
2839
- return this.assertVisibility(element, false, elementDescription, request.message, startTime);
3263
+ return this.assertVisibility(
3264
+ element,
3265
+ false,
3266
+ elementDescription,
3267
+ request.message,
3268
+ startTime
3269
+ );
2840
3270
  case "enabled":
2841
- return this.assertEnabledState(element, true, elementDescription, request.message, startTime);
3271
+ return this.assertEnabledState(
3272
+ element,
3273
+ true,
3274
+ elementDescription,
3275
+ request.message,
3276
+ startTime
3277
+ );
2842
3278
  case "disabled":
2843
- return this.assertEnabledState(element, false, elementDescription, request.message, startTime);
3279
+ return this.assertEnabledState(
3280
+ element,
3281
+ false,
3282
+ elementDescription,
3283
+ request.message,
3284
+ startTime
3285
+ );
2844
3286
  case "focused":
2845
3287
  return this.assertFocused(element, elementDescription, request.message, startTime);
2846
3288
  case "checked":
2847
- return this.assertCheckedState(element, true, elementDescription, request.message, startTime);
3289
+ return this.assertCheckedState(
3290
+ element,
3291
+ true,
3292
+ elementDescription,
3293
+ request.message,
3294
+ startTime
3295
+ );
2848
3296
  case "unchecked":
2849
- return this.assertCheckedState(element, false, elementDescription, request.message, startTime);
3297
+ return this.assertCheckedState(
3298
+ element,
3299
+ false,
3300
+ elementDescription,
3301
+ request.message,
3302
+ startTime
3303
+ );
2850
3304
  case "hasText":
2851
3305
  return this.assertTextMatch(
2852
3306
  element,
@@ -3184,7 +3638,8 @@ var DEFAULT_SNAPSHOT_CONFIG = {
3184
3638
  detectModals: true,
3185
3639
  inferPageType: true,
3186
3640
  generateDescriptions: true,
3187
- maxElements: 500
3641
+ maxElements: 500,
3642
+ useAnnotations: true
3188
3643
  };
3189
3644
  var SemanticSnapshotManager = class {
3190
3645
  constructor(config = {}) {
@@ -3257,26 +3712,52 @@ var SemanticSnapshotManager = class {
3257
3712
  * Convert a single element to AI element
3258
3713
  */
3259
3714
  convertElement(element) {
3715
+ const isContent = element.category === "content";
3260
3716
  const aliases = generateAliases({
3261
3717
  textContent: element.state.textContent,
3262
3718
  elementType: element.type,
3263
3719
  id: element.id,
3264
3720
  labelText: element.label
3265
3721
  });
3266
- const description = this.config.generateDescriptions ? generateDescription({
3267
- textContent: element.state.textContent,
3268
- elementType: element.type,
3269
- id: element.id,
3270
- labelText: element.label
3271
- }) : element.label || element.id;
3272
- const purpose = generatePurpose({
3722
+ let description;
3723
+ if (isContent && element.contentMetadata) {
3724
+ description = this.generateContentDescription(element);
3725
+ } else if (this.config.generateDescriptions) {
3726
+ description = generateDescription({
3727
+ textContent: element.state.textContent,
3728
+ elementType: element.type,
3729
+ id: element.id,
3730
+ labelText: element.label
3731
+ });
3732
+ } else {
3733
+ description = element.label || element.id;
3734
+ }
3735
+ const purpose = isContent ? generatePurpose({ textContent: element.state.textContent, elementType: element.type }) : generatePurpose({ textContent: element.state.textContent, elementType: element.type });
3736
+ const suggestedActions = isContent ? generateSuggestedActions({
3273
3737
  textContent: element.state.textContent,
3274
3738
  elementType: element.type
3275
- });
3276
- const suggestedActions = generateSuggestedActions({
3739
+ }) : generateSuggestedActions({
3277
3740
  textContent: element.state.textContent,
3278
3741
  elementType: element.type
3279
3742
  });
3743
+ let finalDescription = description;
3744
+ let finalPurpose = purpose;
3745
+ let finalAliases = aliases;
3746
+ if (this.config.useAnnotations) {
3747
+ const annotation = getGlobalAnnotationStore().get(element.id);
3748
+ if (annotation) {
3749
+ if (annotation.description) {
3750
+ finalDescription = annotation.description;
3751
+ }
3752
+ if (annotation.purpose) {
3753
+ finalPurpose = annotation.purpose;
3754
+ }
3755
+ if (annotation.tags && annotation.tags.length > 0) {
3756
+ const tagSet = /* @__PURE__ */ new Set([...finalAliases, ...annotation.tags.map((t) => t.toLowerCase())]);
3757
+ finalAliases = [...tagSet];
3758
+ }
3759
+ }
3760
+ }
3280
3761
  return {
3281
3762
  id: element.id,
3282
3763
  type: element.type,
@@ -3287,13 +3768,56 @@ var SemanticSnapshotManager = class {
3287
3768
  actions: element.actions,
3288
3769
  state: element.state,
3289
3770
  registered: true,
3290
- description,
3291
- aliases,
3292
- purpose,
3771
+ description: finalDescription,
3772
+ aliases: finalAliases,
3773
+ purpose: finalPurpose,
3293
3774
  suggestedActions,
3294
- semanticType: this.inferSemanticType(element)
3775
+ semanticType: this.inferSemanticType(element),
3776
+ category: element.category,
3777
+ contentMetadata: element.contentMetadata
3295
3778
  };
3296
3779
  }
3780
+ /**
3781
+ * Generate a content-specific description
3782
+ */
3783
+ generateContentDescription(element) {
3784
+ const meta = element.contentMetadata;
3785
+ const text = element.state.textContent?.trim() || "";
3786
+ const truncatedText = text.length > 60 ? text.substring(0, 57) + "..." : text;
3787
+ if (!meta) return `"${truncatedText}"`;
3788
+ switch (meta.contentRole) {
3789
+ case "heading":
3790
+ return `Level ${meta.headingLevel || "?"} heading: '${truncatedText}'`;
3791
+ case "table-cell":
3792
+ return `Table cell${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3793
+ case "table-header":
3794
+ return `Table header${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3795
+ case "status":
3796
+ return `Status message: '${truncatedText}'`;
3797
+ case "badge":
3798
+ return `Badge: '${truncatedText}'`;
3799
+ case "metric":
3800
+ return `Metric value: '${truncatedText}'`;
3801
+ case "body-text":
3802
+ return `Text: '${truncatedText}'`;
3803
+ case "list-item":
3804
+ return `List item: '${truncatedText}'`;
3805
+ case "quote":
3806
+ return `Blockquote: '${truncatedText}'`;
3807
+ case "code":
3808
+ return `Code block: '${truncatedText}'`;
3809
+ case "caption":
3810
+ return `Caption: '${truncatedText}'`;
3811
+ case "label":
3812
+ return `Label: '${truncatedText}'`;
3813
+ case "description":
3814
+ return `Description: '${truncatedText}'`;
3815
+ case "navigation":
3816
+ return `Navigation text: '${truncatedText}'`;
3817
+ default:
3818
+ return `Content: '${truncatedText}'`;
3819
+ }
3820
+ }
3297
3821
  /**
3298
3822
  * Build full page context
3299
3823
  */
@@ -3397,9 +3921,7 @@ var SemanticSnapshotManager = class {
3397
3921
  */
3398
3922
  detectModals(elements) {
3399
3923
  const modals = [];
3400
- const dialogElements = elements.filter(
3401
- (el) => el.type === "dialog" && el.state.visible
3402
- );
3924
+ const dialogElements = elements.filter((el) => el.type === "dialog" && el.state.visible);
3403
3925
  for (const dialog of dialogElements) {
3404
3926
  const closeButton = elements.find(
3405
3927
  (el) => el.type === "button" && el.state.visible && (el.semanticType === "cancel-button" || el.state.textContent?.toLowerCase().match(/close|cancel|x|dismiss/))
@@ -3450,9 +3972,7 @@ var SemanticSnapshotManager = class {
3450
3972
  * Infer form purpose from fields
3451
3973
  */
3452
3974
  inferFormPurpose(fields) {
3453
- const labels = fields.map(
3454
- (f) => (f.accessibleName || f.label || "").toLowerCase()
3455
- );
3975
+ const labels = fields.map((f) => (f.accessibleName || f.label || "").toLowerCase());
3456
3976
  const allLabels = labels.join(" ");
3457
3977
  if (allLabels.includes("email") && allLabels.includes("password")) {
3458
3978
  if (allLabels.includes("confirm") || allLabels.includes("name")) {
@@ -3506,6 +4026,13 @@ var SemanticSnapshotManager = class {
3506
4026
  * Infer semantic type
3507
4027
  */
3508
4028
  inferSemanticType(element) {
4029
+ if (element.category === "content" && element.contentMetadata) {
4030
+ const role = element.contentMetadata.contentRole;
4031
+ if (role === "heading" && element.contentMetadata.headingLevel) {
4032
+ return `heading-${element.contentMetadata.headingLevel}`;
4033
+ }
4034
+ return role;
4035
+ }
3509
4036
  const text = (element.state.textContent || element.label || "").toLowerCase();
3510
4037
  const type = element.type.toLowerCase();
3511
4038
  if (type === "button") {
@@ -3590,6 +4117,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3590
4117
  const probableTrigger = detectTrigger(appeared, disappeared, limitedModifications);
3591
4118
  const suggestedActions = finalConfig.generateSuggestions ? generateSuggestedActionsFromDiff(appeared, disappeared, limitedModifications, probableTrigger) : void 0;
3592
4119
  const pageChanges = detectPageChanges(fromSnapshot, toSnapshot);
4120
+ const contentChanges = detectContentChanges(fromElements, toElements);
3593
4121
  const summary = generateDiffSummary(
3594
4122
  appeared.map((e) => e.description),
3595
4123
  disappeared.map((e) => e.description),
@@ -3604,6 +4132,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3604
4132
  disappeared,
3605
4133
  modified: limitedModifications
3606
4134
  },
4135
+ contentChanges: contentChanges || void 0,
3607
4136
  probableTrigger,
3608
4137
  suggestedActions,
3609
4138
  pageChanges,
@@ -3738,9 +4267,7 @@ function generateSuggestedActionsFromDiff(appeared, disappeared, modified, trigg
3738
4267
  suggestions.push("Fix the validation errors before submitting");
3739
4268
  }
3740
4269
  if (trigger === "Modal opened") {
3741
- const modal = appeared.find(
3742
- (e) => e.type === "dialog" || e.semanticType?.includes("dialog")
3743
- );
4270
+ const modal = appeared.find((e) => e.type === "dialog" || e.semanticType?.includes("dialog"));
3744
4271
  if (modal) {
3745
4272
  suggestions.push(`Interact with the "${modal.description}" dialog`);
3746
4273
  }
@@ -3811,6 +4338,12 @@ function hasSignificantChanges(diff) {
3811
4338
  if (diff.changes.disappeared.length > 0) return true;
3812
4339
  if (diff.changes.modified.some((m) => m.significant)) return true;
3813
4340
  if (diff.pageChanges?.urlChanged) return true;
4341
+ if (diff.contentChanges) {
4342
+ const cc = diff.contentChanges;
4343
+ if (cc.textChanges.length > 0) return true;
4344
+ if (cc.metricChanges.some((m) => m.significant)) return true;
4345
+ if (cc.statusChanges.length > 0) return true;
4346
+ }
3814
4347
  return false;
3815
4348
  }
3816
4349
  function describeDiff(diff) {
@@ -3828,12 +4361,1739 @@ function describeDiff(diff) {
3828
4361
  if (diff.pageChanges?.urlChanged) {
3829
4362
  parts.push("URL changed");
3830
4363
  }
4364
+ if (diff.contentChanges) {
4365
+ parts.push(diff.contentChanges.summary);
4366
+ }
3831
4367
  if (parts.length === 0) {
3832
4368
  return "No significant changes";
3833
4369
  }
3834
4370
  return parts.join(", ");
3835
4371
  }
4372
+ var METRIC_CONTENT_TYPES = /* @__PURE__ */ new Set(["metric-value"]);
4373
+ var STATUS_CONTENT_TYPES = /* @__PURE__ */ new Set(["status-message", "badge"]);
4374
+ var HEADING_CONTENT_TYPES = /* @__PURE__ */ new Set(["heading"]);
4375
+ function isContentElement(element) {
4376
+ return element.category === "content" || element.contentMetadata !== void 0;
4377
+ }
4378
+ function getContentType(element) {
4379
+ if (element.contentMetadata?.contentRole) {
4380
+ return element.contentMetadata.contentRole;
4381
+ }
4382
+ return element.type;
4383
+ }
4384
+ function detectContentChanges(fromElements, toElements) {
4385
+ const textChanges = [];
4386
+ const metricChanges = [];
4387
+ const statusChanges = [];
4388
+ for (const [id, toElement] of toElements) {
4389
+ const fromElement = fromElements.get(id);
4390
+ if (fromElement) {
4391
+ if (isContentElement(toElement) || isContentElement(fromElement)) {
4392
+ const fromText = (fromElement.state.textContent || "").trim();
4393
+ const toText = (toElement.state.textContent || "").trim();
4394
+ if (fromText !== toText) {
4395
+ const contentType = getContentType(toElement);
4396
+ const label = toElement.description || toElement.accessibleName || id;
4397
+ if (METRIC_CONTENT_TYPES.has(contentType) || contentType === "metric") {
4398
+ const parsed = parseMetricChange(fromText, toText, id, label);
4399
+ if (parsed) {
4400
+ metricChanges.push(parsed);
4401
+ }
4402
+ } else if (STATUS_CONTENT_TYPES.has(contentType) || contentType === "status") {
4403
+ statusChanges.push({
4404
+ elementId: id,
4405
+ label,
4406
+ oldStatus: fromText,
4407
+ newStatus: toText,
4408
+ direction: classifyStatusDirection(fromText, toText)
4409
+ });
4410
+ } else {
4411
+ textChanges.push({
4412
+ elementId: id,
4413
+ contentType,
4414
+ oldText: fromText,
4415
+ newText: toText,
4416
+ changeType: "modified"
4417
+ });
4418
+ }
4419
+ }
4420
+ }
4421
+ } else {
4422
+ if (isContentElement(toElement)) {
4423
+ const toText = (toElement.state.textContent || "").trim();
4424
+ if (toText) {
4425
+ textChanges.push({
4426
+ elementId: id,
4427
+ contentType: getContentType(toElement),
4428
+ oldText: "",
4429
+ newText: toText,
4430
+ changeType: "added"
4431
+ });
4432
+ }
4433
+ }
4434
+ }
4435
+ }
4436
+ for (const [id, fromElement] of fromElements) {
4437
+ if (!toElements.has(id) && isContentElement(fromElement)) {
4438
+ const fromText = (fromElement.state.textContent || "").trim();
4439
+ if (fromText) {
4440
+ textChanges.push({
4441
+ elementId: id,
4442
+ contentType: getContentType(fromElement),
4443
+ oldText: fromText,
4444
+ newText: "",
4445
+ changeType: "removed"
4446
+ });
4447
+ }
4448
+ }
4449
+ }
4450
+ if (textChanges.length === 0 && metricChanges.length === 0 && statusChanges.length === 0) {
4451
+ return null;
4452
+ }
4453
+ return {
4454
+ textChanges,
4455
+ metricChanges,
4456
+ statusChanges,
4457
+ summary: generateContentChangeSummary(textChanges, metricChanges, statusChanges)
4458
+ };
4459
+ }
4460
+ function parseNumericValue(text) {
4461
+ const trimmed = text.trim();
4462
+ if (!trimmed) return null;
4463
+ let working = trimmed;
4464
+ let negate = false;
4465
+ if (working.startsWith("(") && working.endsWith(")")) {
4466
+ working = working.slice(1, -1).trim();
4467
+ negate = true;
4468
+ }
4469
+ if (working.startsWith("-")) {
4470
+ negate = !negate;
4471
+ working = working.slice(1).trim();
4472
+ }
4473
+ if (working.startsWith("+")) {
4474
+ working = working.slice(1).trim();
4475
+ }
4476
+ working = working.replace(/^[£€¥₹$]/, "").trim();
4477
+ const isPercent = working.endsWith("%");
4478
+ if (isPercent) {
4479
+ working = working.slice(0, -1).trim();
4480
+ }
4481
+ working = working.replace(/\s*(ms|s|m|h|d|hrs?|mins?|secs?|days?)$/i, "").trim();
4482
+ working = working.replace(/,/g, "");
4483
+ const num = Number(working);
4484
+ if (isNaN(num) || !isFinite(num) || working === "") {
4485
+ return null;
4486
+ }
4487
+ return negate ? -num : num;
4488
+ }
4489
+ function parseMetricChange(fromText, toText, elementId, label) {
4490
+ const fromNum = parseNumericValue(fromText);
4491
+ const toNum = parseNumericValue(toText);
4492
+ let numericDelta;
4493
+ let percentChange;
4494
+ let significant = false;
4495
+ if (fromNum !== null && toNum !== null) {
4496
+ numericDelta = toNum - fromNum;
4497
+ if (fromNum !== 0) {
4498
+ percentChange = (toNum - fromNum) / Math.abs(fromNum) * 100;
4499
+ }
4500
+ if (percentChange !== void 0 && Math.abs(percentChange) > 10) {
4501
+ significant = true;
4502
+ }
4503
+ if (fromNum > 0 && toNum < 0) significant = true;
4504
+ if (fromNum < 0 && toNum > 0) significant = true;
4505
+ if (fromNum === 0 && toNum !== 0) significant = true;
4506
+ if (fromNum !== 0 && toNum === 0) significant = true;
4507
+ } else {
4508
+ significant = fromText !== toText;
4509
+ }
4510
+ return {
4511
+ elementId,
4512
+ label,
4513
+ oldValue: fromText,
4514
+ newValue: toText,
4515
+ numericDelta,
4516
+ percentChange: percentChange !== void 0 ? Math.round(percentChange * 100) / 100 : void 0,
4517
+ significant
4518
+ };
4519
+ }
4520
+ var STATUS_PROGRESSIONS = [
4521
+ [
4522
+ "failed",
4523
+ "error",
4524
+ "pending",
4525
+ "queued",
4526
+ "running",
4527
+ "in progress",
4528
+ "completed",
4529
+ "success",
4530
+ "done"
4531
+ ],
4532
+ ["disconnected", "connecting", "connected"],
4533
+ ["unhealthy", "degraded", "healthy"],
4534
+ ["offline", "online"],
4535
+ ["inactive", "active"],
4536
+ ["disabled", "enabled"],
4537
+ ["down", "up"],
4538
+ ["stopped", "starting", "started", "running"],
4539
+ ["closed", "open"],
4540
+ ["blocked", "unblocked"],
4541
+ ["rejected", "pending", "approved"],
4542
+ ["critical", "warning", "info", "ok"],
4543
+ ["red", "yellow", "green"]
4544
+ ];
4545
+ function classifyStatusDirection(oldStatus, newStatus) {
4546
+ const oldLower = oldStatus.toLowerCase().trim();
4547
+ const newLower = newStatus.toLowerCase().trim();
4548
+ for (const progression of STATUS_PROGRESSIONS) {
4549
+ let oldIndex = -1;
4550
+ let newIndex = -1;
4551
+ for (let i = 0; i < progression.length; i++) {
4552
+ if (oldLower.includes(progression[i])) oldIndex = i;
4553
+ if (newLower.includes(progression[i])) newIndex = i;
4554
+ }
4555
+ if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
4556
+ return newIndex > oldIndex ? "improved" : "degraded";
4557
+ }
4558
+ }
4559
+ return "neutral";
4560
+ }
4561
+ function generateContentChangeSummary(textChanges, metricChanges, statusChanges) {
4562
+ const parts = [];
4563
+ const modified = textChanges.filter((t) => t.changeType === "modified").length;
4564
+ const added = textChanges.filter((t) => t.changeType === "added").length;
4565
+ const removed = textChanges.filter((t) => t.changeType === "removed").length;
4566
+ const headingChanges = textChanges.filter(
4567
+ (t) => HEADING_CONTENT_TYPES.has(t.contentType) || t.contentType === "heading"
4568
+ );
4569
+ if (headingChanges.length > 0) {
4570
+ parts.push(`${headingChanges.length} heading${headingChanges.length > 1 ? "s" : ""} changed`);
4571
+ }
4572
+ if (metricChanges.length > 0) {
4573
+ const significantMetrics = metricChanges.filter((m) => m.significant);
4574
+ if (significantMetrics.length > 0) {
4575
+ parts.push(
4576
+ `${significantMetrics.length} metric${significantMetrics.length > 1 ? "s" : ""} changed significantly`
4577
+ );
4578
+ } else {
4579
+ parts.push(`${metricChanges.length} metric${metricChanges.length > 1 ? "s" : ""} changed`);
4580
+ }
4581
+ }
4582
+ if (statusChanges.length > 0) {
4583
+ const degraded = statusChanges.filter((s) => s.direction === "degraded");
4584
+ const improved = statusChanges.filter((s) => s.direction === "improved");
4585
+ if (degraded.length > 0) {
4586
+ parts.push(`${degraded.length} status${degraded.length > 1 ? "es" : ""} degraded`);
4587
+ }
4588
+ if (improved.length > 0) {
4589
+ parts.push(`${improved.length} status${improved.length > 1 ? "es" : ""} improved`);
4590
+ }
4591
+ const neutral = statusChanges.length - degraded.length - improved.length;
4592
+ if (neutral > 0 && degraded.length === 0 && improved.length === 0) {
4593
+ parts.push(`${neutral} status${neutral > 1 ? "es" : ""} changed`);
4594
+ }
4595
+ }
4596
+ const otherModified = modified - headingChanges.filter((h) => h.changeType === "modified").length;
4597
+ if (otherModified > 0) {
4598
+ parts.push(`${otherModified} text${otherModified > 1 ? " values" : " value"} modified`);
4599
+ }
4600
+ if (added > 0) {
4601
+ parts.push(`${added} content${added > 1 ? " elements" : " element"} added`);
4602
+ }
4603
+ if (removed > 0) {
4604
+ parts.push(`${removed} content${removed > 1 ? " elements" : " element"} removed`);
4605
+ }
4606
+ if (parts.length === 0) {
4607
+ return "No content changes";
4608
+ }
4609
+ return parts.join(", ");
4610
+ }
4611
+
4612
+ // src/ai/data-extraction.ts
4613
+ var DEFAULT_DATA_EXTRACTION_CONFIG = {
4614
+ minConfidence: 0.3,
4615
+ normalizeWhitespace: true
4616
+ };
4617
+ function classifyDataType(value) {
4618
+ const trimmed = value.trim();
4619
+ if (!trimmed) return { type: "unknown", confidence: 0 };
4620
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmed)) {
4621
+ return { type: "boolean", confidence: 0.95 };
4622
+ }
4623
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
4624
+ return { type: "email", confidence: 0.95 };
4625
+ }
4626
+ if (/^https?:\/\/\S+/.test(trimmed)) {
4627
+ return { type: "url", confidence: 0.95 };
4628
+ }
4629
+ if (/^[+]?[\d\s\-().]{7,20}$/.test(trimmed) && /\d{3,}/.test(trimmed)) {
4630
+ return { type: "phone", confidence: 0.7 };
4631
+ }
4632
+ if (/^[£$€¥₹][\s]?[\d,.]+$/.test(trimmed) || /^[\d,.]+[\s]?[£$€¥₹]$/.test(trimmed)) {
4633
+ return { type: "currency", confidence: 0.9 };
4634
+ }
4635
+ if (/^[\d,.]+\s?%$/.test(trimmed)) {
4636
+ return { type: "percentage", confidence: 0.95 };
4637
+ }
4638
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed) || /^\d{1,2}\/\d{1,2}\/\d{2,4}$/.test(trimmed) || /^\d{1,2}\.\d{1,2}\.\d{2,4}$/.test(trimmed) || /^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) {
4639
+ return { type: "date", confidence: 0.85 };
4640
+ }
4641
+ if (/^-?[\d,]+\.?\d*$/.test(trimmed) && trimmed !== "") {
4642
+ return { type: "number", confidence: 0.9 };
4643
+ }
4644
+ return { type: "text", confidence: 0.5 };
4645
+ }
4646
+ function normalizeValue(value, dataType) {
4647
+ const trimmed = value.trim();
4648
+ switch (dataType) {
4649
+ case "number":
4650
+ case "currency":
4651
+ case "percentage": {
4652
+ const numeric = trimmed.replace(/[^0-9.-]/g, "");
4653
+ const parsed = parseFloat(numeric);
4654
+ return isNaN(parsed) ? trimmed.toLowerCase() : parsed.toString();
4655
+ }
4656
+ case "date": {
4657
+ const d = new Date(trimmed);
4658
+ return isNaN(d.getTime()) ? trimmed.toLowerCase() : d.toISOString().split("T")[0];
4659
+ }
4660
+ case "boolean":
4661
+ return /^(true|yes|on)$/i.test(trimmed) ? "true" : "false";
4662
+ case "email":
4663
+ return trimmed.toLowerCase();
4664
+ case "url":
4665
+ return trimmed.replace(/\/+$/, "").toLowerCase();
4666
+ case "phone":
4667
+ return trimmed.replace(/[^\d+]/g, "");
4668
+ default:
4669
+ return trimmed.toLowerCase().replace(/\s+/g, " ");
4670
+ }
4671
+ }
4672
+ function extractElementValue(element) {
4673
+ const state = element.state;
4674
+ if (state?.value !== void 0 && state.value !== "") {
4675
+ return String(state.value);
4676
+ }
4677
+ if (state?.textContent !== void 0 && state.textContent !== "") {
4678
+ return String(state.textContent);
4679
+ }
4680
+ return "";
4681
+ }
4682
+ function extractLabel(element) {
4683
+ return element.accessibleName || element.labelText || element.label || element.description || element.id;
4684
+ }
4685
+ function extractPageData(elements, config = DEFAULT_DATA_EXTRACTION_CONFIG) {
4686
+ const values = {};
4687
+ let extractedCount = 0;
4688
+ for (const element of elements) {
4689
+ const rawValue = extractElementValue(element);
4690
+ if (!rawValue) continue;
4691
+ const label = extractLabel(element);
4692
+ const { type: dataType, confidence } = classifyDataType(rawValue);
4693
+ if (confidence < config.minConfidence) continue;
4694
+ const normalizedValue = normalizeValue(rawValue, dataType);
4695
+ values[label] = {
4696
+ elementId: element.id,
4697
+ label,
4698
+ rawValue: config.normalizeWhitespace ? rawValue.replace(/\s+/g, " ").trim() : rawValue,
4699
+ normalizedValue,
4700
+ dataType,
4701
+ confidence
4702
+ };
4703
+ extractedCount++;
4704
+ }
4705
+ return {
4706
+ values,
4707
+ scannedCount: elements.length,
4708
+ extractedCount
4709
+ };
4710
+ }
4711
+
4712
+ // src/ai/region-segmentation.ts
4713
+ var DEFAULT_REGION_SEGMENTATION_CONFIG = {
4714
+ minRegionElements: 1,
4715
+ headerFraction: 0.12,
4716
+ footerFraction: 0.9,
4717
+ sidebarFraction: 0.2
4718
+ };
4719
+ function toBounded(el) {
4720
+ const rect = el.state?.rect;
4721
+ if (!rect) return null;
4722
+ return {
4723
+ element: el,
4724
+ x: rect.x ?? 0,
4725
+ y: rect.y ?? 0,
4726
+ width: rect.width ?? 0,
4727
+ height: rect.height ?? 0
4728
+ };
4729
+ }
4730
+ function classifyRegionType(el, relativeY, relativeX, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4731
+ const role = (el.role || "").toLowerCase();
4732
+ const semanticType = (el.semanticType || "").toLowerCase();
4733
+ const tag = (el.tagName || "").toLowerCase();
4734
+ if (role === "navigation" || role === "nav" || tag === "nav") {
4735
+ return { type: "navigation", confidence: 0.95 };
4736
+ }
4737
+ if (role === "banner" || tag === "header") {
4738
+ return { type: "header", confidence: 0.95 };
4739
+ }
4740
+ if (role === "contentinfo" || tag === "footer") {
4741
+ return { type: "footer", confidence: 0.95 };
4742
+ }
4743
+ if (role === "main" || tag === "main") {
4744
+ return { type: "main-content", confidence: 0.95 };
4745
+ }
4746
+ if (role === "complementary" || tag === "aside") {
4747
+ return { type: "sidebar", confidence: 0.9 };
4748
+ }
4749
+ if (role === "form" || tag === "form") {
4750
+ return { type: "form", confidence: 0.9 };
4751
+ }
4752
+ if (role === "table" || tag === "table") {
4753
+ return { type: "table", confidence: 0.9 };
4754
+ }
4755
+ if (role === "dialog" || role === "alertdialog") {
4756
+ return { type: "modal", confidence: 0.95 };
4757
+ }
4758
+ if (role === "toolbar") {
4759
+ return { type: "toolbar", confidence: 0.9 };
4760
+ }
4761
+ if (semanticType.includes("card")) {
4762
+ return { type: "card", confidence: 0.8 };
4763
+ }
4764
+ if (relativeY < config.headerFraction) {
4765
+ return { type: "header", confidence: 0.6 };
4766
+ }
4767
+ if (relativeY > config.footerFraction) {
4768
+ return { type: "footer", confidence: 0.6 };
4769
+ }
4770
+ if (relativeX < config.sidebarFraction) {
4771
+ return { type: "sidebar", confidence: 0.5 };
4772
+ }
4773
+ return { type: "main-content", confidence: 0.3 };
4774
+ }
4775
+ function segmentPageRegions(elements, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4776
+ const bounded = elements.map(toBounded).filter((b) => b !== null);
4777
+ if (bounded.length === 0) {
4778
+ return { regions: [], assignedCount: 0, unassignedIds: elements.map((e) => e.id) };
4779
+ }
4780
+ let maxX = 0;
4781
+ let maxY = 0;
4782
+ for (const b of bounded) {
4783
+ maxX = Math.max(maxX, b.x + b.width);
4784
+ maxY = Math.max(maxY, b.y + b.height);
4785
+ }
4786
+ if (maxX === 0) maxX = 1;
4787
+ if (maxY === 0) maxY = 1;
4788
+ const regionGroups = /* @__PURE__ */ new Map();
4789
+ const unassignedIds = [];
4790
+ for (const b of bounded) {
4791
+ const relativeX = b.x / maxX;
4792
+ const relativeY = b.y / maxY;
4793
+ const { type, confidence } = classifyRegionType(b.element, relativeY, relativeX, config);
4794
+ if (!regionGroups.has(type)) {
4795
+ regionGroups.set(type, { elements: [], confidences: [] });
4796
+ }
4797
+ regionGroups.get(type).elements.push(b);
4798
+ regionGroups.get(type).confidences.push(confidence);
4799
+ }
4800
+ const regions = [];
4801
+ let assignedCount = 0;
4802
+ for (const [type, group] of regionGroups) {
4803
+ if (group.elements.length < config.minRegionElements) {
4804
+ for (const b of group.elements) unassignedIds.push(b.element.id);
4805
+ continue;
4806
+ }
4807
+ let minX = Infinity, minY = Infinity, maxRX = 0, maxRY = 0;
4808
+ const elementIds = [];
4809
+ for (const b of group.elements) {
4810
+ minX = Math.min(minX, b.x);
4811
+ minY = Math.min(minY, b.y);
4812
+ maxRX = Math.max(maxRX, b.x + b.width);
4813
+ maxRY = Math.max(maxRY, b.y + b.height);
4814
+ elementIds.push(b.element.id);
4815
+ }
4816
+ const avgConfidence = group.confidences.reduce((a, b) => a + b, 0) / group.confidences.length;
4817
+ regions.push({
4818
+ type,
4819
+ bounds: { x: minX, y: minY, width: maxRX - minX, height: maxRY - minY },
4820
+ elementIds,
4821
+ label: type.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
4822
+ confidence: Math.round(avgConfidence * 100) / 100
4823
+ });
4824
+ assignedCount += elementIds.length;
4825
+ }
4826
+ return { regions, assignedCount, unassignedIds };
4827
+ }
4828
+
4829
+ // src/ai/table-extraction.ts
4830
+ var DEFAULT_TABLE_EXTRACTION_CONFIG = {
4831
+ minTableColumns: 2,
4832
+ minTableRows: 2,
4833
+ minListItems: 2,
4834
+ columnTolerance: 20,
4835
+ rowTolerance: 10
4836
+ };
4837
+ function getElementBounds(el) {
4838
+ const rect = el.state?.rect;
4839
+ if (!rect || rect.width === 0) return null;
4840
+ const text = el.state?.textContent ?? el.state?.value ?? "";
4841
+ if (!text) return null;
4842
+ return {
4843
+ element: el,
4844
+ x: rect.x ?? 0,
4845
+ y: rect.y ?? 0,
4846
+ width: rect.width ?? 0,
4847
+ height: rect.height ?? 0,
4848
+ text: text.trim()
4849
+ };
4850
+ }
4851
+ function clusterPositions(values, tolerance) {
4852
+ if (values.length === 0) return [];
4853
+ const sorted = [...values].sort((a, b) => a - b);
4854
+ const clusters = [sorted[0]];
4855
+ for (let i = 1; i < sorted.length; i++) {
4856
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
4857
+ clusters.push(sorted[i]);
4858
+ }
4859
+ }
4860
+ return clusters;
4861
+ }
4862
+ function assignToCluster(value, clusters, tolerance) {
4863
+ let best = 0;
4864
+ let bestDist = Math.abs(value - clusters[0]);
4865
+ for (let i = 1; i < clusters.length; i++) {
4866
+ const dist = Math.abs(value - clusters[i]);
4867
+ if (dist < bestDist) {
4868
+ bestDist = dist;
4869
+ best = i;
4870
+ }
4871
+ }
4872
+ return bestDist <= tolerance ? best : -1;
4873
+ }
4874
+ function detectTable(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4875
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4876
+ if (withBounds.length < config.minTableColumns * config.minTableRows) return null;
4877
+ const xPositions = withBounds.map((b) => b.x);
4878
+ const yPositions = withBounds.map((b) => b.y);
4879
+ const columnClusters = clusterPositions(xPositions, config.columnTolerance);
4880
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4881
+ if (columnClusters.length < config.minTableColumns || rowClusters.length < config.minTableRows) {
4882
+ return null;
4883
+ }
4884
+ const grid = Array.from(
4885
+ { length: rowClusters.length },
4886
+ () => Array(columnClusters.length).fill(null)
4887
+ );
4888
+ for (const b of withBounds) {
4889
+ const col = assignToCluster(b.x, columnClusters, config.columnTolerance);
4890
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4891
+ if (col >= 0 && row >= 0 && grid[row][col] === null) {
4892
+ grid[row][col] = b.text;
4893
+ }
4894
+ }
4895
+ const headers = grid[0].map((h) => h ?? "");
4896
+ const columns = headers.map((header, index) => {
4897
+ const bodyCells = grid.slice(1).map((r) => r[index]).filter((c) => c !== null);
4898
+ const types = bodyCells.map((c) => classifyDataType(c).type);
4899
+ const mostCommon = mode(types) ?? "text";
4900
+ return { header, index, dataType: mostCommon };
4901
+ });
4902
+ const rows = grid.slice(1).map((row) => row.map((cell) => cell ?? ""));
4903
+ return {
4904
+ label: headers[0] || "Table",
4905
+ columns,
4906
+ rows
4907
+ };
4908
+ }
4909
+ function detectList(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4910
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4911
+ if (withBounds.length < config.minListItems) return null;
4912
+ const sorted = [...withBounds].sort((a, b) => a.y - b.y);
4913
+ const yPositions = sorted.map((b) => b.y);
4914
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4915
+ if (rowClusters.length < config.minListItems) return null;
4916
+ const rowGroups = /* @__PURE__ */ new Map();
4917
+ for (const b of sorted) {
4918
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4919
+ if (row >= 0) {
4920
+ if (!rowGroups.has(row)) rowGroups.set(row, []);
4921
+ rowGroups.get(row).push(b);
4922
+ }
4923
+ }
4924
+ const items = [];
4925
+ const fieldLabels = [];
4926
+ let fieldLabelsInitialized = false;
4927
+ for (const [, rowElements] of [...rowGroups.entries()].sort(([a], [b]) => a - b)) {
4928
+ const sortedRow = [...rowElements].sort((a, b) => a.x - b.x);
4929
+ const item = {};
4930
+ for (let i = 0; i < sortedRow.length; i++) {
4931
+ const label = `field_${i}`;
4932
+ if (!fieldLabelsInitialized) fieldLabels.push(label);
4933
+ item[label] = sortedRow[i].text;
4934
+ }
4935
+ fieldLabelsInitialized = true;
4936
+ items.push(item);
4937
+ }
4938
+ if (items.length < config.minListItems) return null;
4939
+ const fields = fieldLabels.map((label) => {
4940
+ const values = items.map((item) => item[label]).filter(Boolean);
4941
+ const types = values.map((v) => classifyDataType(v).type);
4942
+ return { label, dataType: mode(types) ?? "text" };
4943
+ });
4944
+ return {
4945
+ label: "List",
4946
+ fields,
4947
+ items
4948
+ };
4949
+ }
4950
+ function extractStructuredData(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4951
+ const tables = [];
4952
+ const lists = [];
4953
+ const table = detectTable(elements, config);
4954
+ if (table) {
4955
+ tables.push(table);
4956
+ }
4957
+ const listCandidates = elements.filter((el) => {
4958
+ const role = el.role || el.type;
4959
+ return ["listitem", "row", "option", "link", "button"].includes(role);
4960
+ });
4961
+ if (listCandidates.length >= config.minListItems) {
4962
+ const list = detectList(listCandidates, config);
4963
+ if (list) {
4964
+ lists.push(list);
4965
+ }
4966
+ }
4967
+ return { tables, lists };
4968
+ }
4969
+ function mode(arr) {
4970
+ if (arr.length === 0) return void 0;
4971
+ const counts = /* @__PURE__ */ new Map();
4972
+ let best = arr[0];
4973
+ let bestCount = 0;
4974
+ for (const v of arr) {
4975
+ const c = (counts.get(v) ?? 0) + 1;
4976
+ counts.set(v, c);
4977
+ if (c > bestCount) {
4978
+ bestCount = c;
4979
+ best = v;
4980
+ }
4981
+ }
4982
+ return best;
4983
+ }
4984
+
4985
+ // src/ai/format-analysis.ts
4986
+ var DEFAULT_FORMAT_ANALYSIS_CONFIG = {
4987
+ lenientFormatting: true
4988
+ };
4989
+ function detectFormatPattern(value, dataType) {
4990
+ const trimmed = value.trim();
4991
+ switch (dataType) {
4992
+ case "currency": {
4993
+ const hasLeadingSymbol = /^[£$€¥₹]/.test(trimmed);
4994
+ const hasTrailingSymbol = /[£$€¥₹]$/.test(trimmed);
4995
+ const usesCommaThousands = /\d{1,3}(,\d{3})+/.test(trimmed);
4996
+ const usesPeriodThousands = /\d{1,3}(\.\d{3})+,/.test(trimmed);
4997
+ let pattern = hasLeadingSymbol ? "$" : "";
4998
+ if (usesCommaThousands) pattern += "#,###";
4999
+ else if (usesPeriodThousands) pattern += "#.###";
5000
+ else pattern += "#";
5001
+ if (/\.\d{2}$/.test(trimmed)) pattern += ".##";
5002
+ else if (/,\d{2}$/.test(trimmed)) pattern += ",##";
5003
+ if (hasTrailingSymbol) pattern += "$";
5004
+ return pattern;
5005
+ }
5006
+ case "date": {
5007
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return "YYYY-MM-DD";
5008
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(trimmed)) return "MM/DD/YYYY";
5009
+ if (/^\d{2}\.\d{2}\.\d{4}$/.test(trimmed)) return "DD.MM.YYYY";
5010
+ if (/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(trimmed)) return "M/D/YY";
5011
+ if (/^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) return "Month DD, YYYY";
5012
+ return "date";
5013
+ }
5014
+ case "percentage":
5015
+ return /\s%$/.test(trimmed) ? "#.## %" : "#.##%";
5016
+ case "number": {
5017
+ const hasCommas = /,/.test(trimmed);
5018
+ const decimalPlaces = trimmed.includes(".") ? trimmed.split(".")[1]?.length || 0 : 0;
5019
+ return (hasCommas ? "#,###" : "#") + (decimalPlaces > 0 ? "." + "#".repeat(decimalPlaces) : "");
5020
+ }
5021
+ case "phone": {
5022
+ if (/^\(\d{3}\)\s?\d{3}-\d{4}$/.test(trimmed)) return "(###) ###-####";
5023
+ if (/^\d{3}-\d{3}-\d{4}$/.test(trimmed)) return "###-###-####";
5024
+ if (/^\+\d/.test(trimmed)) return "+# ###...";
5025
+ return "phone";
5026
+ }
5027
+ default:
5028
+ return dataType;
5029
+ }
5030
+ }
5031
+ function analyzeFormat(elementId, label, rawValue) {
5032
+ const { type: dataType } = classifyDataType(rawValue);
5033
+ const pattern = detectFormatPattern(rawValue, dataType);
5034
+ return {
5035
+ elementId,
5036
+ label,
5037
+ dataType,
5038
+ pattern,
5039
+ example: rawValue.trim()
5040
+ };
5041
+ }
5042
+ function analyzePageFormats(elements) {
5043
+ const descriptors = [];
5044
+ for (const el of elements) {
5045
+ const rawValue = el.state?.value ?? el.state?.textContent ?? "";
5046
+ if (!rawValue) continue;
5047
+ const label = el.accessibleName || el.labelText || el.label || el.description || el.id;
5048
+ descriptors.push(analyzeFormat(el.id, label, rawValue));
5049
+ }
5050
+ return descriptors;
5051
+ }
5052
+ function compareFormats(sourceFormats, targetFormats, config = DEFAULT_FORMAT_ANALYSIS_CONFIG) {
5053
+ const mismatches = [];
5054
+ const targetByLabel = /* @__PURE__ */ new Map();
5055
+ for (const t of targetFormats) {
5056
+ targetByLabel.set(t.label.toLowerCase(), t);
5057
+ }
5058
+ for (const source of sourceFormats) {
5059
+ const target = targetByLabel.get(source.label.toLowerCase());
5060
+ if (!target) continue;
5061
+ if (source.dataType !== target.dataType) {
5062
+ mismatches.push({
5063
+ label: source.label,
5064
+ sourceFormat: source,
5065
+ targetFormat: target,
5066
+ severity: "error",
5067
+ description: `Data type mismatch: source is ${source.dataType}, target is ${target.dataType}`
5068
+ });
5069
+ continue;
5070
+ }
5071
+ if (source.pattern !== target.pattern) {
5072
+ const severity = config.lenientFormatting ? "warning" : "error";
5073
+ mismatches.push({
5074
+ label: source.label,
5075
+ sourceFormat: source,
5076
+ targetFormat: target,
5077
+ severity,
5078
+ description: `Format differs: source uses "${source.pattern}", target uses "${target.pattern}"`
5079
+ });
5080
+ }
5081
+ }
5082
+ return mismatches;
5083
+ }
5084
+
5085
+ // src/ai/cross-app-diff.ts
5086
+ var DEFAULT_CROSS_APP_DIFF_CONFIG = {
5087
+ matchThreshold: 0.5,
5088
+ accessibleNameWeight: 1,
5089
+ textWeight: 0.95,
5090
+ rolePositionWeight: 0.7
5091
+ };
5092
+ function getElementText(el) {
5093
+ return el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "";
5094
+ }
5095
+ function getRole(el) {
5096
+ return (el.role || el.type || "").toLowerCase();
5097
+ }
5098
+ function getCenter(el) {
5099
+ const rect = el.state?.rect;
5100
+ if (!rect) return null;
5101
+ return {
5102
+ x: rect.x + rect.width / 2,
5103
+ y: rect.y + rect.height / 2
5104
+ };
5105
+ }
5106
+ function computeMatchScore(source, target, config) {
5107
+ let bestScore = 0;
5108
+ let bestStrategy = "none";
5109
+ const srcName = (source.accessibleName || "").trim();
5110
+ const tgtName = (target.accessibleName || "").trim();
5111
+ if (srcName && tgtName && srcName.toLowerCase() === tgtName.toLowerCase()) {
5112
+ return { score: config.accessibleNameWeight, strategy: "accessible-name-exact" };
5113
+ }
5114
+ const srcText = getElementText(source);
5115
+ const tgtText = getElementText(target);
5116
+ if (srcText && tgtText && srcText.toLowerCase() === tgtText.toLowerCase()) {
5117
+ const score = config.textWeight;
5118
+ if (score > bestScore) {
5119
+ bestScore = score;
5120
+ bestStrategy = "text-exact";
5121
+ }
5122
+ }
5123
+ if (srcText && tgtText) {
5124
+ const srcNorm = normalizeString(srcText);
5125
+ const tgtNorm = normalizeString(tgtText);
5126
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
5127
+ const score = similarity * 0.85;
5128
+ if (score > bestScore) {
5129
+ bestScore = score;
5130
+ bestStrategy = "text-fuzzy";
5131
+ }
5132
+ }
5133
+ const srcRole = getRole(source);
5134
+ const tgtRole = getRole(target);
5135
+ if (srcRole && srcRole === tgtRole) {
5136
+ const srcCenter = getCenter(source);
5137
+ const tgtCenter = getCenter(target);
5138
+ if (srcCenter && tgtCenter) {
5139
+ const dx = Math.abs(srcCenter.x - tgtCenter.x) / 1920;
5140
+ const dy = Math.abs(srcCenter.y - tgtCenter.y) / 1080;
5141
+ const posSimilarity = 1 - Math.min(1, Math.sqrt(dx * dx + dy * dy));
5142
+ const score = config.rolePositionWeight * posSimilarity;
5143
+ if (score > bestScore) {
5144
+ bestScore = score;
5145
+ bestStrategy = "role-position";
5146
+ }
5147
+ }
5148
+ }
5149
+ const srcVal = source.state?.value ?? source.state?.textContent ?? "";
5150
+ const tgtVal = target.state?.value ?? target.state?.textContent ?? "";
5151
+ if (srcVal && tgtVal) {
5152
+ const srcType = classifyDataType(srcVal).type;
5153
+ const tgtType = classifyDataType(tgtVal).type;
5154
+ const srcNorm = normalizeValue(srcVal, srcType);
5155
+ const tgtNorm = normalizeValue(tgtVal, tgtType);
5156
+ if (srcNorm === tgtNorm && srcNorm !== "") {
5157
+ const score = 0.6;
5158
+ if (score > bestScore) {
5159
+ bestScore = score;
5160
+ bestStrategy = "data-overlap";
5161
+ }
5162
+ }
5163
+ }
5164
+ return { score: bestScore, strategy: bestStrategy };
5165
+ }
5166
+ function matchElements(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
5167
+ const candidates = [];
5168
+ for (let si = 0; si < sourceElements.length; si++) {
5169
+ for (let ti = 0; ti < targetElements.length; ti++) {
5170
+ const { score, strategy } = computeMatchScore(sourceElements[si], targetElements[ti], config);
5171
+ if (score >= config.matchThreshold) {
5172
+ candidates.push({ sourceIdx: si, targetIdx: ti, score, strategy });
5173
+ }
5174
+ }
5175
+ }
5176
+ candidates.sort((a, b) => b.score - a.score);
5177
+ const usedSource = /* @__PURE__ */ new Set();
5178
+ const usedTarget = /* @__PURE__ */ new Set();
5179
+ const pairs = [];
5180
+ for (const c of candidates) {
5181
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5182
+ usedSource.add(c.sourceIdx);
5183
+ usedTarget.add(c.targetIdx);
5184
+ const src = sourceElements[c.sourceIdx];
5185
+ const tgt = targetElements[c.targetIdx];
5186
+ pairs.push({
5187
+ sourceId: src.id,
5188
+ targetId: tgt.id,
5189
+ sourceLabel: getElementText(src) || src.id,
5190
+ targetLabel: getElementText(tgt) || tgt.id,
5191
+ confidence: Math.round(c.score * 100) / 100,
5192
+ matchStrategy: c.strategy
5193
+ });
5194
+ }
5195
+ return pairs;
5196
+ }
5197
+ function computeCrossAppDiff(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
5198
+ const matchedPairs = matchElements(sourceElements, targetElements, config);
5199
+ const matchedSourceIds = new Set(matchedPairs.map((p) => p.sourceId));
5200
+ const matchedTargetIds = new Set(matchedPairs.map((p) => p.targetId));
5201
+ const unmatchedSourceIds = sourceElements.filter((e) => !matchedSourceIds.has(e.id)).map((e) => e.id);
5202
+ const unmatchedTargetIds = targetElements.filter((e) => !matchedTargetIds.has(e.id)).map((e) => e.id);
5203
+ const sourceData = extractPageData(sourceElements);
5204
+ const targetData = extractPageData(targetElements);
5205
+ const dataComparisons = [];
5206
+ for (const pair of matchedPairs) {
5207
+ const srcEntry = Object.values(sourceData.values).find((v) => v.elementId === pair.sourceId);
5208
+ const tgtEntry = Object.values(targetData.values).find((v) => v.elementId === pair.targetId);
5209
+ if (srcEntry && tgtEntry) {
5210
+ dataComparisons.push({
5211
+ label: pair.sourceLabel,
5212
+ sourceValue: srcEntry.rawValue,
5213
+ targetValue: tgtEntry.rawValue,
5214
+ valuesMatch: srcEntry.normalizedValue === tgtEntry.normalizedValue,
5215
+ formatsMatch: srcEntry.dataType === tgtEntry.dataType
5216
+ });
5217
+ }
5218
+ }
5219
+ const sourceFormats = analyzePageFormats(sourceElements);
5220
+ const targetFormats = analyzePageFormats(targetElements);
5221
+ const formatMismatches = compareFormats(sourceFormats, targetFormats);
5222
+ return {
5223
+ matchedPairs,
5224
+ unmatchedSourceIds,
5225
+ unmatchedTargetIds,
5226
+ dataComparisons,
5227
+ formatMismatches
5228
+ };
5229
+ }
5230
+
5231
+ // src/ai/action-parity.ts
5232
+ var DEFAULT_ACTION_PARITY_CONFIG = {
5233
+ ignoreActions: []
5234
+ };
5235
+ function getActions(el, ignoreActions) {
5236
+ const actions = el.actions || el.suggestedActions || [];
5237
+ const ignoreSet = new Set(ignoreActions.map((a) => a.toLowerCase()));
5238
+ return actions.map(
5239
+ (a) => typeof a === "string" ? a : a.action || a.name || ""
5240
+ ).filter((a) => a && !ignoreSet.has(a.toLowerCase()));
5241
+ }
5242
+ function analyzeActionParity(matchedPairs, sourceElements, targetElements, config = DEFAULT_ACTION_PARITY_CONFIG) {
5243
+ const sourceById = new Map(sourceElements.map((e) => [e.id, e]));
5244
+ const targetById = new Map(targetElements.map((e) => [e.id, e]));
5245
+ const results = [];
5246
+ for (const pair of matchedPairs) {
5247
+ const src = sourceById.get(pair.sourceId);
5248
+ const tgt = targetById.get(pair.targetId);
5249
+ if (!src || !tgt) continue;
5250
+ const sourceActions = getActions(src, config.ignoreActions);
5251
+ const targetActions = getActions(tgt, config.ignoreActions);
5252
+ const sourceSet = new Set(sourceActions.map((a) => a.toLowerCase()));
5253
+ const targetSet = new Set(targetActions.map((a) => a.toLowerCase()));
5254
+ const missingInTarget = sourceActions.filter((a) => !targetSet.has(a.toLowerCase()));
5255
+ const missingInSource = targetActions.filter((a) => !sourceSet.has(a.toLowerCase()));
5256
+ results.push({
5257
+ pair,
5258
+ sourceActions,
5259
+ targetActions,
5260
+ missingInTarget,
5261
+ missingInSource
5262
+ });
5263
+ }
5264
+ return results;
5265
+ }
5266
+
5267
+ // src/ai/navigation-map.ts
5268
+ var DEFAULT_NAVIGATION_MAP_CONFIG = {
5269
+ labelMatchThreshold: 0.8
5270
+ };
5271
+ function isNavigationElement(el) {
5272
+ const role = (el.role || "").toLowerCase();
5273
+ const type = (el.type || "").toLowerCase();
5274
+ const semanticType = (el.semanticType || "").toLowerCase();
5275
+ if (["link", "menuitem", "tab"].includes(role)) return true;
5276
+ if (["link", "menuitem"].includes(type)) return true;
5277
+ if (semanticType.includes("nav") || semanticType.includes("menu") || semanticType.includes("tab")) {
5278
+ return true;
5279
+ }
5280
+ const context = (el.parentContext || "").toLowerCase();
5281
+ if (context.includes("nav") || context.includes("menu") || context.includes("sidebar")) {
5282
+ if (role === "button" || type === "button" || role === "link" || type === "link") {
5283
+ return true;
5284
+ }
5285
+ }
5286
+ return false;
5287
+ }
5288
+ function getNavLabel(el) {
5289
+ return el.accessibleName || el.labelText || el.label || el.description || el.id;
5290
+ }
5291
+ function getHref(el) {
5292
+ const state = el.state;
5293
+ return state?.href || void 0;
5294
+ }
5295
+ function hrefsMatch(a, b) {
5296
+ if (!a || !b) return false;
5297
+ const normalize = (h) => h.replace(/^https?:\/\//, "").replace(/localhost:\d+/, "").replace(/\/+$/, "").toLowerCase();
5298
+ return normalize(a) === normalize(b);
5299
+ }
5300
+ function buildNavigationMap(sourceElements, targetElements, config = DEFAULT_NAVIGATION_MAP_CONFIG) {
5301
+ const sourceNav = sourceElements.filter(isNavigationElement);
5302
+ const targetNav = targetElements.filter(isNavigationElement);
5303
+ const pairs = [];
5304
+ const matchedTargetIds = /* @__PURE__ */ new Set();
5305
+ for (const src of sourceNav) {
5306
+ const srcLabel = getNavLabel(src);
5307
+ const srcNorm = normalizeString(srcLabel);
5308
+ let bestTarget = null;
5309
+ let bestScore = 0;
5310
+ for (const tgt of targetNav) {
5311
+ if (matchedTargetIds.has(tgt.id)) continue;
5312
+ const tgtLabel = getNavLabel(tgt);
5313
+ const tgtNorm = normalizeString(tgtLabel);
5314
+ if (srcNorm === tgtNorm) {
5315
+ bestTarget = tgt;
5316
+ bestScore = 1;
5317
+ break;
5318
+ }
5319
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
5320
+ if (similarity > bestScore && similarity >= config.labelMatchThreshold) {
5321
+ bestScore = similarity;
5322
+ bestTarget = tgt;
5323
+ }
5324
+ }
5325
+ if (bestTarget) {
5326
+ matchedTargetIds.add(bestTarget.id);
5327
+ const srcHref = getHref(src);
5328
+ const tgtHref = getHref(bestTarget);
5329
+ pairs.push({
5330
+ sourceId: src.id,
5331
+ targetId: bestTarget.id,
5332
+ label: srcLabel,
5333
+ sourceHref: srcHref,
5334
+ targetHref: tgtHref,
5335
+ destinationMatch: hrefsMatch(srcHref, tgtHref)
5336
+ });
5337
+ }
5338
+ }
5339
+ const sourceOnly = sourceNav.filter((s) => !pairs.some((p) => p.sourceId === s.id)).map((s) => s.id);
5340
+ const targetOnly = targetNav.filter((t) => !matchedTargetIds.has(t.id)).map((t) => t.id);
5341
+ return { pairs, sourceOnly, targetOnly };
5342
+ }
5343
+
5344
+ // src/ai/component-comparison.ts
5345
+ var DEFAULT_COMPONENT_COMPARISON_CONFIG = {
5346
+ nameMatchThreshold: 0.75
5347
+ };
5348
+ function computeComponentMatchScore(source, target) {
5349
+ if (source.name.toLowerCase() === target.name.toLowerCase()) return 1;
5350
+ let score = 0;
5351
+ if (source.type === target.type) {
5352
+ score += 0.3;
5353
+ }
5354
+ const nameSimilarity = jaroWinklerSimilarity(
5355
+ normalizeString(source.name),
5356
+ normalizeString(target.name)
5357
+ );
5358
+ score += nameSimilarity * 0.7;
5359
+ return score;
5360
+ }
5361
+ function compareComponents(sourceComponents, targetComponents, config = DEFAULT_COMPONENT_COMPARISON_CONFIG) {
5362
+ const candidates = [];
5363
+ for (let si = 0; si < sourceComponents.length; si++) {
5364
+ for (let ti = 0; ti < targetComponents.length; ti++) {
5365
+ const score = computeComponentMatchScore(sourceComponents[si], targetComponents[ti]);
5366
+ if (score >= config.nameMatchThreshold) {
5367
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5368
+ }
5369
+ }
5370
+ }
5371
+ candidates.sort((a, b) => b.score - a.score);
5372
+ const usedSource = /* @__PURE__ */ new Set();
5373
+ const usedTarget = /* @__PURE__ */ new Set();
5374
+ const matches = [];
5375
+ for (const c of candidates) {
5376
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5377
+ usedSource.add(c.sourceIdx);
5378
+ usedTarget.add(c.targetIdx);
5379
+ const src = sourceComponents[c.sourceIdx];
5380
+ const tgt = targetComponents[c.targetIdx];
5381
+ const srcKeys = new Set(src.stateKeys);
5382
+ const tgtKeys = new Set(tgt.stateKeys);
5383
+ const missingKeys = src.stateKeys.filter((k) => !tgtKeys.has(k));
5384
+ const extraKeys = tgt.stateKeys.filter((k) => !srcKeys.has(k));
5385
+ const srcActions = new Set(src.actions.map((a) => a.toLowerCase()));
5386
+ const tgtActions = new Set(tgt.actions.map((a) => a.toLowerCase()));
5387
+ const missingActions = src.actions.filter((a) => !tgtActions.has(a.toLowerCase()));
5388
+ const extraActions = tgt.actions.filter((a) => !srcActions.has(a.toLowerCase()));
5389
+ matches.push({
5390
+ source: src,
5391
+ target: tgt,
5392
+ confidence: Math.round(c.score * 100) / 100,
5393
+ stateKeyDiff: { missing: missingKeys, extra: extraKeys },
5394
+ actionDiff: { missing: missingActions, extra: extraActions }
5395
+ });
5396
+ }
5397
+ const sourceOnly = sourceComponents.filter((_, i) => !usedSource.has(i));
5398
+ const targetOnly = targetComponents.filter((_, i) => !usedTarget.has(i));
5399
+ return { matches, sourceOnly, targetOnly };
5400
+ }
5401
+
5402
+ // src/ai/layout-comparison.ts
5403
+ var DEFAULT_LAYOUT_COMPARISON_CONFIG = {
5404
+ gridTolerance: 20
5405
+ };
5406
+ function getRect(el) {
5407
+ const rect = el.state?.rect;
5408
+ if (!rect || !rect.width) return null;
5409
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
5410
+ }
5411
+ function clusterValues(values, tolerance) {
5412
+ if (values.length === 0) return [];
5413
+ const sorted = [...values].sort((a, b) => a - b);
5414
+ const clusters = [sorted[0]];
5415
+ for (let i = 1; i < sorted.length; i++) {
5416
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
5417
+ clusters.push(sorted[i]);
5418
+ }
5419
+ }
5420
+ return clusters;
5421
+ }
5422
+ function detectGridStructure(elements, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5423
+ const rects = elements.map(getRect).filter((r) => r !== null);
5424
+ const xPositions = rects.map((r) => r.x);
5425
+ const yPositions = rects.map((r) => r.y);
5426
+ const columns = clusterValues(xPositions, config.gridTolerance);
5427
+ const rows = clusterValues(yPositions, config.gridTolerance);
5428
+ return {
5429
+ columns,
5430
+ rows,
5431
+ columnCount: columns.length,
5432
+ rowCount: rows.length
5433
+ };
5434
+ }
5435
+ function computeMaxDepth(elements) {
5436
+ let maxDepth = 0;
5437
+ for (const el of elements) {
5438
+ const context = el.parentContext || "";
5439
+ const depth = context ? context.split(">").length : 1;
5440
+ maxDepth = Math.max(maxDepth, depth);
5441
+ }
5442
+ return maxDepth;
5443
+ }
5444
+ function computeProminence(element, pageWidth, pageHeight) {
5445
+ const rect = getRect(element);
5446
+ if (!rect || pageWidth === 0 || pageHeight === 0) return 0;
5447
+ const sizeScore = rect.width * rect.height / (pageWidth * pageHeight);
5448
+ const positionScore = 1 - rect.y / pageHeight;
5449
+ return Math.min(1, sizeScore * 0.6 + positionScore * 0.4);
5450
+ }
5451
+ function compareLayouts(sourceElements, targetElements, sourceRegions, targetRegions, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5452
+ const sourceGrid = detectGridStructure(sourceElements, config);
5453
+ const targetGrid = detectGridStructure(targetElements, config);
5454
+ const gridDiff = {
5455
+ sourceGrid,
5456
+ targetGrid,
5457
+ columnDiff: sourceGrid.columnCount - targetGrid.columnCount,
5458
+ rowDiff: sourceGrid.rowCount - targetGrid.rowCount
5459
+ };
5460
+ const sourceDepth = computeMaxDepth(sourceElements);
5461
+ const targetDepth = computeMaxDepth(targetElements);
5462
+ const hierarchyDiff = {
5463
+ sourceDepth,
5464
+ targetDepth,
5465
+ depthDiff: sourceDepth - targetDepth
5466
+ };
5467
+ const sourceRegionCount = sourceRegions?.regions.length || 1;
5468
+ const targetRegionCount = targetRegions?.regions.length || 1;
5469
+ const sourceDensity = sourceElements.length / sourceRegionCount;
5470
+ const targetDensity = targetElements.length / targetRegionCount;
5471
+ const density = {
5472
+ sourceDensity: Math.round(sourceDensity * 100) / 100,
5473
+ targetDensity: Math.round(targetDensity * 100) / 100,
5474
+ ratio: targetDensity > 0 ? Math.round(sourceDensity / targetDensity * 100) / 100 : 0
5475
+ };
5476
+ const gridSimilarity = sourceGrid.columnCount === 0 && targetGrid.columnCount === 0 ? 1 : 1 - Math.abs(gridDiff.columnDiff) / Math.max(sourceGrid.columnCount, targetGrid.columnCount, 1);
5477
+ const hierarchySimilarity = sourceDepth === 0 && targetDepth === 0 ? 1 : 1 - Math.abs(hierarchyDiff.depthDiff) / Math.max(sourceDepth, targetDepth, 1);
5478
+ const densitySimilarity = density.ratio > 0 ? Math.min(density.ratio, 1 / density.ratio) : 0;
5479
+ const similarity = Math.round((gridSimilarity * 0.4 + hierarchySimilarity * 0.3 + densitySimilarity * 0.3) * 100) / 100;
5480
+ return {
5481
+ gridDiff,
5482
+ hierarchyDiff,
5483
+ density,
5484
+ similarity
5485
+ };
5486
+ }
5487
+
5488
+ // src/ai/content-comparison.ts
5489
+ var DEFAULT_CONTENT_COMPARISON_CONFIG = {
5490
+ labelMatchThreshold: 0.8,
5491
+ headingMatchThreshold: 0.75,
5492
+ maxCellDifferences: 50
5493
+ };
5494
+ function getElementText2(el) {
5495
+ return (el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "").trim();
5496
+ }
5497
+ function getContentRole(el) {
5498
+ if (el.contentMetadata?.contentRole) {
5499
+ return el.contentMetadata.contentRole;
5500
+ }
5501
+ const t = (el.type || "").toLowerCase();
5502
+ if (t === "heading" || t.startsWith("h") && /^h[1-6]$/.test(t)) return "heading";
5503
+ if (t === "metric-value" || t === "metric") return "metric";
5504
+ if (t === "status-message" || t === "status") return "status";
5505
+ if (t === "label") return "label";
5506
+ if (t === "badge") return "badge";
5507
+ if (t === "table-cell") return "table-cell";
5508
+ if (t === "table-header") return "table-header";
5509
+ if (t === "caption") return "caption";
5510
+ return null;
5511
+ }
5512
+ function getHeadingLevel(el) {
5513
+ if (el.contentMetadata?.headingLevel) {
5514
+ return el.contentMetadata.headingLevel;
5515
+ }
5516
+ const tag = (el.tagName || el.type || "").toLowerCase();
5517
+ const match = /^h([1-6])$/.exec(tag);
5518
+ if (match) return parseInt(match[1], 10);
5519
+ return void 0;
5520
+ }
5521
+ function isContentElement2(el) {
5522
+ if (el.category === "content") return true;
5523
+ if (el.contentMetadata) return true;
5524
+ const role = getContentRole(el);
5525
+ return role !== null;
5526
+ }
5527
+ function normalizeText(text) {
5528
+ return normalizeString(text, { caseSensitive: false, ignoreWhitespace: true });
5529
+ }
5530
+ function parseMetricText(el) {
5531
+ const text = getElementText2(el);
5532
+ const colonMatch = text.match(/^(.+?):\s*(.+)$/);
5533
+ if (colonMatch) {
5534
+ return { label: colonMatch[1].trim(), value: colonMatch[2].trim() };
5535
+ }
5536
+ const dashMatch = text.match(/^(.+?)\s*[-]\s*(.+)$/);
5537
+ if (dashMatch) {
5538
+ return { label: dashMatch[1].trim(), value: dashMatch[2].trim() };
5539
+ }
5540
+ const elLabel = el.accessibleName || el.labelText || el.label || el.id;
5541
+ return { label: elLabel, value: text };
5542
+ }
5543
+ function filterHeadings(elements) {
5544
+ return elements.filter((el) => getContentRole(el) === "heading");
5545
+ }
5546
+ function filterMetrics(elements) {
5547
+ return elements.filter((el) => getContentRole(el) === "metric");
5548
+ }
5549
+ function filterStatuses(elements) {
5550
+ return elements.filter((el) => {
5551
+ const role = getContentRole(el);
5552
+ return role === "status" || role === "badge";
5553
+ });
5554
+ }
5555
+ function filterLabels(elements) {
5556
+ return elements.filter((el) => {
5557
+ const role = getContentRole(el);
5558
+ return role === "label" || role === "caption";
5559
+ });
5560
+ }
5561
+ function matchTexts(sourceTexts, targetTexts, threshold) {
5562
+ const candidates = [];
5563
+ for (let si = 0; si < sourceTexts.length; si++) {
5564
+ const sNorm = normalizeText(sourceTexts[si]);
5565
+ if (!sNorm) continue;
5566
+ for (let ti = 0; ti < targetTexts.length; ti++) {
5567
+ const tNorm = normalizeText(targetTexts[ti]);
5568
+ if (!tNorm) continue;
5569
+ const score = sNorm === tNorm ? 1 : jaroWinklerSimilarity(sNorm, tNorm);
5570
+ if (score >= threshold) {
5571
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5572
+ }
5573
+ }
5574
+ }
5575
+ candidates.sort((a, b) => b.score - a.score);
5576
+ const usedSource = /* @__PURE__ */ new Set();
5577
+ const usedTarget = /* @__PURE__ */ new Set();
5578
+ const matched = [];
5579
+ for (const c of candidates) {
5580
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5581
+ usedSource.add(c.sourceIdx);
5582
+ usedTarget.add(c.targetIdx);
5583
+ matched.push(c);
5584
+ }
5585
+ const unmatchedSource = sourceTexts.map((_, i) => i).filter((i) => !usedSource.has(i));
5586
+ const unmatchedTarget = targetTexts.map((_, i) => i).filter((i) => !usedTarget.has(i));
5587
+ return { matched, unmatchedSource, unmatchedTarget };
5588
+ }
5589
+ function compareHeadings(sourceElements, targetElements, config) {
5590
+ const srcHeadings = filterHeadings(sourceElements);
5591
+ const tgtHeadings = filterHeadings(targetElements);
5592
+ const srcTexts = srcHeadings.map(getElementText2);
5593
+ const tgtTexts = tgtHeadings.map(getElementText2);
5594
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5595
+ srcTexts,
5596
+ tgtTexts,
5597
+ config.headingMatchThreshold
5598
+ );
5599
+ const headingMatched = [];
5600
+ const headingChanged = [];
5601
+ for (const m of matched) {
5602
+ const srcText = srcTexts[m.sourceIdx];
5603
+ const tgtText = tgtTexts[m.targetIdx];
5604
+ const srcLevel = getHeadingLevel(srcHeadings[m.sourceIdx]);
5605
+ const tgtLevel = getHeadingLevel(tgtHeadings[m.targetIdx]);
5606
+ if (normalizeText(srcText) === normalizeText(tgtText)) {
5607
+ headingMatched.push({
5608
+ source: srcText,
5609
+ target: tgtText,
5610
+ level: srcLevel
5611
+ });
5612
+ } else {
5613
+ headingChanged.push({
5614
+ source: srcText,
5615
+ target: tgtText,
5616
+ level: srcLevel ?? tgtLevel
5617
+ });
5618
+ }
5619
+ }
5620
+ return {
5621
+ matched: headingMatched,
5622
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5623
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i]),
5624
+ changed: headingChanged
5625
+ };
5626
+ }
5627
+ function compareMetrics(sourceElements, targetElements, config) {
5628
+ const srcMetrics = filterMetrics(sourceElements);
5629
+ const tgtMetrics = filterMetrics(targetElements);
5630
+ const srcParsed = srcMetrics.map(parseMetricText);
5631
+ const tgtParsed = tgtMetrics.map(parseMetricText);
5632
+ const srcLabels = srcParsed.map((p) => p.label);
5633
+ const tgtLabels = tgtParsed.map((p) => p.label);
5634
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5635
+ srcLabels,
5636
+ tgtLabels,
5637
+ config.labelMatchThreshold
5638
+ );
5639
+ const metricMatched = [];
5640
+ const metricChanged = [];
5641
+ for (const m of matched) {
5642
+ const src = srcParsed[m.sourceIdx];
5643
+ const tgt = tgtParsed[m.targetIdx];
5644
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5645
+ metricMatched.push({
5646
+ label: src.label,
5647
+ sourceValue: src.value,
5648
+ targetValue: tgt.value
5649
+ });
5650
+ } else {
5651
+ metricChanged.push({
5652
+ label: src.label,
5653
+ sourceValue: src.value,
5654
+ targetValue: tgt.value
5655
+ });
5656
+ }
5657
+ }
5658
+ return {
5659
+ matched: metricMatched,
5660
+ changed: metricChanged,
5661
+ sourceOnly: unmatchedSource.map((i) => srcParsed[i].label),
5662
+ targetOnly: unmatchedTarget.map((i) => tgtParsed[i].label)
5663
+ };
5664
+ }
5665
+ function compareStatuses(sourceElements, targetElements, config) {
5666
+ const srcStatuses = filterStatuses(sourceElements);
5667
+ const tgtStatuses = filterStatuses(targetElements);
5668
+ const srcParsed = srcStatuses.map(parseMetricText);
5669
+ const tgtParsed = tgtStatuses.map(parseMetricText);
5670
+ const srcLabels = srcParsed.map((p) => p.label);
5671
+ const tgtLabels = tgtParsed.map((p) => p.label);
5672
+ const { matched } = matchTexts(srcLabels, tgtLabels, config.labelMatchThreshold);
5673
+ const statusMatched = [];
5674
+ const statusChanged = [];
5675
+ for (const m of matched) {
5676
+ const src = srcParsed[m.sourceIdx];
5677
+ const tgt = tgtParsed[m.targetIdx];
5678
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5679
+ statusMatched.push({
5680
+ label: src.label,
5681
+ sourceStatus: src.value,
5682
+ targetStatus: tgt.value
5683
+ });
5684
+ } else {
5685
+ statusChanged.push({
5686
+ label: src.label,
5687
+ sourceStatus: src.value,
5688
+ targetStatus: tgt.value
5689
+ });
5690
+ }
5691
+ }
5692
+ return {
5693
+ matched: statusMatched,
5694
+ changed: statusChanged
5695
+ };
5696
+ }
5697
+ function compareLabels(sourceElements, targetElements, config) {
5698
+ const srcLabels = filterLabels(sourceElements);
5699
+ const tgtLabels = filterLabels(targetElements);
5700
+ const srcTexts = srcLabels.map(getElementText2);
5701
+ const tgtTexts = tgtLabels.map(getElementText2);
5702
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5703
+ srcTexts,
5704
+ tgtTexts,
5705
+ config.labelMatchThreshold
5706
+ );
5707
+ return {
5708
+ matched: matched.map((m) => srcTexts[m.sourceIdx]),
5709
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5710
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i])
5711
+ };
5712
+ }
5713
+ function compareTables(sourceElements, targetElements, config) {
5714
+ const srcData = extractStructuredData(sourceElements);
5715
+ const tgtData = extractStructuredData(targetElements);
5716
+ const srcTables = srcData.tables;
5717
+ const tgtTables = tgtData.tables;
5718
+ if (srcTables.length === 0 || tgtTables.length === 0) {
5719
+ return [];
5720
+ }
5721
+ const srcTableLabels = srcTables.map((t) => t.label || "");
5722
+ const tgtTableLabels = tgtTables.map((t) => t.label || "");
5723
+ const { matched } = matchTexts(srcTableLabels, tgtTableLabels, config.labelMatchThreshold);
5724
+ const tablePairs = [];
5725
+ if (matched.length > 0) {
5726
+ for (const m of matched) {
5727
+ tablePairs.push({ srcIdx: m.sourceIdx, tgtIdx: m.targetIdx });
5728
+ }
5729
+ } else if (srcTables.length === 1 && tgtTables.length === 1) {
5730
+ tablePairs.push({ srcIdx: 0, tgtIdx: 0 });
5731
+ }
5732
+ const comparisons = [];
5733
+ for (const pair of tablePairs) {
5734
+ const srcTable = srcTables[pair.srcIdx];
5735
+ const tgtTable = tgtTables[pair.tgtIdx];
5736
+ const srcHeaders = srcTable.columns.map((c) => c.header);
5737
+ const tgtHeaders = tgtTable.columns.map((c) => c.header);
5738
+ const srcHeaderSet = new Set(srcHeaders.map(normalizeText));
5739
+ const tgtHeaderSet = new Set(tgtHeaders.map(normalizeText));
5740
+ const sourceOnlyColumns = srcHeaders.filter((h) => !tgtHeaderSet.has(normalizeText(h)));
5741
+ const targetOnlyColumns = tgtHeaders.filter((h) => !srcHeaderSet.has(normalizeText(h)));
5742
+ const columnsMatch = sourceOnlyColumns.length === 0 && targetOnlyColumns.length === 0;
5743
+ const cellDifferences = [];
5744
+ const commonHeaders = srcHeaders.filter((h) => tgtHeaderSet.has(normalizeText(h)));
5745
+ const minRows = Math.min(srcTable.rows.length, tgtTable.rows.length);
5746
+ for (let row = 0; row < minRows; row++) {
5747
+ if (cellDifferences.length >= config.maxCellDifferences) break;
5748
+ for (const header of commonHeaders) {
5749
+ const srcColIdx = srcHeaders.indexOf(header);
5750
+ const tgtColIdx = tgtHeaders.findIndex((h) => normalizeText(h) === normalizeText(header));
5751
+ if (srcColIdx < 0 || tgtColIdx < 0) continue;
5752
+ const srcValue = srcTable.rows[row]?.[srcColIdx] ?? "";
5753
+ const tgtValue = tgtTable.rows[row]?.[tgtColIdx] ?? "";
5754
+ if (normalizeText(srcValue) !== normalizeText(tgtValue)) {
5755
+ cellDifferences.push({
5756
+ row,
5757
+ column: header,
5758
+ sourceValue: srcValue,
5759
+ targetValue: tgtValue
5760
+ });
5761
+ }
5762
+ }
5763
+ }
5764
+ comparisons.push({
5765
+ sourceLabel: srcTable.label,
5766
+ targetLabel: tgtTable.label,
5767
+ columnsMatch,
5768
+ sourceOnlyColumns,
5769
+ targetOnlyColumns,
5770
+ sourceRowCount: srcTable.rows.length,
5771
+ targetRowCount: tgtTable.rows.length,
5772
+ cellDifferences
5773
+ });
5774
+ }
5775
+ return comparisons;
5776
+ }
5777
+ function compareHeadingHierarchy(sourceElements, targetElements) {
5778
+ const srcHeadings = filterHeadings(sourceElements);
5779
+ const tgtHeadings = filterHeadings(targetElements);
5780
+ const srcByLevel = /* @__PURE__ */ new Map();
5781
+ const tgtByLevel = /* @__PURE__ */ new Map();
5782
+ for (const el of srcHeadings) {
5783
+ const level = getHeadingLevel(el) ?? 0;
5784
+ srcByLevel.set(level, (srcByLevel.get(level) ?? 0) + 1);
5785
+ }
5786
+ for (const el of tgtHeadings) {
5787
+ const level = getHeadingLevel(el) ?? 0;
5788
+ tgtByLevel.set(level, (tgtByLevel.get(level) ?? 0) + 1);
5789
+ }
5790
+ const allLevels = /* @__PURE__ */ new Set([...srcByLevel.keys(), ...tgtByLevel.keys()]);
5791
+ const result = [];
5792
+ for (const level of [...allLevels].sort()) {
5793
+ result.push({
5794
+ level,
5795
+ sourceCount: srcByLevel.get(level) ?? 0,
5796
+ targetCount: tgtByLevel.get(level) ?? 0
5797
+ });
5798
+ }
5799
+ return result;
5800
+ }
5801
+ function compareContent(sourceElements, targetElements, config = DEFAULT_CONTENT_COMPARISON_CONFIG) {
5802
+ const srcContent = sourceElements.filter(isContentElement2);
5803
+ const tgtContent = targetElements.filter(isContentElement2);
5804
+ const headings = compareHeadings(srcContent, tgtContent, config);
5805
+ const metrics = compareMetrics(srcContent, tgtContent, config);
5806
+ const statuses = compareStatuses(srcContent, tgtContent, config);
5807
+ const labels = compareLabels(srcContent, tgtContent, config);
5808
+ const tables = compareTables(sourceElements, targetElements, config);
5809
+ const headingHierarchy = compareHeadingHierarchy(srcContent, tgtContent);
5810
+ const contentParity = calculateContentParity(headings, metrics, statuses, labels, tables);
5811
+ return {
5812
+ headings,
5813
+ metrics,
5814
+ statuses,
5815
+ labels,
5816
+ tables,
5817
+ headingHierarchy,
5818
+ contentParity
5819
+ };
5820
+ }
5821
+ function calculateContentParity(headings, metrics, statuses, labels, tables) {
5822
+ const scores = [];
5823
+ const totalHeadings = headings.matched.length + headings.changed.length + headings.sourceOnly.length + headings.targetOnly.length;
5824
+ if (totalHeadings > 0) {
5825
+ scores.push(headings.matched.length / totalHeadings);
5826
+ }
5827
+ const totalMetrics = metrics.matched.length + metrics.changed.length + metrics.sourceOnly.length + metrics.targetOnly.length;
5828
+ if (totalMetrics > 0) {
5829
+ const metricScore = (metrics.matched.length + metrics.changed.length * 0.5) / totalMetrics;
5830
+ scores.push(metricScore);
5831
+ }
5832
+ const totalStatuses = statuses.matched.length + statuses.changed.length;
5833
+ if (totalStatuses > 0) {
5834
+ scores.push(statuses.matched.length / totalStatuses);
5835
+ }
5836
+ const totalLabels = labels.matched.length + labels.sourceOnly.length + labels.targetOnly.length;
5837
+ if (totalLabels > 0) {
5838
+ scores.push(labels.matched.length / totalLabels);
5839
+ }
5840
+ if (tables.length > 0) {
5841
+ let tableScore = 0;
5842
+ for (const table of tables) {
5843
+ let tScore = table.columnsMatch ? 0.5 : 0;
5844
+ if (table.sourceRowCount > 0) {
5845
+ const rowRatio = Math.min(
5846
+ table.targetRowCount / table.sourceRowCount,
5847
+ table.sourceRowCount / table.targetRowCount
5848
+ );
5849
+ tScore += rowRatio * 0.3;
5850
+ } else {
5851
+ tScore += 0.3;
5852
+ }
5853
+ const totalCells = Math.max(table.sourceRowCount, 1) * Math.max(
5854
+ table.sourceOnlyColumns.length + table.targetOnlyColumns.length + (table.columnsMatch ? 1 : 0),
5855
+ 1
5856
+ );
5857
+ const diffRatio = totalCells > 0 ? 1 - Math.min(table.cellDifferences.length / totalCells, 1) : 1;
5858
+ tScore += diffRatio * 0.2;
5859
+ tableScore += tScore;
5860
+ }
5861
+ scores.push(tableScore / tables.length);
5862
+ }
5863
+ if (scores.length === 0) return 1;
5864
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100;
5865
+ }
5866
+
5867
+ // src/ai/comparison-report.ts
5868
+ var DEFAULT_COMPARISON_REPORT_CONFIG = {
5869
+ includeComponents: false
5870
+ };
5871
+ function generateComparisonReport(source, target, options) {
5872
+ const startTime = Date.now();
5873
+ const config = { ...DEFAULT_COMPARISON_REPORT_CONFIG, ...options?.config };
5874
+ const srcElements = source.elements;
5875
+ const tgtElements = target.elements;
5876
+ const diff = computeCrossAppDiff(srcElements, tgtElements);
5877
+ const navigation = buildNavigationMap(srcElements, tgtElements);
5878
+ const sourceRegions = segmentPageRegions(srcElements);
5879
+ const targetRegions = segmentPageRegions(tgtElements);
5880
+ const layout = compareLayouts(srcElements, tgtElements, sourceRegions, targetRegions);
5881
+ const actionParityResults = analyzeActionParity(diff.matchedPairs, srcElements, tgtElements);
5882
+ const componentComparison = config.includeComponents && options?.sourceComponents && options?.targetComponents ? compareComponents(options.sourceComponents, options.targetComponents) : null;
5883
+ const contentComparison = compareContent(srcElements, tgtElements);
5884
+ const sourceData = extractPageData(srcElements);
5885
+ extractPageData(tgtElements);
5886
+ const sourceFieldCount = Object.keys(sourceData.values).length;
5887
+ const matchedDataCount = diff.dataComparisons.length;
5888
+ const dataCompleteness = sourceFieldCount > 0 ? Math.round(matchedDataCount / sourceFieldCount * 100) / 100 : 1;
5889
+ const formatMatchCount = diff.dataComparisons.filter((c) => c.formatsMatch).length;
5890
+ const formatAlignment = matchedDataCount > 0 ? Math.round(formatMatchCount / matchedDataCount * 100) / 100 : 1;
5891
+ const presentationAlignment = layout.similarity;
5892
+ const totalNavItems = navigation.pairs.length + navigation.sourceOnly.length;
5893
+ const navigationParity = totalNavItems > 0 ? Math.round(navigation.pairs.length / totalNavItems * 100) / 100 : 1;
5894
+ const totalActionChecks = actionParityResults.length;
5895
+ const fullParityCount = actionParityResults.filter((r) => r.missingInTarget.length === 0).length;
5896
+ const actionParity = totalActionChecks > 0 ? Math.round(fullParityCount / totalActionChecks * 100) / 100 : 1;
5897
+ const contentParity = contentComparison.contentParity;
5898
+ const overallScore = Math.round(
5899
+ (dataCompleteness * 0.2 + formatAlignment * 0.1 + presentationAlignment * 0.15 + navigationParity * 0.15 + actionParity * 0.15 + contentParity * 0.25) * 100
5900
+ ) / 100;
5901
+ const issues = [];
5902
+ for (const srcId of diff.unmatchedSourceIds) {
5903
+ const srcVal = Object.values(sourceData.values).find((v) => v.elementId === srcId);
5904
+ if (srcVal) {
5905
+ issues.push({
5906
+ severity: "warning",
5907
+ category: "missing-data",
5908
+ description: `Data field "${srcVal.label}" (${srcVal.dataType}) exists in source but has no match in target`,
5909
+ sourceElementId: srcId
5910
+ });
5911
+ }
5912
+ }
5913
+ for (const comp of diff.dataComparisons) {
5914
+ if (!comp.valuesMatch) {
5915
+ issues.push({
5916
+ severity: "error",
5917
+ category: "value-mismatch",
5918
+ description: `Value mismatch for "${comp.label}": source="${comp.sourceValue}", target="${comp.targetValue}"`
5919
+ });
5920
+ }
5921
+ }
5922
+ for (const fm of diff.formatMismatches) {
5923
+ issues.push({
5924
+ severity: fm.severity,
5925
+ category: "format-mismatch",
5926
+ description: fm.description
5927
+ });
5928
+ }
5929
+ for (const ap of actionParityResults) {
5930
+ for (const action of ap.missingInTarget) {
5931
+ issues.push({
5932
+ severity: "warning",
5933
+ category: "missing-action",
5934
+ description: `Action "${action}" available on source element "${ap.pair.sourceLabel}" is missing in target`,
5935
+ sourceElementId: ap.pair.sourceId,
5936
+ targetElementId: ap.pair.targetId
5937
+ });
5938
+ }
5939
+ }
5940
+ for (const srcId of navigation.sourceOnly) {
5941
+ issues.push({
5942
+ severity: "warning",
5943
+ category: "navigation-gap",
5944
+ description: `Navigation item "${srcId}" in source has no match in target`,
5945
+ sourceElementId: srcId
5946
+ });
5947
+ }
5948
+ if (layout.similarity < 0.5) {
5949
+ issues.push({
5950
+ severity: "warning",
5951
+ category: "layout-difference",
5952
+ description: `Layout similarity is low (${layout.similarity}). Grid: ${layout.gridDiff.sourceGrid.columnCount} cols vs ${layout.gridDiff.targetGrid.columnCount} cols`
5953
+ });
5954
+ }
5955
+ if (componentComparison) {
5956
+ for (const src of componentComparison.sourceOnly) {
5957
+ issues.push({
5958
+ severity: "info",
5959
+ category: "component-mismatch",
5960
+ description: `Component "${src.name}" (${src.type}) exists in source but not target`
5961
+ });
5962
+ }
5963
+ for (const match of componentComparison.matches) {
5964
+ if (match.stateKeyDiff.missing.length > 0) {
5965
+ issues.push({
5966
+ severity: "warning",
5967
+ category: "component-mismatch",
5968
+ description: `Component "${match.source.name}": state keys missing in target: ${match.stateKeyDiff.missing.join(", ")}`
5969
+ });
5970
+ }
5971
+ }
5972
+ }
5973
+ for (const heading of contentComparison.headings.sourceOnly) {
5974
+ issues.push({
5975
+ severity: "warning",
5976
+ category: "content-difference",
5977
+ description: `Heading "${heading}" exists in source but not in target`
5978
+ });
5979
+ }
5980
+ for (const heading of contentComparison.headings.targetOnly) {
5981
+ issues.push({
5982
+ severity: "info",
5983
+ category: "content-difference",
5984
+ description: `Heading "${heading}" exists in target but not in source`
5985
+ });
5986
+ }
5987
+ for (const change of contentComparison.headings.changed) {
5988
+ issues.push({
5989
+ severity: "warning",
5990
+ category: "content-difference",
5991
+ description: `Heading changed: "${change.source}" -> "${change.target}"`
5992
+ });
5993
+ }
5994
+ for (const change of contentComparison.metrics.changed) {
5995
+ issues.push({
5996
+ severity: "warning",
5997
+ category: "content-difference",
5998
+ description: `Metric "${change.label}" value differs: "${change.sourceValue}" vs "${change.targetValue}"`
5999
+ });
6000
+ }
6001
+ for (const label of contentComparison.metrics.sourceOnly) {
6002
+ issues.push({
6003
+ severity: "warning",
6004
+ category: "content-difference",
6005
+ description: `Metric "${label}" exists in source but not in target`
6006
+ });
6007
+ }
6008
+ for (const change of contentComparison.statuses.changed) {
6009
+ issues.push({
6010
+ severity: "warning",
6011
+ category: "content-difference",
6012
+ description: `Status "${change.label}" differs: "${change.sourceStatus}" vs "${change.targetStatus}"`
6013
+ });
6014
+ }
6015
+ for (const table of contentComparison.tables) {
6016
+ if (!table.columnsMatch) {
6017
+ issues.push({
6018
+ severity: "warning",
6019
+ category: "content-difference",
6020
+ description: `Table "${table.sourceLabel}" column mismatch: source-only=[${table.sourceOnlyColumns.join(", ")}], target-only=[${table.targetOnlyColumns.join(", ")}]`
6021
+ });
6022
+ }
6023
+ if (table.sourceRowCount !== table.targetRowCount) {
6024
+ issues.push({
6025
+ severity: "info",
6026
+ category: "content-difference",
6027
+ description: `Table "${table.sourceLabel}" row count differs: ${table.sourceRowCount} vs ${table.targetRowCount}`
6028
+ });
6029
+ }
6030
+ if (table.cellDifferences.length > 0) {
6031
+ issues.push({
6032
+ severity: "warning",
6033
+ category: "content-difference",
6034
+ description: `Table "${table.sourceLabel}" has ${table.cellDifferences.length} cell value difference(s)`
6035
+ });
6036
+ }
6037
+ }
6038
+ const severityOrder = { error: 0, warning: 1, info: 2 };
6039
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
6040
+ const errorCount = issues.filter((i) => i.severity === "error").length;
6041
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
6042
+ const infoCount = issues.filter((i) => i.severity === "info").length;
6043
+ const summaryLines = [
6044
+ `Cross-app comparison: ${source.page.url} vs ${target.page.url}`,
6045
+ `Overall score: ${(overallScore * 100).toFixed(0)}%`,
6046
+ `Matched elements: ${diff.matchedPairs.length}`,
6047
+ `Unmatched: ${diff.unmatchedSourceIds.length} source, ${diff.unmatchedTargetIds.length} target`,
6048
+ `Navigation: ${navigation.pairs.length} matched, ${navigation.sourceOnly.length} source-only, ${navigation.targetOnly.length} target-only`
6049
+ ];
6050
+ if (componentComparison) {
6051
+ summaryLines.push(
6052
+ `Components: ${componentComparison.matches.length} matched, ${componentComparison.sourceOnly.length} source-only, ${componentComparison.targetOnly.length} target-only`
6053
+ );
6054
+ }
6055
+ const hMatched = contentComparison.headings.matched.length;
6056
+ const hChanged = contentComparison.headings.changed.length;
6057
+ const hSrcOnly = contentComparison.headings.sourceOnly.length;
6058
+ const hTgtOnly = contentComparison.headings.targetOnly.length;
6059
+ const mMatched = contentComparison.metrics.matched.length;
6060
+ const mChanged = contentComparison.metrics.changed.length;
6061
+ const sMatched = contentComparison.statuses.matched.length;
6062
+ const sChanged = contentComparison.statuses.changed.length;
6063
+ const totalContent = hMatched + hChanged + hSrcOnly + hTgtOnly + mMatched + mChanged + sMatched + sChanged;
6064
+ if (totalContent > 0) {
6065
+ summaryLines.push(
6066
+ `Content: headings=${hMatched} matched/${hChanged} changed/${hSrcOnly + hTgtOnly} unmatched, metrics=${mMatched} matched/${mChanged} changed, statuses=${sMatched} matched/${sChanged} changed, parity=${(contentParity * 100).toFixed(0)}%`
6067
+ );
6068
+ }
6069
+ summaryLines.push(`Issues: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
6070
+ const summary = summaryLines.join("\n");
6071
+ const report = {
6072
+ sourceUrl: source.page.url,
6073
+ targetUrl: target.page.url,
6074
+ timestamp: Date.now(),
6075
+ durationMs: Date.now() - startTime,
6076
+ scores: {
6077
+ dataCompleteness,
6078
+ formatAlignment,
6079
+ presentationAlignment,
6080
+ navigationParity,
6081
+ actionParity,
6082
+ overallScore
6083
+ },
6084
+ diff,
6085
+ navigation,
6086
+ layout,
6087
+ contentComparison,
6088
+ issues,
6089
+ summary
6090
+ };
6091
+ if (componentComparison) {
6092
+ report.components = componentComparison;
6093
+ }
6094
+ return report;
6095
+ }
3836
6096
 
3837
- export { AssertionExecutor, DEFAULT_ALIAS_CONFIG, DEFAULT_ASSERTION_CONFIG, DEFAULT_DIFF_CONFIG, DEFAULT_EXECUTOR_CONFIG, DEFAULT_FUZZY_CONFIG, DEFAULT_SEARCH_CONFIG, DEFAULT_SNAPSHOT_CONFIG, ErrorCodes, NLActionExecutor, SearchEngine, SemanticDiffManager, SemanticSnapshotManager, areSynonyms, computeDiff, createAssertionExecutor, createDiffManager, createErrorContext, createNLActionExecutor, createSearchEngine, createSimpleError, createSnapshotManager, describeAction, describeDiff, extractModifiers, findAllMatches, findBestMatch, formatErrorContext, fuzzyContains, fuzzyMatch, generateAliases, generateDescription, generateDiffSummary, generateElementDescription, generateNgrams, generatePageSummary, generatePurpose, generateSnapshotSummary, generateSuggestedActions, getBestRecoverySuggestion, getSynonyms, hasSignificantChanges, inferPageType, isRecoverableError, jaroSimilarity, jaroWinklerSimilarity, levenshteinDistance, levenshteinSimilarity, ngramSimilarity, normalizeString, parseNLInstruction, parseNLInstructions, splitCompoundInstruction, tokenSimilarity, tokenize, validateParsedAction, wordSimilarity };
6097
+ export { AssertionExecutor, DEFAULT_ACTION_PARITY_CONFIG, DEFAULT_ALIAS_CONFIG, DEFAULT_ASSERTION_CONFIG, DEFAULT_COMPARISON_REPORT_CONFIG, DEFAULT_COMPONENT_COMPARISON_CONFIG, DEFAULT_CONTENT_COMPARISON_CONFIG, DEFAULT_CROSS_APP_DIFF_CONFIG, DEFAULT_DATA_EXTRACTION_CONFIG, DEFAULT_DIFF_CONFIG, DEFAULT_EXECUTOR_CONFIG, DEFAULT_FORMAT_ANALYSIS_CONFIG, DEFAULT_FUZZY_CONFIG, DEFAULT_LAYOUT_COMPARISON_CONFIG, DEFAULT_NAVIGATION_MAP_CONFIG, DEFAULT_REGION_SEGMENTATION_CONFIG, DEFAULT_SEARCH_CONFIG, DEFAULT_SNAPSHOT_CONFIG, DEFAULT_TABLE_EXTRACTION_CONFIG, ErrorCodes, NLActionExecutor, SearchEngine, SemanticDiffManager, SemanticSnapshotManager, analyzeActionParity, analyzeFormat, analyzePageFormats, areSynonyms, buildNavigationMap, classifyDataType, classifyRegionType, classifyStatusDirection, compareComponents, compareContent, compareFormats, compareLayouts, computeCrossAppDiff, computeDiff, computeProminence, createAssertionExecutor, createDiffManager, createErrorContext, createNLActionExecutor, createSearchEngine, createSimpleError, createSnapshotManager, describeAction, describeDiff, detectFormatPattern, detectGridStructure, detectList, detectTable, extractModifiers, extractPageData, extractStructuredData, findAllMatches, findBestMatch, formatErrorContext, fuzzyContains, fuzzyMatch, generateAliases, generateComparisonReport, generateDescription, generateDiffSummary, generateElementDescription, generateNgrams, generatePageSummary, generatePurpose, generateSnapshotSummary, generateSuggestedActions, getBestRecoverySuggestion, getSynonyms, hasSignificantChanges, inferPageType, isNavigationElement, isRecoverableError, jaroSimilarity, jaroWinklerSimilarity, levenshteinDistance, levenshteinSimilarity, matchElements, ngramSimilarity, normalizeString, normalizeValue, parseNLInstruction, parseNLInstructions, parseNumericValue, segmentPageRegions, splitCompoundInstruction, tokenSimilarity, tokenize, validateParsedAction, wordSimilarity };
3838
6098
  //# sourceMappingURL=index.mjs.map
3839
6099
  //# sourceMappingURL=index.mjs.map