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