@qontinui/ui-bridge 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/ai/index.d.mts +312 -155
  2. package/dist/ai/index.d.ts +312 -155
  3. package/dist/ai/index.js +2363 -67
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/index.mjs +2328 -68
  6. package/dist/ai/index.mjs.map +1 -1
  7. package/dist/annotations/index.d.mts +218 -0
  8. package/dist/annotations/index.d.ts +218 -0
  9. package/dist/annotations/index.js +246 -0
  10. package/dist/annotations/index.js.map +1 -0
  11. package/dist/annotations/index.mjs +241 -0
  12. package/dist/annotations/index.mjs.map +1 -0
  13. package/dist/assertions-BSR3afVr.d.ts +161 -0
  14. package/dist/assertions-CTw1hfOx.d.mts +161 -0
  15. package/dist/babel-plugin/index.js +23 -34
  16. package/dist/babel-plugin/index.js.map +1 -1
  17. package/dist/babel-plugin/index.mjs +23 -34
  18. package/dist/babel-plugin/index.mjs.map +1 -1
  19. package/dist/browser-capture-Bms60T6f.d.mts +47 -0
  20. package/dist/browser-capture-CsTU29mb.d.ts +47 -0
  21. package/dist/control/index.d.mts +26 -7
  22. package/dist/control/index.d.ts +26 -7
  23. package/dist/control/index.js +276 -48
  24. package/dist/control/index.js.map +1 -1
  25. package/dist/control/index.mjs +276 -48
  26. package/dist/control/index.mjs.map +1 -1
  27. package/dist/core/index.d.mts +2 -2
  28. package/dist/core/index.d.ts +2 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs.map +1 -1
  31. package/dist/debug/index.d.mts +5 -3
  32. package/dist/debug/index.d.ts +5 -3
  33. package/dist/debug/index.js +925 -1
  34. package/dist/debug/index.js.map +1 -1
  35. package/dist/debug/index.mjs +924 -2
  36. package/dist/debug/index.mjs.map +1 -1
  37. package/dist/index.d.mts +12 -7
  38. package/dist/index.d.ts +12 -7
  39. package/dist/index.js +4720 -173
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +4656 -174
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  44. package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  45. package/dist/native/control/index.js +2 -7
  46. package/dist/native/control/index.js.map +1 -1
  47. package/dist/native/control/index.mjs +2 -7
  48. package/dist/native/control/index.mjs.map +1 -1
  49. package/dist/native/core/index.js.map +1 -1
  50. package/dist/native/core/index.mjs.map +1 -1
  51. package/dist/native/debug/index.js +23 -66
  52. package/dist/native/debug/index.js.map +1 -1
  53. package/dist/native/debug/index.mjs +23 -66
  54. package/dist/native/debug/index.mjs.map +1 -1
  55. package/dist/native/index.js +89 -131
  56. package/dist/native/index.js.map +1 -1
  57. package/dist/native/index.mjs +89 -131
  58. package/dist/native/index.mjs.map +1 -1
  59. package/dist/native/react/index.js +28 -52
  60. package/dist/native/react/index.js.map +1 -1
  61. package/dist/native/react/index.mjs +28 -52
  62. package/dist/native/react/index.mjs.map +1 -1
  63. package/dist/native/server/index.js +38 -13
  64. package/dist/native/server/index.js.map +1 -1
  65. package/dist/native/server/index.mjs +38 -13
  66. package/dist/native/server/index.mjs.map +1 -1
  67. package/dist/react/index.d.mts +107 -8
  68. package/dist/react/index.d.ts +107 -8
  69. package/dist/react/index.js +2194 -84
  70. package/dist/react/index.js.map +1 -1
  71. package/dist/react/index.mjs +2194 -85
  72. package/dist/react/index.mjs.map +1 -1
  73. package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
  74. package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
  75. package/dist/render-log/index.d.mts +1 -1
  76. package/dist/render-log/index.d.ts +1 -1
  77. package/dist/server/express.d.mts +5 -4
  78. package/dist/server/express.d.ts +5 -4
  79. package/dist/server/express.js +104 -2
  80. package/dist/server/express.js.map +1 -1
  81. package/dist/server/express.mjs +104 -2
  82. package/dist/server/express.mjs.map +1 -1
  83. package/dist/server/handlers.d.mts +36 -5
  84. package/dist/server/handlers.d.ts +36 -5
  85. package/dist/server/handlers.js +3129 -224
  86. package/dist/server/handlers.js.map +1 -1
  87. package/dist/server/handlers.mjs +3129 -224
  88. package/dist/server/handlers.mjs.map +1 -1
  89. package/dist/server/index.d.mts +7 -5
  90. package/dist/server/index.d.ts +7 -5
  91. package/dist/server/index.js +3215 -183
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/index.mjs +3215 -183
  94. package/dist/server/index.mjs.map +1 -1
  95. package/dist/server/nextjs.d.mts +6 -4
  96. package/dist/server/nextjs.d.ts +6 -4
  97. package/dist/server/nextjs.js +106 -3
  98. package/dist/server/nextjs.js.map +1 -1
  99. package/dist/server/nextjs.mjs +106 -3
  100. package/dist/server/nextjs.mjs.map +1 -1
  101. package/dist/server/standalone.d.mts +6 -5
  102. package/dist/server/standalone.d.ts +6 -5
  103. package/dist/server/standalone.js +131 -5
  104. package/dist/server/standalone.js.map +1 -1
  105. package/dist/server/standalone.mjs +131 -5
  106. package/dist/server/standalone.mjs.map +1 -1
  107. package/dist/specs/index.d.mts +365 -0
  108. package/dist/specs/index.d.ts +365 -0
  109. package/dist/specs/index.js +2809 -0
  110. package/dist/specs/index.js.map +1 -0
  111. package/dist/specs/index.mjs +2786 -0
  112. package/dist/specs/index.mjs.map +1 -0
  113. package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
  114. package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
  115. package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
  116. package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
  117. package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
  118. package/dist/types-CebMQj76.d.ts +1275 -0
  119. package/dist/types-D_ypYl3T.d.mts +1275 -0
  120. package/dist/types-UBtp7R0u.d.mts +132 -0
  121. package/dist/types-UBtp7R0u.d.ts +132 -0
  122. package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
  123. package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
  124. package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
  125. package/package.json +18 -2
  126. package/dist/types-B5Q0GVo0.d.mts +0 -646
  127. package/dist/types-DfPqwU-i.d.ts +0 -646
@@ -423,17 +423,72 @@ function generateDescription(input) {
423
423
  }
424
424
  parts.push(`"${name}"`);
425
425
  }
426
- const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [input.elementType || "element"];
426
+ const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
427
+ input.elementType || "element"
428
+ ];
427
429
  parts.push(typeWords[0]);
428
430
  if (input.inputType && input.inputType !== "text") {
429
431
  parts.push(`(${input.inputType})`);
430
432
  }
431
433
  return parts.join(" ");
432
434
  }
435
+ var CONTENT_TYPES = /* @__PURE__ */ new Set([
436
+ "heading",
437
+ "paragraph",
438
+ "list-item",
439
+ "table-cell",
440
+ "table-header",
441
+ "label",
442
+ "caption",
443
+ "blockquote",
444
+ "code-block",
445
+ "badge",
446
+ "status-message",
447
+ "metric-value",
448
+ "description-text",
449
+ "nav-text",
450
+ "content-generic"
451
+ ]);
433
452
  function generatePurpose(input) {
434
453
  const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
435
454
  const type = input.elementType?.toLowerCase() || "";
436
455
  const inputType = input.inputType?.toLowerCase() || "";
456
+ if (CONTENT_TYPES.has(type)) {
457
+ switch (type) {
458
+ case "heading":
459
+ return "Section heading";
460
+ case "paragraph":
461
+ return "Body text content";
462
+ case "list-item":
463
+ return "List item";
464
+ case "table-cell":
465
+ return "Table data cell";
466
+ case "table-header":
467
+ return "Table column header";
468
+ case "label":
469
+ return "Field label or definition term";
470
+ case "caption":
471
+ return "Figure or table caption";
472
+ case "blockquote":
473
+ return "Quoted content";
474
+ case "code-block":
475
+ return "Code or preformatted text";
476
+ case "badge":
477
+ return "Status badge or tag";
478
+ case "status-message":
479
+ return "Dynamic status indicator";
480
+ case "metric-value":
481
+ return "Metric or statistic value";
482
+ case "description-text":
483
+ return "Description or definition";
484
+ case "nav-text":
485
+ return "Navigation label";
486
+ case "content-generic":
487
+ return "Text content";
488
+ default:
489
+ return "Static content";
490
+ }
491
+ }
437
492
  if (type === "button" || inputType === "submit") {
438
493
  if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
439
494
  return "Submits the form";
@@ -498,6 +553,10 @@ function generateSuggestedActions(input) {
498
553
  const inputType = input.inputType?.toLowerCase() || "";
499
554
  const text = (input.textContent || input.ariaLabel || "").toLowerCase();
500
555
  const actions = [];
556
+ if (CONTENT_TYPES.has(type)) {
557
+ actions.push("read text content", "verify text matches expected");
558
+ return actions;
559
+ }
501
560
  switch (type) {
502
561
  case "button":
503
562
  actions.push(`click "${text || "this button"}"`);
@@ -545,6 +604,241 @@ function areSynonyms(word1, word2) {
545
604
  return synonyms1.includes(w2) || synonyms2.includes(w1);
546
605
  }
547
606
 
607
+ // src/annotations/types.ts
608
+ var ANNOTATION_CONFIG_VERSION = "1.0.0";
609
+
610
+ // src/annotations/store.ts
611
+ var AnnotationStore = class {
612
+ constructor() {
613
+ this.store = /* @__PURE__ */ new Map();
614
+ this.listeners = /* @__PURE__ */ new Set();
615
+ }
616
+ /**
617
+ * Get an annotation by element ID.
618
+ */
619
+ get(elementId) {
620
+ return this.store.get(elementId);
621
+ }
622
+ /**
623
+ * Get all annotations as a record.
624
+ */
625
+ getAll() {
626
+ const result = {};
627
+ for (const [id, annotation] of this.store) {
628
+ result[id] = annotation;
629
+ }
630
+ return result;
631
+ }
632
+ /**
633
+ * Set an annotation for an element. Auto-sets `updatedAt`.
634
+ */
635
+ set(elementId, annotation) {
636
+ const updated = {
637
+ ...annotation,
638
+ updatedAt: Date.now()
639
+ };
640
+ this.store.set(elementId, updated);
641
+ this.emit({
642
+ type: "annotation:set",
643
+ elementId,
644
+ annotation: updated,
645
+ timestamp: Date.now()
646
+ });
647
+ }
648
+ /**
649
+ * Delete an annotation by element ID.
650
+ *
651
+ * @returns true if the annotation existed and was deleted
652
+ */
653
+ delete(elementId) {
654
+ const existed = this.store.delete(elementId);
655
+ if (existed) {
656
+ this.emit({
657
+ type: "annotation:deleted",
658
+ elementId,
659
+ timestamp: Date.now()
660
+ });
661
+ }
662
+ return existed;
663
+ }
664
+ /**
665
+ * Check if an annotation exists for an element.
666
+ */
667
+ has(elementId) {
668
+ return this.store.has(elementId);
669
+ }
670
+ /**
671
+ * Get the number of stored annotations.
672
+ */
673
+ get count() {
674
+ return this.store.size;
675
+ }
676
+ /**
677
+ * Clear all annotations.
678
+ */
679
+ clear() {
680
+ this.store.clear();
681
+ this.emit({
682
+ type: "annotation:cleared",
683
+ timestamp: Date.now()
684
+ });
685
+ }
686
+ /**
687
+ * Import annotations from a config object.
688
+ *
689
+ * Merges with existing annotations (new values overwrite per element ID).
690
+ *
691
+ * @returns Number of annotations imported
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * const config: AnnotationConfig = {
696
+ * version: '1.0.0',
697
+ * annotations: {
698
+ * 'btn-1': { description: 'Submit button', tags: ['form'] },
699
+ * 'input-1': { description: 'Name field' },
700
+ * },
701
+ * };
702
+ * const count = store.importConfig(config); // 2
703
+ * ```
704
+ */
705
+ importConfig(config) {
706
+ let count = 0;
707
+ for (const [id, annotation] of Object.entries(config.annotations)) {
708
+ this.store.set(id, {
709
+ ...annotation,
710
+ updatedAt: annotation.updatedAt ?? Date.now()
711
+ });
712
+ count++;
713
+ }
714
+ this.emit({
715
+ type: "annotation:imported",
716
+ count,
717
+ timestamp: Date.now()
718
+ });
719
+ return count;
720
+ }
721
+ /**
722
+ * Export all annotations as a config object.
723
+ *
724
+ * The returned object can be serialized to JSON and saved to a file,
725
+ * then later re-imported with {@link importConfig}.
726
+ *
727
+ * @param metadata - Optional metadata to include (appName, description, etc.)
728
+ * @returns AnnotationConfig with all current annotations
729
+ *
730
+ * @example
731
+ * ```ts
732
+ * const config = store.exportConfig({ appName: 'MyApp' });
733
+ * // config.version === '1.0.0'
734
+ * // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
735
+ * // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
736
+ *
737
+ * // Save to file
738
+ * fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
739
+ * ```
740
+ */
741
+ exportConfig(metadata) {
742
+ return {
743
+ version: ANNOTATION_CONFIG_VERSION,
744
+ annotations: this.getAll(),
745
+ metadata: {
746
+ ...metadata,
747
+ exportedAt: Date.now()
748
+ }
749
+ };
750
+ }
751
+ /**
752
+ * Compute annotation coverage against a set of known element IDs.
753
+ *
754
+ * Compares the store's annotations against the provided list of element IDs
755
+ * to determine what percentage of elements have been annotated.
756
+ *
757
+ * @param allElementIds - Array of all known element IDs in the UI
758
+ * @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * store.set('btn-1', { description: 'Submit' });
763
+ * store.set('input-1', { description: 'Name' });
764
+ *
765
+ * const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
766
+ * // coverage.totalElements === 4
767
+ * // coverage.annotatedElements === 2
768
+ * // coverage.coveragePercent === 50
769
+ * // coverage.annotatedIds === ['btn-1', 'input-1']
770
+ * // coverage.unannotatedIds === ['input-2', 'link-1']
771
+ * ```
772
+ */
773
+ getCoverage(allElementIds) {
774
+ const annotatedIds = [];
775
+ const unannotatedIds = [];
776
+ for (const id of allElementIds) {
777
+ if (this.store.has(id)) {
778
+ annotatedIds.push(id);
779
+ } else {
780
+ unannotatedIds.push(id);
781
+ }
782
+ }
783
+ const total = allElementIds.length;
784
+ return {
785
+ totalElements: total,
786
+ annotatedElements: annotatedIds.length,
787
+ coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
788
+ annotatedIds,
789
+ unannotatedIds,
790
+ timestamp: Date.now()
791
+ };
792
+ }
793
+ /**
794
+ * Subscribe to annotation events.
795
+ *
796
+ * The listener is called whenever annotations are set, deleted, imported,
797
+ * or cleared. Returns an unsubscribe function to stop listening.
798
+ *
799
+ * @param listener - Callback function receiving {@link AnnotationEvent} objects
800
+ * @returns Unsubscribe function - call it to remove the listener
801
+ *
802
+ * @example
803
+ * ```ts
804
+ * const unsubscribe = store.on((event) => {
805
+ * if (event.type === 'annotation:set') {
806
+ * console.log(`Element ${event.elementId} annotated:`, event.annotation);
807
+ * }
808
+ * });
809
+ *
810
+ * store.set('btn-1', { description: 'Submit' });
811
+ * // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
812
+ *
813
+ * unsubscribe(); // Stop listening
814
+ * ```
815
+ */
816
+ on(listener) {
817
+ this.listeners.add(listener);
818
+ return () => {
819
+ this.listeners.delete(listener);
820
+ };
821
+ }
822
+ /**
823
+ * Emit an event to all listeners.
824
+ */
825
+ emit(event) {
826
+ for (const listener of this.listeners) {
827
+ try {
828
+ listener(event);
829
+ } catch {
830
+ }
831
+ }
832
+ }
833
+ };
834
+ var globalStore = null;
835
+ function getGlobalAnnotationStore() {
836
+ if (!globalStore) {
837
+ globalStore = new AnnotationStore();
838
+ }
839
+ return globalStore;
840
+ }
841
+
548
842
  // src/ai/search-engine.ts
549
843
  var DEFAULT_SEARCH_CONFIG = {
550
844
  fuzzyThreshold: 0.7,
@@ -587,17 +881,40 @@ var SearchEngine = class {
587
881
  if ("getState" in element && typeof element.getState === "function") {
588
882
  state = getState ? getState(element) : element.getState();
589
883
  textContent = state.textContent || void 0;
590
- tagName = element.element.tagName.toLowerCase();
591
- role = element.element.getAttribute("role") || void 0;
592
- ariaLabel = element.element.getAttribute("aria-label") || void 0;
593
- placeholder = element.element.getAttribute("placeholder") || void 0;
594
- title = element.element.getAttribute("title") || void 0;
595
- if (element.element.id) {
596
- const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
597
- labelText = labelEl?.textContent?.trim() || void 0;
598
- }
599
- if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
600
- value = element.element.value || void 0;
884
+ try {
885
+ tagName = element.element.tagName.toLowerCase();
886
+ } catch {
887
+ tagName = element.type || "unknown";
888
+ }
889
+ try {
890
+ role = element.element.getAttribute("role") || void 0;
891
+ ariaLabel = element.element.getAttribute("aria-label") || void 0;
892
+ placeholder = element.element.getAttribute("placeholder") || void 0;
893
+ title = element.element.getAttribute("title") || void 0;
894
+ } catch {
895
+ }
896
+ if (!ariaLabel && element.label) {
897
+ ariaLabel = element.label;
898
+ }
899
+ try {
900
+ if (element.element.id) {
901
+ const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
902
+ labelText = labelEl?.textContent?.trim() || void 0;
903
+ }
904
+ } catch {
905
+ }
906
+ if (!labelText && element.label) {
907
+ labelText = element.label;
908
+ }
909
+ if (!textContent && element.label) {
910
+ textContent = element.label;
911
+ }
912
+ try {
913
+ if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
914
+ value = element.element.value || void 0;
915
+ }
916
+ } catch {
917
+ value = state.value || void 0;
601
918
  }
602
919
  } else {
603
920
  const discovered = element;
@@ -606,8 +923,11 @@ var SearchEngine = class {
606
923
  tagName = discovered.tagName;
607
924
  role = discovered.role || void 0;
608
925
  ariaLabel = discovered.accessibleName || void 0;
926
+ if (!labelText && element.label) {
927
+ labelText = element.label;
928
+ }
609
929
  }
610
- const aliases = generateAliases({
930
+ let aliases = generateAliases({
611
931
  textContent,
612
932
  ariaLabel,
613
933
  placeholder,
@@ -617,7 +937,14 @@ var SearchEngine = class {
617
937
  labelText,
618
938
  value
619
939
  });
620
- const description = generateDescription({
940
+ if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
941
+ const aliasSet = /* @__PURE__ */ new Set([
942
+ ...aliases,
943
+ ...element.aliases.map((a) => a.toLowerCase())
944
+ ]);
945
+ aliases = [...aliasSet];
946
+ }
947
+ let description = generateDescription({
621
948
  textContent,
622
949
  ariaLabel,
623
950
  placeholder,
@@ -626,6 +953,22 @@ var SearchEngine = class {
626
953
  id: element.id,
627
954
  labelText
628
955
  });
956
+ if (!description && "description" in element && element.description) {
957
+ description = element.description;
958
+ }
959
+ const annotation = getGlobalAnnotationStore().get(element.id);
960
+ if (annotation) {
961
+ if (annotation.description) {
962
+ description = annotation.description;
963
+ }
964
+ if (annotation.tags && annotation.tags.length > 0) {
965
+ const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
966
+ aliases = [...tagSet];
967
+ }
968
+ if (annotation.notes) {
969
+ aliases.push(annotation.notes.toLowerCase());
970
+ }
971
+ }
629
972
  return {
630
973
  id: element.id,
631
974
  element,
@@ -728,7 +1071,12 @@ var SearchEngine = class {
728
1071
  threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
729
1072
  };
730
1073
  if (criteria.text) {
731
- const textScore = this.scoreTextMatch(searchable, criteria.text, criteria.fuzzy !== false, fuzzyConfig.threshold);
1074
+ const textScore = this.scoreTextMatch(
1075
+ searchable,
1076
+ criteria.text,
1077
+ criteria.fuzzy !== false,
1078
+ fuzzyConfig.threshold
1079
+ );
732
1080
  scores.text = textScore.score;
733
1081
  if (textScore.score > 0) {
734
1082
  matchReasons.push(...textScore.reasons);
@@ -736,8 +1084,37 @@ var SearchEngine = class {
736
1084
  weightedScore += textScore.score * this.config.textWeight;
737
1085
  totalWeight += this.config.textWeight;
738
1086
  }
1087
+ if (criteria.textContent && !criteria.text) {
1088
+ const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
1089
+ let bestScore = 0;
1090
+ let bestReasons = [];
1091
+ for (const alt of alternatives) {
1092
+ const exactScore = this.scoreTextMatch(
1093
+ searchable,
1094
+ alt,
1095
+ criteria.fuzzy !== false,
1096
+ fuzzyConfig.threshold
1097
+ );
1098
+ const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
1099
+ const altBest = Math.max(exactScore.score, containsScore.score);
1100
+ if (altBest > bestScore) {
1101
+ bestScore = altBest;
1102
+ bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
1103
+ }
1104
+ }
1105
+ scores.text = bestScore;
1106
+ if (bestScore > 0) {
1107
+ matchReasons.push(...bestReasons);
1108
+ }
1109
+ weightedScore += bestScore * this.config.textWeight;
1110
+ totalWeight += this.config.textWeight;
1111
+ }
739
1112
  if (criteria.textContains) {
740
- const containsScore = this.scoreContainsMatch(searchable, criteria.textContains, criteria.fuzzy !== false);
1113
+ const containsScore = this.scoreContainsMatch(
1114
+ searchable,
1115
+ criteria.textContains,
1116
+ criteria.fuzzy !== false
1117
+ );
741
1118
  scores.text = Math.max(scores.text || 0, containsScore.score);
742
1119
  if (containsScore.score > 0 && containsScore.reasons.length > 0) {
743
1120
  matchReasons.push(...containsScore.reasons);
@@ -786,7 +1163,11 @@ var SearchEngine = class {
786
1163
  totalWeight += this.config.spatialWeight;
787
1164
  }
788
1165
  if (criteria.placeholder && searchable.placeholder) {
789
- const placeholderResult = fuzzyMatch(searchable.placeholder, criteria.placeholder, fuzzyConfig);
1166
+ const placeholderResult = fuzzyMatch(
1167
+ searchable.placeholder,
1168
+ criteria.placeholder,
1169
+ fuzzyConfig
1170
+ );
790
1171
  if (placeholderResult.isMatch) {
791
1172
  matchReasons.push(`placeholder matches`);
792
1173
  weightedScore += placeholderResult.similarity * this.config.textWeight;
@@ -831,11 +1212,9 @@ var SearchEngine = class {
831
1212
  scoreTextMatch(searchable, text, fuzzy, threshold) {
832
1213
  const reasons = [];
833
1214
  let maxScore = 0;
834
- const textsToMatch = [
835
- searchable.textContent,
836
- searchable.labelText,
837
- searchable.value
838
- ].filter(Boolean);
1215
+ const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
1216
+ Boolean
1217
+ );
839
1218
  for (const targetText of textsToMatch) {
840
1219
  if (targetText.toLowerCase() === text.toLowerCase()) {
841
1220
  maxScore = Math.max(maxScore, 1);
@@ -931,7 +1310,9 @@ var SearchEngine = class {
931
1310
  heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
932
1311
  };
933
1312
  const inferredRoles = tagRoleMap[normalizedRole] || [];
934
- if (inferredRoles.some((r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole)) {
1313
+ if (inferredRoles.some(
1314
+ (r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
1315
+ )) {
935
1316
  return { score: 0.8, reasons: [`inferred role: ${role}`] };
936
1317
  }
937
1318
  return { score: 0, reasons };
@@ -1118,11 +1499,14 @@ function generatePageSummary(elements, pageContext, config = {}) {
1118
1499
  if (finalConfig.includeElementCounts) {
1119
1500
  const counts = countElementTypes(elements);
1120
1501
  const countParts = [];
1121
- if (counts.button > 0) countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1502
+ if (counts.button > 0)
1503
+ countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1122
1504
  if (counts.input > 0) countParts.push(`${counts.input} input${counts.input > 1 ? "s" : ""}`);
1123
1505
  if (counts.link > 0) countParts.push(`${counts.link} link${counts.link > 1 ? "s" : ""}`);
1124
- if (counts.select > 0) countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1125
- if (counts.checkbox > 0) countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1506
+ if (counts.select > 0)
1507
+ countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1508
+ if (counts.checkbox > 0)
1509
+ countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1126
1510
  if (countParts.length > 0) {
1127
1511
  lines.push(`Contains: ${countParts.join(", ")}`);
1128
1512
  }
@@ -1165,7 +1549,9 @@ function generateFormSummary(form, verbosity) {
1165
1549
  if (verbosity === "brief") {
1166
1550
  const fieldCount = form.fields.length;
1167
1551
  const filledCount = form.fields.filter((f) => f.value).length;
1168
- lines.push(` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`);
1552
+ lines.push(
1553
+ ` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`
1554
+ );
1169
1555
  } else {
1170
1556
  for (const field of form.fields) {
1171
1557
  let fieldLine = ` - ${field.label || field.id}`;
@@ -1200,7 +1586,9 @@ function generateDiffSummary(appeared, disappeared, modified) {
1200
1586
  if (modified.length > 0) {
1201
1587
  lines.push("Changed:");
1202
1588
  for (const mod of modified.slice(0, 5)) {
1203
- lines.push(` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`);
1589
+ lines.push(
1590
+ ` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`
1591
+ );
1204
1592
  }
1205
1593
  if (modified.length > 5) {
1206
1594
  lines.push(` ... and ${modified.length - 5} more changes`);
@@ -1631,7 +2019,9 @@ function parseNLInstruction(instruction) {
1631
2019
  }
1632
2020
  }
1633
2021
  if (pattern.action === "assert") {
1634
- const assertMatch = trimmed.match(/(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i);
2022
+ const assertMatch = trimmed.match(
2023
+ /(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i
2024
+ );
1635
2025
  if (assertMatch) {
1636
2026
  parsed.assertionType = ASSERTION_TYPE_MAP[assertMatch[1].toLowerCase()];
1637
2027
  }
@@ -2392,7 +2782,9 @@ var NLActionExecutor = class {
2392
2782
  switch (errorCode) {
2393
2783
  case "PARSE_ERROR":
2394
2784
  suggestions.push('Try using a simpler phrase like "click Submit button"');
2395
- suggestions.push('Ensure the instruction follows patterns like "click X" or "type Y into X"');
2785
+ suggestions.push(
2786
+ 'Ensure the instruction follows patterns like "click X" or "type Y into X"'
2787
+ );
2396
2788
  break;
2397
2789
  case "ELEMENT_NOT_FOUND":
2398
2790
  if (alternatives.length > 0) {
@@ -2460,9 +2852,15 @@ var AssertionExecutor = class {
2460
2852
  async assert(request) {
2461
2853
  const startTime = performance.now();
2462
2854
  const timeout = request.timeout ?? this.config.defaultTimeout;
2463
- const element = await this.findElement(request.target, request.fuzzy !== false);
2855
+ const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
2856
+ const element = searchResult?.element ?? null;
2857
+ const searchDetails = searchResult ? {
2858
+ confidence: searchResult.confidence,
2859
+ matchReasons: searchResult.matchReasons,
2860
+ candidateCount: this.elements.length
2861
+ } : void 0;
2464
2862
  if (!element && request.type !== "notExists") {
2465
- return this.createResult(
2863
+ const result2 = this.createResult(
2466
2864
  false,
2467
2865
  typeof request.target === "string" ? request.target : JSON.stringify(request.target),
2468
2866
  "element not found",
@@ -2472,8 +2870,16 @@ var AssertionExecutor = class {
2472
2870
  this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
2473
2871
  startTime
2474
2872
  );
2873
+ if (searchDetails) {
2874
+ result2.searchDetails = searchDetails;
2875
+ }
2876
+ return result2;
2475
2877
  }
2476
- return this.executeAssertion(request, element, timeout, startTime);
2878
+ const result = await this.executeAssertion(request, element, timeout, startTime);
2879
+ if (searchDetails) {
2880
+ result.searchDetails = searchDetails;
2881
+ }
2882
+ return result;
2477
2883
  }
2478
2884
  /**
2479
2885
  * Execute multiple assertions
@@ -2578,16 +2984,26 @@ var AssertionExecutor = class {
2578
2984
  return this.assert({ target, type: "count", expected: expectedCount, timeout });
2579
2985
  }
2580
2986
  /**
2581
- * Find element by target (string or criteria)
2987
+ * Find element by target with full search metadata.
2988
+ * Returns the SearchResult (including confidence, matchReasons, scores)
2989
+ * or null if no match above the fuzzy threshold.
2582
2990
  */
2583
- async findElement(target, fuzzy = true) {
2991
+ findElementDetailed(target, fuzzy = true) {
2584
2992
  const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
2585
- const searchResult = this.searchEngine.findBest(criteria);
2993
+ const searchResult = this.searchEngine.findBest(criteria, this.elements);
2586
2994
  if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
2587
- return searchResult.element;
2995
+ return searchResult;
2588
2996
  }
2589
2997
  return null;
2590
2998
  }
2999
+ /**
3000
+ * Find element by target (string or criteria).
3001
+ * Public for use by condition evaluation in SpecExecutor.
3002
+ */
3003
+ async findElement(target, fuzzy = true) {
3004
+ const result = this.findElementDetailed(target, fuzzy);
3005
+ return result?.element ?? null;
3006
+ }
2591
3007
  /**
2592
3008
  * Execute the actual assertion
2593
3009
  */
@@ -2596,19 +3012,55 @@ var AssertionExecutor = class {
2596
3012
  const elementDescription = element?.description || targetStr;
2597
3013
  switch (request.type) {
2598
3014
  case "visible":
2599
- return this.assertVisibility(element, true, elementDescription, request.message, startTime);
3015
+ return this.assertVisibility(
3016
+ element,
3017
+ true,
3018
+ elementDescription,
3019
+ request.message,
3020
+ startTime
3021
+ );
2600
3022
  case "hidden":
2601
- return this.assertVisibility(element, false, elementDescription, request.message, startTime);
3023
+ return this.assertVisibility(
3024
+ element,
3025
+ false,
3026
+ elementDescription,
3027
+ request.message,
3028
+ startTime
3029
+ );
2602
3030
  case "enabled":
2603
- return this.assertEnabledState(element, true, elementDescription, request.message, startTime);
3031
+ return this.assertEnabledState(
3032
+ element,
3033
+ true,
3034
+ elementDescription,
3035
+ request.message,
3036
+ startTime
3037
+ );
2604
3038
  case "disabled":
2605
- return this.assertEnabledState(element, false, elementDescription, request.message, startTime);
3039
+ return this.assertEnabledState(
3040
+ element,
3041
+ false,
3042
+ elementDescription,
3043
+ request.message,
3044
+ startTime
3045
+ );
2606
3046
  case "focused":
2607
3047
  return this.assertFocused(element, elementDescription, request.message, startTime);
2608
3048
  case "checked":
2609
- return this.assertCheckedState(element, true, elementDescription, request.message, startTime);
3049
+ return this.assertCheckedState(
3050
+ element,
3051
+ true,
3052
+ elementDescription,
3053
+ request.message,
3054
+ startTime
3055
+ );
2610
3056
  case "unchecked":
2611
- return this.assertCheckedState(element, false, elementDescription, request.message, startTime);
3057
+ return this.assertCheckedState(
3058
+ element,
3059
+ false,
3060
+ elementDescription,
3061
+ request.message,
3062
+ startTime
3063
+ );
2612
3064
  case "hasText":
2613
3065
  return this.assertTextMatch(
2614
3066
  element,
@@ -2943,7 +3395,8 @@ var DEFAULT_SNAPSHOT_CONFIG = {
2943
3395
  detectModals: true,
2944
3396
  inferPageType: true,
2945
3397
  generateDescriptions: true,
2946
- maxElements: 500
3398
+ maxElements: 500,
3399
+ useAnnotations: true
2947
3400
  };
2948
3401
  var SemanticSnapshotManager = class {
2949
3402
  constructor(config = {}) {
@@ -3016,26 +3469,52 @@ var SemanticSnapshotManager = class {
3016
3469
  * Convert a single element to AI element
3017
3470
  */
3018
3471
  convertElement(element) {
3472
+ const isContent = element.category === "content";
3019
3473
  const aliases = generateAliases({
3020
3474
  textContent: element.state.textContent,
3021
3475
  elementType: element.type,
3022
3476
  id: element.id,
3023
3477
  labelText: element.label
3024
3478
  });
3025
- const description = this.config.generateDescriptions ? generateDescription({
3026
- textContent: element.state.textContent,
3027
- elementType: element.type,
3028
- id: element.id,
3029
- labelText: element.label
3030
- }) : element.label || element.id;
3031
- const purpose = generatePurpose({
3479
+ let description;
3480
+ if (isContent && element.contentMetadata) {
3481
+ description = this.generateContentDescription(element);
3482
+ } else if (this.config.generateDescriptions) {
3483
+ description = generateDescription({
3484
+ textContent: element.state.textContent,
3485
+ elementType: element.type,
3486
+ id: element.id,
3487
+ labelText: element.label
3488
+ });
3489
+ } else {
3490
+ description = element.label || element.id;
3491
+ }
3492
+ const purpose = isContent ? generatePurpose({ textContent: element.state.textContent, elementType: element.type }) : generatePurpose({ textContent: element.state.textContent, elementType: element.type });
3493
+ const suggestedActions = isContent ? generateSuggestedActions({
3032
3494
  textContent: element.state.textContent,
3033
3495
  elementType: element.type
3034
- });
3035
- const suggestedActions = generateSuggestedActions({
3496
+ }) : generateSuggestedActions({
3036
3497
  textContent: element.state.textContent,
3037
3498
  elementType: element.type
3038
3499
  });
3500
+ let finalDescription = description;
3501
+ let finalPurpose = purpose;
3502
+ let finalAliases = aliases;
3503
+ if (this.config.useAnnotations) {
3504
+ const annotation = getGlobalAnnotationStore().get(element.id);
3505
+ if (annotation) {
3506
+ if (annotation.description) {
3507
+ finalDescription = annotation.description;
3508
+ }
3509
+ if (annotation.purpose) {
3510
+ finalPurpose = annotation.purpose;
3511
+ }
3512
+ if (annotation.tags && annotation.tags.length > 0) {
3513
+ const tagSet = /* @__PURE__ */ new Set([...finalAliases, ...annotation.tags.map((t) => t.toLowerCase())]);
3514
+ finalAliases = [...tagSet];
3515
+ }
3516
+ }
3517
+ }
3039
3518
  return {
3040
3519
  id: element.id,
3041
3520
  type: element.type,
@@ -3046,13 +3525,56 @@ var SemanticSnapshotManager = class {
3046
3525
  actions: element.actions,
3047
3526
  state: element.state,
3048
3527
  registered: true,
3049
- description,
3050
- aliases,
3051
- purpose,
3528
+ description: finalDescription,
3529
+ aliases: finalAliases,
3530
+ purpose: finalPurpose,
3052
3531
  suggestedActions,
3053
- semanticType: this.inferSemanticType(element)
3532
+ semanticType: this.inferSemanticType(element),
3533
+ category: element.category,
3534
+ contentMetadata: element.contentMetadata
3054
3535
  };
3055
3536
  }
3537
+ /**
3538
+ * Generate a content-specific description
3539
+ */
3540
+ generateContentDescription(element) {
3541
+ const meta = element.contentMetadata;
3542
+ const text = element.state.textContent?.trim() || "";
3543
+ const truncatedText = text.length > 60 ? text.substring(0, 57) + "..." : text;
3544
+ if (!meta) return `"${truncatedText}"`;
3545
+ switch (meta.contentRole) {
3546
+ case "heading":
3547
+ return `Level ${meta.headingLevel || "?"} heading: '${truncatedText}'`;
3548
+ case "table-cell":
3549
+ return `Table cell${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3550
+ case "table-header":
3551
+ return `Table header${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3552
+ case "status":
3553
+ return `Status message: '${truncatedText}'`;
3554
+ case "badge":
3555
+ return `Badge: '${truncatedText}'`;
3556
+ case "metric":
3557
+ return `Metric value: '${truncatedText}'`;
3558
+ case "body-text":
3559
+ return `Text: '${truncatedText}'`;
3560
+ case "list-item":
3561
+ return `List item: '${truncatedText}'`;
3562
+ case "quote":
3563
+ return `Blockquote: '${truncatedText}'`;
3564
+ case "code":
3565
+ return `Code block: '${truncatedText}'`;
3566
+ case "caption":
3567
+ return `Caption: '${truncatedText}'`;
3568
+ case "label":
3569
+ return `Label: '${truncatedText}'`;
3570
+ case "description":
3571
+ return `Description: '${truncatedText}'`;
3572
+ case "navigation":
3573
+ return `Navigation text: '${truncatedText}'`;
3574
+ default:
3575
+ return `Content: '${truncatedText}'`;
3576
+ }
3577
+ }
3056
3578
  /**
3057
3579
  * Build full page context
3058
3580
  */
@@ -3156,9 +3678,7 @@ var SemanticSnapshotManager = class {
3156
3678
  */
3157
3679
  detectModals(elements) {
3158
3680
  const modals = [];
3159
- const dialogElements = elements.filter(
3160
- (el) => el.type === "dialog" && el.state.visible
3161
- );
3681
+ const dialogElements = elements.filter((el) => el.type === "dialog" && el.state.visible);
3162
3682
  for (const dialog of dialogElements) {
3163
3683
  const closeButton = elements.find(
3164
3684
  (el) => el.type === "button" && el.state.visible && (el.semanticType === "cancel-button" || el.state.textContent?.toLowerCase().match(/close|cancel|x|dismiss/))
@@ -3209,9 +3729,7 @@ var SemanticSnapshotManager = class {
3209
3729
  * Infer form purpose from fields
3210
3730
  */
3211
3731
  inferFormPurpose(fields) {
3212
- const labels = fields.map(
3213
- (f) => (f.accessibleName || f.label || "").toLowerCase()
3214
- );
3732
+ const labels = fields.map((f) => (f.accessibleName || f.label || "").toLowerCase());
3215
3733
  const allLabels = labels.join(" ");
3216
3734
  if (allLabels.includes("email") && allLabels.includes("password")) {
3217
3735
  if (allLabels.includes("confirm") || allLabels.includes("name")) {
@@ -3265,6 +3783,13 @@ var SemanticSnapshotManager = class {
3265
3783
  * Infer semantic type
3266
3784
  */
3267
3785
  inferSemanticType(element) {
3786
+ if (element.category === "content" && element.contentMetadata) {
3787
+ const role = element.contentMetadata.contentRole;
3788
+ if (role === "heading" && element.contentMetadata.headingLevel) {
3789
+ return `heading-${element.contentMetadata.headingLevel}`;
3790
+ }
3791
+ return role;
3792
+ }
3268
3793
  const text = (element.state.textContent || element.label || "").toLowerCase();
3269
3794
  const type = element.type.toLowerCase();
3270
3795
  if (type === "button") {
@@ -3346,6 +3871,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3346
3871
  const probableTrigger = detectTrigger(appeared, disappeared, limitedModifications);
3347
3872
  const suggestedActions = finalConfig.generateSuggestions ? generateSuggestedActionsFromDiff(appeared, disappeared, limitedModifications, probableTrigger) : void 0;
3348
3873
  const pageChanges = detectPageChanges(fromSnapshot, toSnapshot);
3874
+ const contentChanges = detectContentChanges(fromElements, toElements);
3349
3875
  const summary = generateDiffSummary(
3350
3876
  appeared.map((e) => e.description),
3351
3877
  disappeared.map((e) => e.description),
@@ -3360,6 +3886,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3360
3886
  disappeared,
3361
3887
  modified: limitedModifications
3362
3888
  },
3889
+ contentChanges: contentChanges || void 0,
3363
3890
  probableTrigger,
3364
3891
  suggestedActions,
3365
3892
  pageChanges,
@@ -3494,9 +4021,7 @@ function generateSuggestedActionsFromDiff(appeared, disappeared, modified, trigg
3494
4021
  suggestions.push("Fix the validation errors before submitting");
3495
4022
  }
3496
4023
  if (trigger === "Modal opened") {
3497
- const modal = appeared.find(
3498
- (e) => e.type === "dialog" || e.semanticType?.includes("dialog")
3499
- );
4024
+ const modal = appeared.find((e) => e.type === "dialog" || e.semanticType?.includes("dialog"));
3500
4025
  if (modal) {
3501
4026
  suggestions.push(`Interact with the "${modal.description}" dialog`);
3502
4027
  }
@@ -3559,76 +4084,1869 @@ var SemanticDiffManager = class {
3559
4084
  return this.lastSnapshot;
3560
4085
  }
3561
4086
  };
3562
-
3563
- // src/server/handlers.ts
3564
- function success(data) {
3565
- return {
3566
- success: true,
3567
- data,
3568
- timestamp: Date.now()
3569
- };
4087
+ var METRIC_CONTENT_TYPES = /* @__PURE__ */ new Set(["metric-value"]);
4088
+ var STATUS_CONTENT_TYPES = /* @__PURE__ */ new Set(["status-message", "badge"]);
4089
+ var HEADING_CONTENT_TYPES = /* @__PURE__ */ new Set(["heading"]);
4090
+ function isContentElement(element) {
4091
+ return element.category === "content" || element.contentMetadata !== void 0;
3570
4092
  }
3571
- function error(message, code) {
4093
+ function getContentType(element) {
4094
+ if (element.contentMetadata?.contentRole) {
4095
+ return element.contentMetadata.contentRole;
4096
+ }
4097
+ return element.type;
4098
+ }
4099
+ function detectContentChanges(fromElements, toElements) {
4100
+ const textChanges = [];
4101
+ const metricChanges = [];
4102
+ const statusChanges = [];
4103
+ for (const [id, toElement] of toElements) {
4104
+ const fromElement = fromElements.get(id);
4105
+ if (fromElement) {
4106
+ if (isContentElement(toElement) || isContentElement(fromElement)) {
4107
+ const fromText = (fromElement.state.textContent || "").trim();
4108
+ const toText = (toElement.state.textContent || "").trim();
4109
+ if (fromText !== toText) {
4110
+ const contentType = getContentType(toElement);
4111
+ const label = toElement.description || toElement.accessibleName || id;
4112
+ if (METRIC_CONTENT_TYPES.has(contentType) || contentType === "metric") {
4113
+ const parsed = parseMetricChange(fromText, toText, id, label);
4114
+ if (parsed) {
4115
+ metricChanges.push(parsed);
4116
+ }
4117
+ } else if (STATUS_CONTENT_TYPES.has(contentType) || contentType === "status") {
4118
+ statusChanges.push({
4119
+ elementId: id,
4120
+ label,
4121
+ oldStatus: fromText,
4122
+ newStatus: toText,
4123
+ direction: classifyStatusDirection(fromText, toText)
4124
+ });
4125
+ } else {
4126
+ textChanges.push({
4127
+ elementId: id,
4128
+ contentType,
4129
+ oldText: fromText,
4130
+ newText: toText,
4131
+ changeType: "modified"
4132
+ });
4133
+ }
4134
+ }
4135
+ }
4136
+ } else {
4137
+ if (isContentElement(toElement)) {
4138
+ const toText = (toElement.state.textContent || "").trim();
4139
+ if (toText) {
4140
+ textChanges.push({
4141
+ elementId: id,
4142
+ contentType: getContentType(toElement),
4143
+ oldText: "",
4144
+ newText: toText,
4145
+ changeType: "added"
4146
+ });
4147
+ }
4148
+ }
4149
+ }
4150
+ }
4151
+ for (const [id, fromElement] of fromElements) {
4152
+ if (!toElements.has(id) && isContentElement(fromElement)) {
4153
+ const fromText = (fromElement.state.textContent || "").trim();
4154
+ if (fromText) {
4155
+ textChanges.push({
4156
+ elementId: id,
4157
+ contentType: getContentType(fromElement),
4158
+ oldText: fromText,
4159
+ newText: "",
4160
+ changeType: "removed"
4161
+ });
4162
+ }
4163
+ }
4164
+ }
4165
+ if (textChanges.length === 0 && metricChanges.length === 0 && statusChanges.length === 0) {
4166
+ return null;
4167
+ }
3572
4168
  return {
3573
- success: false,
3574
- error: message,
3575
- code,
3576
- timestamp: Date.now()
4169
+ textChanges,
4170
+ metricChanges,
4171
+ statusChanges,
4172
+ summary: generateContentChangeSummary(textChanges, metricChanges, statusChanges)
3577
4173
  };
3578
4174
  }
3579
- function getRecoverySuggestions(errorCode) {
3580
- switch (errorCode) {
3581
- case "ELEMENT_NOT_FOUND":
3582
- return [
3583
- { suggestion: "Wait for the page to fully load", command: "wait for page to load", confidence: 0.7, retryable: true },
3584
- { suggestion: "Use a different description for the element", confidence: 0.8, retryable: false },
3585
- { suggestion: "Scroll the page to reveal the element", command: "scroll down", confidence: 0.6, retryable: true }
3586
- ];
3587
- case "ELEMENT_NOT_VISIBLE":
3588
- return [
3589
- { suggestion: "Scroll to make the element visible", command: "scroll to element", confidence: 0.9, retryable: true },
3590
- { suggestion: "Wait for any loading overlays to disappear", confidence: 0.7, retryable: true },
3591
- { suggestion: "Close any blocking modals or popups", command: "click close button", confidence: 0.8, retryable: true }
3592
- ];
3593
- case "ELEMENT_NOT_ENABLED":
3594
- return [
3595
- { suggestion: "Fill in required fields first", confidence: 0.8, retryable: false },
3596
- { suggestion: "Complete prerequisite steps in the form", confidence: 0.7, retryable: false },
3597
- { suggestion: "Wait for the element to become enabled", command: "wait for element to be enabled", confidence: 0.6, retryable: true }
3598
- ];
3599
- case "ELEMENT_NOT_INTERACTABLE":
3600
- return [
3601
- { suggestion: "Close any modal or popup blocking the element", command: "click close button", confidence: 0.9, retryable: true },
3602
- { suggestion: "Wait for animations to complete", confidence: 0.7, retryable: true },
3603
- { suggestion: "Scroll the element into the viewport", command: "scroll to element", confidence: 0.8, retryable: true }
3604
- ];
3605
- case "ACTION_TIMEOUT":
3606
- return [
3607
- { suggestion: "Increase the timeout duration", confidence: 0.8, retryable: true },
3608
- { suggestion: "Check if the condition can ever be met", confidence: 0.7, retryable: false },
3609
- { suggestion: "Verify the page is responding", command: "check page status", confidence: 0.6, retryable: true }
3610
- ];
3611
- case "LOW_CONFIDENCE":
3612
- return [
3613
- { suggestion: "Use the exact text shown on the element", confidence: 0.9, retryable: false },
3614
- { suggestion: "Try a different description that more closely matches the element", confidence: 0.8, retryable: false },
3615
- { suggestion: "Lower the confidence threshold if the match is correct", confidence: 0.7, retryable: true }
3616
- ];
3617
- case "AMBIGUOUS_MATCH":
3618
- return [
3619
- { suggestion: "Be more specific about which element you mean", confidence: 0.9, retryable: false },
3620
- { suggestion: "Include the section or form name in the description", confidence: 0.8, retryable: false },
3621
- { suggestion: "Use the element ID directly", confidence: 0.7, retryable: false }
3622
- ];
3623
- default:
3624
- return [
3625
- { suggestion: "Try a different approach or check the page state", confidence: 0.5, retryable: false }
3626
- ];
4175
+ function parseNumericValue(text) {
4176
+ const trimmed = text.trim();
4177
+ if (!trimmed) return null;
4178
+ let working = trimmed;
4179
+ let negate = false;
4180
+ if (working.startsWith("(") && working.endsWith(")")) {
4181
+ working = working.slice(1, -1).trim();
4182
+ negate = true;
4183
+ }
4184
+ if (working.startsWith("-")) {
4185
+ negate = !negate;
4186
+ working = working.slice(1).trim();
4187
+ }
4188
+ if (working.startsWith("+")) {
4189
+ working = working.slice(1).trim();
4190
+ }
4191
+ working = working.replace(/^[£€¥₹$]/, "").trim();
4192
+ const isPercent = working.endsWith("%");
4193
+ if (isPercent) {
4194
+ working = working.slice(0, -1).trim();
4195
+ }
4196
+ working = working.replace(/\s*(ms|s|m|h|d|hrs?|mins?|secs?|days?)$/i, "").trim();
4197
+ working = working.replace(/,/g, "");
4198
+ const num = Number(working);
4199
+ if (isNaN(num) || !isFinite(num) || working === "") {
4200
+ return null;
3627
4201
  }
4202
+ return negate ? -num : num;
3628
4203
  }
3629
- function createFailureDetails(errorCode, message, options = {}) {
3630
- const retryableErrors = [
3631
- "ELEMENT_NOT_VISIBLE",
4204
+ function parseMetricChange(fromText, toText, elementId, label) {
4205
+ const fromNum = parseNumericValue(fromText);
4206
+ const toNum = parseNumericValue(toText);
4207
+ let numericDelta;
4208
+ let percentChange;
4209
+ let significant = false;
4210
+ if (fromNum !== null && toNum !== null) {
4211
+ numericDelta = toNum - fromNum;
4212
+ if (fromNum !== 0) {
4213
+ percentChange = (toNum - fromNum) / Math.abs(fromNum) * 100;
4214
+ }
4215
+ if (percentChange !== void 0 && Math.abs(percentChange) > 10) {
4216
+ significant = true;
4217
+ }
4218
+ if (fromNum > 0 && toNum < 0) significant = true;
4219
+ if (fromNum < 0 && toNum > 0) significant = true;
4220
+ if (fromNum === 0 && toNum !== 0) significant = true;
4221
+ if (fromNum !== 0 && toNum === 0) significant = true;
4222
+ } else {
4223
+ significant = fromText !== toText;
4224
+ }
4225
+ return {
4226
+ elementId,
4227
+ label,
4228
+ oldValue: fromText,
4229
+ newValue: toText,
4230
+ numericDelta,
4231
+ percentChange: percentChange !== void 0 ? Math.round(percentChange * 100) / 100 : void 0,
4232
+ significant
4233
+ };
4234
+ }
4235
+ var STATUS_PROGRESSIONS = [
4236
+ [
4237
+ "failed",
4238
+ "error",
4239
+ "pending",
4240
+ "queued",
4241
+ "running",
4242
+ "in progress",
4243
+ "completed",
4244
+ "success",
4245
+ "done"
4246
+ ],
4247
+ ["disconnected", "connecting", "connected"],
4248
+ ["unhealthy", "degraded", "healthy"],
4249
+ ["offline", "online"],
4250
+ ["inactive", "active"],
4251
+ ["disabled", "enabled"],
4252
+ ["down", "up"],
4253
+ ["stopped", "starting", "started", "running"],
4254
+ ["closed", "open"],
4255
+ ["blocked", "unblocked"],
4256
+ ["rejected", "pending", "approved"],
4257
+ ["critical", "warning", "info", "ok"],
4258
+ ["red", "yellow", "green"]
4259
+ ];
4260
+ function classifyStatusDirection(oldStatus, newStatus) {
4261
+ const oldLower = oldStatus.toLowerCase().trim();
4262
+ const newLower = newStatus.toLowerCase().trim();
4263
+ for (const progression of STATUS_PROGRESSIONS) {
4264
+ let oldIndex = -1;
4265
+ let newIndex = -1;
4266
+ for (let i = 0; i < progression.length; i++) {
4267
+ if (oldLower.includes(progression[i])) oldIndex = i;
4268
+ if (newLower.includes(progression[i])) newIndex = i;
4269
+ }
4270
+ if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
4271
+ return newIndex > oldIndex ? "improved" : "degraded";
4272
+ }
4273
+ }
4274
+ return "neutral";
4275
+ }
4276
+ function generateContentChangeSummary(textChanges, metricChanges, statusChanges) {
4277
+ const parts = [];
4278
+ const modified = textChanges.filter((t) => t.changeType === "modified").length;
4279
+ const added = textChanges.filter((t) => t.changeType === "added").length;
4280
+ const removed = textChanges.filter((t) => t.changeType === "removed").length;
4281
+ const headingChanges = textChanges.filter(
4282
+ (t) => HEADING_CONTENT_TYPES.has(t.contentType) || t.contentType === "heading"
4283
+ );
4284
+ if (headingChanges.length > 0) {
4285
+ parts.push(`${headingChanges.length} heading${headingChanges.length > 1 ? "s" : ""} changed`);
4286
+ }
4287
+ if (metricChanges.length > 0) {
4288
+ const significantMetrics = metricChanges.filter((m) => m.significant);
4289
+ if (significantMetrics.length > 0) {
4290
+ parts.push(
4291
+ `${significantMetrics.length} metric${significantMetrics.length > 1 ? "s" : ""} changed significantly`
4292
+ );
4293
+ } else {
4294
+ parts.push(`${metricChanges.length} metric${metricChanges.length > 1 ? "s" : ""} changed`);
4295
+ }
4296
+ }
4297
+ if (statusChanges.length > 0) {
4298
+ const degraded = statusChanges.filter((s) => s.direction === "degraded");
4299
+ const improved = statusChanges.filter((s) => s.direction === "improved");
4300
+ if (degraded.length > 0) {
4301
+ parts.push(`${degraded.length} status${degraded.length > 1 ? "es" : ""} degraded`);
4302
+ }
4303
+ if (improved.length > 0) {
4304
+ parts.push(`${improved.length} status${improved.length > 1 ? "es" : ""} improved`);
4305
+ }
4306
+ const neutral = statusChanges.length - degraded.length - improved.length;
4307
+ if (neutral > 0 && degraded.length === 0 && improved.length === 0) {
4308
+ parts.push(`${neutral} status${neutral > 1 ? "es" : ""} changed`);
4309
+ }
4310
+ }
4311
+ const otherModified = modified - headingChanges.filter((h) => h.changeType === "modified").length;
4312
+ if (otherModified > 0) {
4313
+ parts.push(`${otherModified} text${otherModified > 1 ? " values" : " value"} modified`);
4314
+ }
4315
+ if (added > 0) {
4316
+ parts.push(`${added} content${added > 1 ? " elements" : " element"} added`);
4317
+ }
4318
+ if (removed > 0) {
4319
+ parts.push(`${removed} content${removed > 1 ? " elements" : " element"} removed`);
4320
+ }
4321
+ if (parts.length === 0) {
4322
+ return "No content changes";
4323
+ }
4324
+ return parts.join(", ");
4325
+ }
4326
+
4327
+ // src/ai/data-extraction.ts
4328
+ var DEFAULT_DATA_EXTRACTION_CONFIG = {
4329
+ minConfidence: 0.3,
4330
+ normalizeWhitespace: true
4331
+ };
4332
+ function classifyDataType(value) {
4333
+ const trimmed = value.trim();
4334
+ if (!trimmed) return { type: "unknown", confidence: 0 };
4335
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmed)) {
4336
+ return { type: "boolean", confidence: 0.95 };
4337
+ }
4338
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
4339
+ return { type: "email", confidence: 0.95 };
4340
+ }
4341
+ if (/^https?:\/\/\S+/.test(trimmed)) {
4342
+ return { type: "url", confidence: 0.95 };
4343
+ }
4344
+ if (/^[+]?[\d\s\-().]{7,20}$/.test(trimmed) && /\d{3,}/.test(trimmed)) {
4345
+ return { type: "phone", confidence: 0.7 };
4346
+ }
4347
+ if (/^[£$€¥₹][\s]?[\d,.]+$/.test(trimmed) || /^[\d,.]+[\s]?[£$€¥₹]$/.test(trimmed)) {
4348
+ return { type: "currency", confidence: 0.9 };
4349
+ }
4350
+ if (/^[\d,.]+\s?%$/.test(trimmed)) {
4351
+ return { type: "percentage", confidence: 0.95 };
4352
+ }
4353
+ 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)) {
4354
+ return { type: "date", confidence: 0.85 };
4355
+ }
4356
+ if (/^-?[\d,]+\.?\d*$/.test(trimmed) && trimmed !== "") {
4357
+ return { type: "number", confidence: 0.9 };
4358
+ }
4359
+ return { type: "text", confidence: 0.5 };
4360
+ }
4361
+ function normalizeValue(value, dataType) {
4362
+ const trimmed = value.trim();
4363
+ switch (dataType) {
4364
+ case "number":
4365
+ case "currency":
4366
+ case "percentage": {
4367
+ const numeric = trimmed.replace(/[^0-9.-]/g, "");
4368
+ const parsed = parseFloat(numeric);
4369
+ return isNaN(parsed) ? trimmed.toLowerCase() : parsed.toString();
4370
+ }
4371
+ case "date": {
4372
+ const d = new Date(trimmed);
4373
+ return isNaN(d.getTime()) ? trimmed.toLowerCase() : d.toISOString().split("T")[0];
4374
+ }
4375
+ case "boolean":
4376
+ return /^(true|yes|on)$/i.test(trimmed) ? "true" : "false";
4377
+ case "email":
4378
+ return trimmed.toLowerCase();
4379
+ case "url":
4380
+ return trimmed.replace(/\/+$/, "").toLowerCase();
4381
+ case "phone":
4382
+ return trimmed.replace(/[^\d+]/g, "");
4383
+ default:
4384
+ return trimmed.toLowerCase().replace(/\s+/g, " ");
4385
+ }
4386
+ }
4387
+ function extractElementValue(element) {
4388
+ const state = element.state;
4389
+ if (state?.value !== void 0 && state.value !== "") {
4390
+ return String(state.value);
4391
+ }
4392
+ if (state?.textContent !== void 0 && state.textContent !== "") {
4393
+ return String(state.textContent);
4394
+ }
4395
+ return "";
4396
+ }
4397
+ function extractLabel(element) {
4398
+ return element.accessibleName || element.labelText || element.label || element.description || element.id;
4399
+ }
4400
+ function extractPageData(elements, config = DEFAULT_DATA_EXTRACTION_CONFIG) {
4401
+ const values = {};
4402
+ let extractedCount = 0;
4403
+ for (const element of elements) {
4404
+ const rawValue = extractElementValue(element);
4405
+ if (!rawValue) continue;
4406
+ const label = extractLabel(element);
4407
+ const { type: dataType, confidence } = classifyDataType(rawValue);
4408
+ if (confidence < config.minConfidence) continue;
4409
+ const normalizedValue = normalizeValue(rawValue, dataType);
4410
+ values[label] = {
4411
+ elementId: element.id,
4412
+ label,
4413
+ rawValue: config.normalizeWhitespace ? rawValue.replace(/\s+/g, " ").trim() : rawValue,
4414
+ normalizedValue,
4415
+ dataType,
4416
+ confidence
4417
+ };
4418
+ extractedCount++;
4419
+ }
4420
+ return {
4421
+ values,
4422
+ scannedCount: elements.length,
4423
+ extractedCount
4424
+ };
4425
+ }
4426
+
4427
+ // src/ai/region-segmentation.ts
4428
+ var DEFAULT_REGION_SEGMENTATION_CONFIG = {
4429
+ minRegionElements: 1,
4430
+ headerFraction: 0.12,
4431
+ footerFraction: 0.9,
4432
+ sidebarFraction: 0.2
4433
+ };
4434
+ function toBounded(el) {
4435
+ const rect = el.state?.rect;
4436
+ if (!rect) return null;
4437
+ return {
4438
+ element: el,
4439
+ x: rect.x ?? 0,
4440
+ y: rect.y ?? 0,
4441
+ width: rect.width ?? 0,
4442
+ height: rect.height ?? 0
4443
+ };
4444
+ }
4445
+ function classifyRegionType(el, relativeY, relativeX, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4446
+ const role = (el.role || "").toLowerCase();
4447
+ const semanticType = (el.semanticType || "").toLowerCase();
4448
+ const tag = (el.tagName || "").toLowerCase();
4449
+ if (role === "navigation" || role === "nav" || tag === "nav") {
4450
+ return { type: "navigation", confidence: 0.95 };
4451
+ }
4452
+ if (role === "banner" || tag === "header") {
4453
+ return { type: "header", confidence: 0.95 };
4454
+ }
4455
+ if (role === "contentinfo" || tag === "footer") {
4456
+ return { type: "footer", confidence: 0.95 };
4457
+ }
4458
+ if (role === "main" || tag === "main") {
4459
+ return { type: "main-content", confidence: 0.95 };
4460
+ }
4461
+ if (role === "complementary" || tag === "aside") {
4462
+ return { type: "sidebar", confidence: 0.9 };
4463
+ }
4464
+ if (role === "form" || tag === "form") {
4465
+ return { type: "form", confidence: 0.9 };
4466
+ }
4467
+ if (role === "table" || tag === "table") {
4468
+ return { type: "table", confidence: 0.9 };
4469
+ }
4470
+ if (role === "dialog" || role === "alertdialog") {
4471
+ return { type: "modal", confidence: 0.95 };
4472
+ }
4473
+ if (role === "toolbar") {
4474
+ return { type: "toolbar", confidence: 0.9 };
4475
+ }
4476
+ if (semanticType.includes("card")) {
4477
+ return { type: "card", confidence: 0.8 };
4478
+ }
4479
+ if (relativeY < config.headerFraction) {
4480
+ return { type: "header", confidence: 0.6 };
4481
+ }
4482
+ if (relativeY > config.footerFraction) {
4483
+ return { type: "footer", confidence: 0.6 };
4484
+ }
4485
+ if (relativeX < config.sidebarFraction) {
4486
+ return { type: "sidebar", confidence: 0.5 };
4487
+ }
4488
+ return { type: "main-content", confidence: 0.3 };
4489
+ }
4490
+ function segmentPageRegions(elements, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4491
+ const bounded = elements.map(toBounded).filter((b) => b !== null);
4492
+ if (bounded.length === 0) {
4493
+ return { regions: [], assignedCount: 0, unassignedIds: elements.map((e) => e.id) };
4494
+ }
4495
+ let maxX = 0;
4496
+ let maxY = 0;
4497
+ for (const b of bounded) {
4498
+ maxX = Math.max(maxX, b.x + b.width);
4499
+ maxY = Math.max(maxY, b.y + b.height);
4500
+ }
4501
+ if (maxX === 0) maxX = 1;
4502
+ if (maxY === 0) maxY = 1;
4503
+ const regionGroups = /* @__PURE__ */ new Map();
4504
+ const unassignedIds = [];
4505
+ for (const b of bounded) {
4506
+ const relativeX = b.x / maxX;
4507
+ const relativeY = b.y / maxY;
4508
+ const { type, confidence } = classifyRegionType(b.element, relativeY, relativeX, config);
4509
+ if (!regionGroups.has(type)) {
4510
+ regionGroups.set(type, { elements: [], confidences: [] });
4511
+ }
4512
+ regionGroups.get(type).elements.push(b);
4513
+ regionGroups.get(type).confidences.push(confidence);
4514
+ }
4515
+ const regions = [];
4516
+ let assignedCount = 0;
4517
+ for (const [type, group] of regionGroups) {
4518
+ if (group.elements.length < config.minRegionElements) {
4519
+ for (const b of group.elements) unassignedIds.push(b.element.id);
4520
+ continue;
4521
+ }
4522
+ let minX = Infinity, minY = Infinity, maxRX = 0, maxRY = 0;
4523
+ const elementIds = [];
4524
+ for (const b of group.elements) {
4525
+ minX = Math.min(minX, b.x);
4526
+ minY = Math.min(minY, b.y);
4527
+ maxRX = Math.max(maxRX, b.x + b.width);
4528
+ maxRY = Math.max(maxRY, b.y + b.height);
4529
+ elementIds.push(b.element.id);
4530
+ }
4531
+ const avgConfidence = group.confidences.reduce((a, b) => a + b, 0) / group.confidences.length;
4532
+ regions.push({
4533
+ type,
4534
+ bounds: { x: minX, y: minY, width: maxRX - minX, height: maxRY - minY },
4535
+ elementIds,
4536
+ label: type.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
4537
+ confidence: Math.round(avgConfidence * 100) / 100
4538
+ });
4539
+ assignedCount += elementIds.length;
4540
+ }
4541
+ return { regions, assignedCount, unassignedIds };
4542
+ }
4543
+
4544
+ // src/ai/table-extraction.ts
4545
+ var DEFAULT_TABLE_EXTRACTION_CONFIG = {
4546
+ minTableColumns: 2,
4547
+ minTableRows: 2,
4548
+ minListItems: 2,
4549
+ columnTolerance: 20,
4550
+ rowTolerance: 10
4551
+ };
4552
+ function getElementBounds(el) {
4553
+ const rect = el.state?.rect;
4554
+ if (!rect || rect.width === 0) return null;
4555
+ const text = el.state?.textContent ?? el.state?.value ?? "";
4556
+ if (!text) return null;
4557
+ return {
4558
+ element: el,
4559
+ x: rect.x ?? 0,
4560
+ y: rect.y ?? 0,
4561
+ width: rect.width ?? 0,
4562
+ height: rect.height ?? 0,
4563
+ text: text.trim()
4564
+ };
4565
+ }
4566
+ function clusterPositions(values, tolerance) {
4567
+ if (values.length === 0) return [];
4568
+ const sorted = [...values].sort((a, b) => a - b);
4569
+ const clusters = [sorted[0]];
4570
+ for (let i = 1; i < sorted.length; i++) {
4571
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
4572
+ clusters.push(sorted[i]);
4573
+ }
4574
+ }
4575
+ return clusters;
4576
+ }
4577
+ function assignToCluster(value, clusters, tolerance) {
4578
+ let best = 0;
4579
+ let bestDist = Math.abs(value - clusters[0]);
4580
+ for (let i = 1; i < clusters.length; i++) {
4581
+ const dist = Math.abs(value - clusters[i]);
4582
+ if (dist < bestDist) {
4583
+ bestDist = dist;
4584
+ best = i;
4585
+ }
4586
+ }
4587
+ return bestDist <= tolerance ? best : -1;
4588
+ }
4589
+ function detectTable(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4590
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4591
+ if (withBounds.length < config.minTableColumns * config.minTableRows) return null;
4592
+ const xPositions = withBounds.map((b) => b.x);
4593
+ const yPositions = withBounds.map((b) => b.y);
4594
+ const columnClusters = clusterPositions(xPositions, config.columnTolerance);
4595
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4596
+ if (columnClusters.length < config.minTableColumns || rowClusters.length < config.minTableRows) {
4597
+ return null;
4598
+ }
4599
+ const grid = Array.from(
4600
+ { length: rowClusters.length },
4601
+ () => Array(columnClusters.length).fill(null)
4602
+ );
4603
+ for (const b of withBounds) {
4604
+ const col = assignToCluster(b.x, columnClusters, config.columnTolerance);
4605
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4606
+ if (col >= 0 && row >= 0 && grid[row][col] === null) {
4607
+ grid[row][col] = b.text;
4608
+ }
4609
+ }
4610
+ const headers = grid[0].map((h) => h ?? "");
4611
+ const columns = headers.map((header, index) => {
4612
+ const bodyCells = grid.slice(1).map((r) => r[index]).filter((c) => c !== null);
4613
+ const types = bodyCells.map((c) => classifyDataType(c).type);
4614
+ const mostCommon = mode(types) ?? "text";
4615
+ return { header, index, dataType: mostCommon };
4616
+ });
4617
+ const rows = grid.slice(1).map((row) => row.map((cell) => cell ?? ""));
4618
+ return {
4619
+ label: headers[0] || "Table",
4620
+ columns,
4621
+ rows
4622
+ };
4623
+ }
4624
+ function detectList(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4625
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4626
+ if (withBounds.length < config.minListItems) return null;
4627
+ const sorted = [...withBounds].sort((a, b) => a.y - b.y);
4628
+ const yPositions = sorted.map((b) => b.y);
4629
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4630
+ if (rowClusters.length < config.minListItems) return null;
4631
+ const rowGroups = /* @__PURE__ */ new Map();
4632
+ for (const b of sorted) {
4633
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4634
+ if (row >= 0) {
4635
+ if (!rowGroups.has(row)) rowGroups.set(row, []);
4636
+ rowGroups.get(row).push(b);
4637
+ }
4638
+ }
4639
+ const items = [];
4640
+ const fieldLabels = [];
4641
+ let fieldLabelsInitialized = false;
4642
+ for (const [, rowElements] of [...rowGroups.entries()].sort(([a], [b]) => a - b)) {
4643
+ const sortedRow = [...rowElements].sort((a, b) => a.x - b.x);
4644
+ const item = {};
4645
+ for (let i = 0; i < sortedRow.length; i++) {
4646
+ const label = `field_${i}`;
4647
+ if (!fieldLabelsInitialized) fieldLabels.push(label);
4648
+ item[label] = sortedRow[i].text;
4649
+ }
4650
+ fieldLabelsInitialized = true;
4651
+ items.push(item);
4652
+ }
4653
+ if (items.length < config.minListItems) return null;
4654
+ const fields = fieldLabels.map((label) => {
4655
+ const values = items.map((item) => item[label]).filter(Boolean);
4656
+ const types = values.map((v) => classifyDataType(v).type);
4657
+ return { label, dataType: mode(types) ?? "text" };
4658
+ });
4659
+ return {
4660
+ label: "List",
4661
+ fields,
4662
+ items
4663
+ };
4664
+ }
4665
+ function extractStructuredData(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4666
+ const tables = [];
4667
+ const lists = [];
4668
+ const table = detectTable(elements, config);
4669
+ if (table) {
4670
+ tables.push(table);
4671
+ }
4672
+ const listCandidates = elements.filter((el) => {
4673
+ const role = el.role || el.type;
4674
+ return ["listitem", "row", "option", "link", "button"].includes(role);
4675
+ });
4676
+ if (listCandidates.length >= config.minListItems) {
4677
+ const list = detectList(listCandidates, config);
4678
+ if (list) {
4679
+ lists.push(list);
4680
+ }
4681
+ }
4682
+ return { tables, lists };
4683
+ }
4684
+ function mode(arr) {
4685
+ if (arr.length === 0) return void 0;
4686
+ const counts = /* @__PURE__ */ new Map();
4687
+ let best = arr[0];
4688
+ let bestCount = 0;
4689
+ for (const v of arr) {
4690
+ const c = (counts.get(v) ?? 0) + 1;
4691
+ counts.set(v, c);
4692
+ if (c > bestCount) {
4693
+ bestCount = c;
4694
+ best = v;
4695
+ }
4696
+ }
4697
+ return best;
4698
+ }
4699
+
4700
+ // src/ai/format-analysis.ts
4701
+ var DEFAULT_FORMAT_ANALYSIS_CONFIG = {
4702
+ lenientFormatting: true
4703
+ };
4704
+ function detectFormatPattern(value, dataType) {
4705
+ const trimmed = value.trim();
4706
+ switch (dataType) {
4707
+ case "currency": {
4708
+ const hasLeadingSymbol = /^[£$€¥₹]/.test(trimmed);
4709
+ const hasTrailingSymbol = /[£$€¥₹]$/.test(trimmed);
4710
+ const usesCommaThousands = /\d{1,3}(,\d{3})+/.test(trimmed);
4711
+ const usesPeriodThousands = /\d{1,3}(\.\d{3})+,/.test(trimmed);
4712
+ let pattern = hasLeadingSymbol ? "$" : "";
4713
+ if (usesCommaThousands) pattern += "#,###";
4714
+ else if (usesPeriodThousands) pattern += "#.###";
4715
+ else pattern += "#";
4716
+ if (/\.\d{2}$/.test(trimmed)) pattern += ".##";
4717
+ else if (/,\d{2}$/.test(trimmed)) pattern += ",##";
4718
+ if (hasTrailingSymbol) pattern += "$";
4719
+ return pattern;
4720
+ }
4721
+ case "date": {
4722
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return "YYYY-MM-DD";
4723
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(trimmed)) return "MM/DD/YYYY";
4724
+ if (/^\d{2}\.\d{2}\.\d{4}$/.test(trimmed)) return "DD.MM.YYYY";
4725
+ if (/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(trimmed)) return "M/D/YY";
4726
+ if (/^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) return "Month DD, YYYY";
4727
+ return "date";
4728
+ }
4729
+ case "percentage":
4730
+ return /\s%$/.test(trimmed) ? "#.## %" : "#.##%";
4731
+ case "number": {
4732
+ const hasCommas = /,/.test(trimmed);
4733
+ const decimalPlaces = trimmed.includes(".") ? trimmed.split(".")[1]?.length || 0 : 0;
4734
+ return (hasCommas ? "#,###" : "#") + (decimalPlaces > 0 ? "." + "#".repeat(decimalPlaces) : "");
4735
+ }
4736
+ case "phone": {
4737
+ if (/^\(\d{3}\)\s?\d{3}-\d{4}$/.test(trimmed)) return "(###) ###-####";
4738
+ if (/^\d{3}-\d{3}-\d{4}$/.test(trimmed)) return "###-###-####";
4739
+ if (/^\+\d/.test(trimmed)) return "+# ###...";
4740
+ return "phone";
4741
+ }
4742
+ default:
4743
+ return dataType;
4744
+ }
4745
+ }
4746
+ function analyzeFormat(elementId, label, rawValue) {
4747
+ const { type: dataType } = classifyDataType(rawValue);
4748
+ const pattern = detectFormatPattern(rawValue, dataType);
4749
+ return {
4750
+ elementId,
4751
+ label,
4752
+ dataType,
4753
+ pattern,
4754
+ example: rawValue.trim()
4755
+ };
4756
+ }
4757
+ function analyzePageFormats(elements) {
4758
+ const descriptors = [];
4759
+ for (const el of elements) {
4760
+ const rawValue = el.state?.value ?? el.state?.textContent ?? "";
4761
+ if (!rawValue) continue;
4762
+ const label = el.accessibleName || el.labelText || el.label || el.description || el.id;
4763
+ descriptors.push(analyzeFormat(el.id, label, rawValue));
4764
+ }
4765
+ return descriptors;
4766
+ }
4767
+ function compareFormats(sourceFormats, targetFormats, config = DEFAULT_FORMAT_ANALYSIS_CONFIG) {
4768
+ const mismatches = [];
4769
+ const targetByLabel = /* @__PURE__ */ new Map();
4770
+ for (const t of targetFormats) {
4771
+ targetByLabel.set(t.label.toLowerCase(), t);
4772
+ }
4773
+ for (const source of sourceFormats) {
4774
+ const target = targetByLabel.get(source.label.toLowerCase());
4775
+ if (!target) continue;
4776
+ if (source.dataType !== target.dataType) {
4777
+ mismatches.push({
4778
+ label: source.label,
4779
+ sourceFormat: source,
4780
+ targetFormat: target,
4781
+ severity: "error",
4782
+ description: `Data type mismatch: source is ${source.dataType}, target is ${target.dataType}`
4783
+ });
4784
+ continue;
4785
+ }
4786
+ if (source.pattern !== target.pattern) {
4787
+ const severity = config.lenientFormatting ? "warning" : "error";
4788
+ mismatches.push({
4789
+ label: source.label,
4790
+ sourceFormat: source,
4791
+ targetFormat: target,
4792
+ severity,
4793
+ description: `Format differs: source uses "${source.pattern}", target uses "${target.pattern}"`
4794
+ });
4795
+ }
4796
+ }
4797
+ return mismatches;
4798
+ }
4799
+
4800
+ // src/ai/cross-app-diff.ts
4801
+ var DEFAULT_CROSS_APP_DIFF_CONFIG = {
4802
+ matchThreshold: 0.5,
4803
+ accessibleNameWeight: 1,
4804
+ textWeight: 0.95,
4805
+ rolePositionWeight: 0.7
4806
+ };
4807
+ function getElementText(el) {
4808
+ return el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "";
4809
+ }
4810
+ function getRole(el) {
4811
+ return (el.role || el.type || "").toLowerCase();
4812
+ }
4813
+ function getCenter(el) {
4814
+ const rect = el.state?.rect;
4815
+ if (!rect) return null;
4816
+ return {
4817
+ x: rect.x + rect.width / 2,
4818
+ y: rect.y + rect.height / 2
4819
+ };
4820
+ }
4821
+ function computeMatchScore(source, target, config) {
4822
+ let bestScore = 0;
4823
+ let bestStrategy = "none";
4824
+ const srcName = (source.accessibleName || "").trim();
4825
+ const tgtName = (target.accessibleName || "").trim();
4826
+ if (srcName && tgtName && srcName.toLowerCase() === tgtName.toLowerCase()) {
4827
+ return { score: config.accessibleNameWeight, strategy: "accessible-name-exact" };
4828
+ }
4829
+ const srcText = getElementText(source);
4830
+ const tgtText = getElementText(target);
4831
+ if (srcText && tgtText && srcText.toLowerCase() === tgtText.toLowerCase()) {
4832
+ const score = config.textWeight;
4833
+ if (score > bestScore) {
4834
+ bestScore = score;
4835
+ bestStrategy = "text-exact";
4836
+ }
4837
+ }
4838
+ if (srcText && tgtText) {
4839
+ const srcNorm = normalizeString(srcText);
4840
+ const tgtNorm = normalizeString(tgtText);
4841
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
4842
+ const score = similarity * 0.85;
4843
+ if (score > bestScore) {
4844
+ bestScore = score;
4845
+ bestStrategy = "text-fuzzy";
4846
+ }
4847
+ }
4848
+ const srcRole = getRole(source);
4849
+ const tgtRole = getRole(target);
4850
+ if (srcRole && srcRole === tgtRole) {
4851
+ const srcCenter = getCenter(source);
4852
+ const tgtCenter = getCenter(target);
4853
+ if (srcCenter && tgtCenter) {
4854
+ const dx = Math.abs(srcCenter.x - tgtCenter.x) / 1920;
4855
+ const dy = Math.abs(srcCenter.y - tgtCenter.y) / 1080;
4856
+ const posSimilarity = 1 - Math.min(1, Math.sqrt(dx * dx + dy * dy));
4857
+ const score = config.rolePositionWeight * posSimilarity;
4858
+ if (score > bestScore) {
4859
+ bestScore = score;
4860
+ bestStrategy = "role-position";
4861
+ }
4862
+ }
4863
+ }
4864
+ const srcVal = source.state?.value ?? source.state?.textContent ?? "";
4865
+ const tgtVal = target.state?.value ?? target.state?.textContent ?? "";
4866
+ if (srcVal && tgtVal) {
4867
+ const srcType = classifyDataType(srcVal).type;
4868
+ const tgtType = classifyDataType(tgtVal).type;
4869
+ const srcNorm = normalizeValue(srcVal, srcType);
4870
+ const tgtNorm = normalizeValue(tgtVal, tgtType);
4871
+ if (srcNorm === tgtNorm && srcNorm !== "") {
4872
+ const score = 0.6;
4873
+ if (score > bestScore) {
4874
+ bestScore = score;
4875
+ bestStrategy = "data-overlap";
4876
+ }
4877
+ }
4878
+ }
4879
+ return { score: bestScore, strategy: bestStrategy };
4880
+ }
4881
+ function matchElements(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
4882
+ const candidates = [];
4883
+ for (let si = 0; si < sourceElements.length; si++) {
4884
+ for (let ti = 0; ti < targetElements.length; ti++) {
4885
+ const { score, strategy } = computeMatchScore(sourceElements[si], targetElements[ti], config);
4886
+ if (score >= config.matchThreshold) {
4887
+ candidates.push({ sourceIdx: si, targetIdx: ti, score, strategy });
4888
+ }
4889
+ }
4890
+ }
4891
+ candidates.sort((a, b) => b.score - a.score);
4892
+ const usedSource = /* @__PURE__ */ new Set();
4893
+ const usedTarget = /* @__PURE__ */ new Set();
4894
+ const pairs = [];
4895
+ for (const c of candidates) {
4896
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
4897
+ usedSource.add(c.sourceIdx);
4898
+ usedTarget.add(c.targetIdx);
4899
+ const src = sourceElements[c.sourceIdx];
4900
+ const tgt = targetElements[c.targetIdx];
4901
+ pairs.push({
4902
+ sourceId: src.id,
4903
+ targetId: tgt.id,
4904
+ sourceLabel: getElementText(src) || src.id,
4905
+ targetLabel: getElementText(tgt) || tgt.id,
4906
+ confidence: Math.round(c.score * 100) / 100,
4907
+ matchStrategy: c.strategy
4908
+ });
4909
+ }
4910
+ return pairs;
4911
+ }
4912
+ function computeCrossAppDiff(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
4913
+ const matchedPairs = matchElements(sourceElements, targetElements, config);
4914
+ const matchedSourceIds = new Set(matchedPairs.map((p) => p.sourceId));
4915
+ const matchedTargetIds = new Set(matchedPairs.map((p) => p.targetId));
4916
+ const unmatchedSourceIds = sourceElements.filter((e) => !matchedSourceIds.has(e.id)).map((e) => e.id);
4917
+ const unmatchedTargetIds = targetElements.filter((e) => !matchedTargetIds.has(e.id)).map((e) => e.id);
4918
+ const sourceData = extractPageData(sourceElements);
4919
+ const targetData = extractPageData(targetElements);
4920
+ const dataComparisons = [];
4921
+ for (const pair of matchedPairs) {
4922
+ const srcEntry = Object.values(sourceData.values).find((v) => v.elementId === pair.sourceId);
4923
+ const tgtEntry = Object.values(targetData.values).find((v) => v.elementId === pair.targetId);
4924
+ if (srcEntry && tgtEntry) {
4925
+ dataComparisons.push({
4926
+ label: pair.sourceLabel,
4927
+ sourceValue: srcEntry.rawValue,
4928
+ targetValue: tgtEntry.rawValue,
4929
+ valuesMatch: srcEntry.normalizedValue === tgtEntry.normalizedValue,
4930
+ formatsMatch: srcEntry.dataType === tgtEntry.dataType
4931
+ });
4932
+ }
4933
+ }
4934
+ const sourceFormats = analyzePageFormats(sourceElements);
4935
+ const targetFormats = analyzePageFormats(targetElements);
4936
+ const formatMismatches = compareFormats(sourceFormats, targetFormats);
4937
+ return {
4938
+ matchedPairs,
4939
+ unmatchedSourceIds,
4940
+ unmatchedTargetIds,
4941
+ dataComparisons,
4942
+ formatMismatches
4943
+ };
4944
+ }
4945
+
4946
+ // src/ai/action-parity.ts
4947
+ var DEFAULT_ACTION_PARITY_CONFIG = {
4948
+ ignoreActions: []
4949
+ };
4950
+ function getActions(el, ignoreActions) {
4951
+ const actions = el.actions || el.suggestedActions || [];
4952
+ const ignoreSet = new Set(ignoreActions.map((a) => a.toLowerCase()));
4953
+ return actions.map(
4954
+ (a) => typeof a === "string" ? a : a.action || a.name || ""
4955
+ ).filter((a) => a && !ignoreSet.has(a.toLowerCase()));
4956
+ }
4957
+ function analyzeActionParity(matchedPairs, sourceElements, targetElements, config = DEFAULT_ACTION_PARITY_CONFIG) {
4958
+ const sourceById = new Map(sourceElements.map((e) => [e.id, e]));
4959
+ const targetById = new Map(targetElements.map((e) => [e.id, e]));
4960
+ const results = [];
4961
+ for (const pair of matchedPairs) {
4962
+ const src = sourceById.get(pair.sourceId);
4963
+ const tgt = targetById.get(pair.targetId);
4964
+ if (!src || !tgt) continue;
4965
+ const sourceActions = getActions(src, config.ignoreActions);
4966
+ const targetActions = getActions(tgt, config.ignoreActions);
4967
+ const sourceSet = new Set(sourceActions.map((a) => a.toLowerCase()));
4968
+ const targetSet = new Set(targetActions.map((a) => a.toLowerCase()));
4969
+ const missingInTarget = sourceActions.filter((a) => !targetSet.has(a.toLowerCase()));
4970
+ const missingInSource = targetActions.filter((a) => !sourceSet.has(a.toLowerCase()));
4971
+ results.push({
4972
+ pair,
4973
+ sourceActions,
4974
+ targetActions,
4975
+ missingInTarget,
4976
+ missingInSource
4977
+ });
4978
+ }
4979
+ return results;
4980
+ }
4981
+
4982
+ // src/ai/navigation-map.ts
4983
+ var DEFAULT_NAVIGATION_MAP_CONFIG = {
4984
+ labelMatchThreshold: 0.8
4985
+ };
4986
+ function isNavigationElement(el) {
4987
+ const role = (el.role || "").toLowerCase();
4988
+ const type = (el.type || "").toLowerCase();
4989
+ const semanticType = (el.semanticType || "").toLowerCase();
4990
+ if (["link", "menuitem", "tab"].includes(role)) return true;
4991
+ if (["link", "menuitem"].includes(type)) return true;
4992
+ if (semanticType.includes("nav") || semanticType.includes("menu") || semanticType.includes("tab")) {
4993
+ return true;
4994
+ }
4995
+ const context = (el.parentContext || "").toLowerCase();
4996
+ if (context.includes("nav") || context.includes("menu") || context.includes("sidebar")) {
4997
+ if (role === "button" || type === "button" || role === "link" || type === "link") {
4998
+ return true;
4999
+ }
5000
+ }
5001
+ return false;
5002
+ }
5003
+ function getNavLabel(el) {
5004
+ return el.accessibleName || el.labelText || el.label || el.description || el.id;
5005
+ }
5006
+ function getHref(el) {
5007
+ const state = el.state;
5008
+ return state?.href || void 0;
5009
+ }
5010
+ function hrefsMatch(a, b) {
5011
+ if (!a || !b) return false;
5012
+ const normalize = (h) => h.replace(/^https?:\/\//, "").replace(/localhost:\d+/, "").replace(/\/+$/, "").toLowerCase();
5013
+ return normalize(a) === normalize(b);
5014
+ }
5015
+ function buildNavigationMap(sourceElements, targetElements, config = DEFAULT_NAVIGATION_MAP_CONFIG) {
5016
+ const sourceNav = sourceElements.filter(isNavigationElement);
5017
+ const targetNav = targetElements.filter(isNavigationElement);
5018
+ const pairs = [];
5019
+ const matchedTargetIds = /* @__PURE__ */ new Set();
5020
+ for (const src of sourceNav) {
5021
+ const srcLabel = getNavLabel(src);
5022
+ const srcNorm = normalizeString(srcLabel);
5023
+ let bestTarget = null;
5024
+ let bestScore = 0;
5025
+ for (const tgt of targetNav) {
5026
+ if (matchedTargetIds.has(tgt.id)) continue;
5027
+ const tgtLabel = getNavLabel(tgt);
5028
+ const tgtNorm = normalizeString(tgtLabel);
5029
+ if (srcNorm === tgtNorm) {
5030
+ bestTarget = tgt;
5031
+ bestScore = 1;
5032
+ break;
5033
+ }
5034
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
5035
+ if (similarity > bestScore && similarity >= config.labelMatchThreshold) {
5036
+ bestScore = similarity;
5037
+ bestTarget = tgt;
5038
+ }
5039
+ }
5040
+ if (bestTarget) {
5041
+ matchedTargetIds.add(bestTarget.id);
5042
+ const srcHref = getHref(src);
5043
+ const tgtHref = getHref(bestTarget);
5044
+ pairs.push({
5045
+ sourceId: src.id,
5046
+ targetId: bestTarget.id,
5047
+ label: srcLabel,
5048
+ sourceHref: srcHref,
5049
+ targetHref: tgtHref,
5050
+ destinationMatch: hrefsMatch(srcHref, tgtHref)
5051
+ });
5052
+ }
5053
+ }
5054
+ const sourceOnly = sourceNav.filter((s) => !pairs.some((p) => p.sourceId === s.id)).map((s) => s.id);
5055
+ const targetOnly = targetNav.filter((t) => !matchedTargetIds.has(t.id)).map((t) => t.id);
5056
+ return { pairs, sourceOnly, targetOnly };
5057
+ }
5058
+
5059
+ // src/ai/component-comparison.ts
5060
+ var DEFAULT_COMPONENT_COMPARISON_CONFIG = {
5061
+ nameMatchThreshold: 0.75
5062
+ };
5063
+ function computeComponentMatchScore(source, target) {
5064
+ if (source.name.toLowerCase() === target.name.toLowerCase()) return 1;
5065
+ let score = 0;
5066
+ if (source.type === target.type) {
5067
+ score += 0.3;
5068
+ }
5069
+ const nameSimilarity = jaroWinklerSimilarity(
5070
+ normalizeString(source.name),
5071
+ normalizeString(target.name)
5072
+ );
5073
+ score += nameSimilarity * 0.7;
5074
+ return score;
5075
+ }
5076
+ function compareComponents(sourceComponents, targetComponents, config = DEFAULT_COMPONENT_COMPARISON_CONFIG) {
5077
+ const candidates = [];
5078
+ for (let si = 0; si < sourceComponents.length; si++) {
5079
+ for (let ti = 0; ti < targetComponents.length; ti++) {
5080
+ const score = computeComponentMatchScore(sourceComponents[si], targetComponents[ti]);
5081
+ if (score >= config.nameMatchThreshold) {
5082
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5083
+ }
5084
+ }
5085
+ }
5086
+ candidates.sort((a, b) => b.score - a.score);
5087
+ const usedSource = /* @__PURE__ */ new Set();
5088
+ const usedTarget = /* @__PURE__ */ new Set();
5089
+ const matches = [];
5090
+ for (const c of candidates) {
5091
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5092
+ usedSource.add(c.sourceIdx);
5093
+ usedTarget.add(c.targetIdx);
5094
+ const src = sourceComponents[c.sourceIdx];
5095
+ const tgt = targetComponents[c.targetIdx];
5096
+ const srcKeys = new Set(src.stateKeys);
5097
+ const tgtKeys = new Set(tgt.stateKeys);
5098
+ const missingKeys = src.stateKeys.filter((k) => !tgtKeys.has(k));
5099
+ const extraKeys = tgt.stateKeys.filter((k) => !srcKeys.has(k));
5100
+ const srcActions = new Set(src.actions.map((a) => a.toLowerCase()));
5101
+ const tgtActions = new Set(tgt.actions.map((a) => a.toLowerCase()));
5102
+ const missingActions = src.actions.filter((a) => !tgtActions.has(a.toLowerCase()));
5103
+ const extraActions = tgt.actions.filter((a) => !srcActions.has(a.toLowerCase()));
5104
+ matches.push({
5105
+ source: src,
5106
+ target: tgt,
5107
+ confidence: Math.round(c.score * 100) / 100,
5108
+ stateKeyDiff: { missing: missingKeys, extra: extraKeys },
5109
+ actionDiff: { missing: missingActions, extra: extraActions }
5110
+ });
5111
+ }
5112
+ const sourceOnly = sourceComponents.filter((_, i) => !usedSource.has(i));
5113
+ const targetOnly = targetComponents.filter((_, i) => !usedTarget.has(i));
5114
+ return { matches, sourceOnly, targetOnly };
5115
+ }
5116
+
5117
+ // src/ai/layout-comparison.ts
5118
+ var DEFAULT_LAYOUT_COMPARISON_CONFIG = {
5119
+ gridTolerance: 20
5120
+ };
5121
+ function getRect(el) {
5122
+ const rect = el.state?.rect;
5123
+ if (!rect || !rect.width) return null;
5124
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
5125
+ }
5126
+ function clusterValues(values, tolerance) {
5127
+ if (values.length === 0) return [];
5128
+ const sorted = [...values].sort((a, b) => a - b);
5129
+ const clusters = [sorted[0]];
5130
+ for (let i = 1; i < sorted.length; i++) {
5131
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
5132
+ clusters.push(sorted[i]);
5133
+ }
5134
+ }
5135
+ return clusters;
5136
+ }
5137
+ function detectGridStructure(elements, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5138
+ const rects = elements.map(getRect).filter((r) => r !== null);
5139
+ const xPositions = rects.map((r) => r.x);
5140
+ const yPositions = rects.map((r) => r.y);
5141
+ const columns = clusterValues(xPositions, config.gridTolerance);
5142
+ const rows = clusterValues(yPositions, config.gridTolerance);
5143
+ return {
5144
+ columns,
5145
+ rows,
5146
+ columnCount: columns.length,
5147
+ rowCount: rows.length
5148
+ };
5149
+ }
5150
+ function computeMaxDepth(elements) {
5151
+ let maxDepth = 0;
5152
+ for (const el of elements) {
5153
+ const context = el.parentContext || "";
5154
+ const depth = context ? context.split(">").length : 1;
5155
+ maxDepth = Math.max(maxDepth, depth);
5156
+ }
5157
+ return maxDepth;
5158
+ }
5159
+ function compareLayouts(sourceElements, targetElements, sourceRegions, targetRegions, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5160
+ const sourceGrid = detectGridStructure(sourceElements, config);
5161
+ const targetGrid = detectGridStructure(targetElements, config);
5162
+ const gridDiff = {
5163
+ sourceGrid,
5164
+ targetGrid,
5165
+ columnDiff: sourceGrid.columnCount - targetGrid.columnCount,
5166
+ rowDiff: sourceGrid.rowCount - targetGrid.rowCount
5167
+ };
5168
+ const sourceDepth = computeMaxDepth(sourceElements);
5169
+ const targetDepth = computeMaxDepth(targetElements);
5170
+ const hierarchyDiff = {
5171
+ sourceDepth,
5172
+ targetDepth,
5173
+ depthDiff: sourceDepth - targetDepth
5174
+ };
5175
+ const sourceRegionCount = sourceRegions?.regions.length || 1;
5176
+ const targetRegionCount = targetRegions?.regions.length || 1;
5177
+ const sourceDensity = sourceElements.length / sourceRegionCount;
5178
+ const targetDensity = targetElements.length / targetRegionCount;
5179
+ const density = {
5180
+ sourceDensity: Math.round(sourceDensity * 100) / 100,
5181
+ targetDensity: Math.round(targetDensity * 100) / 100,
5182
+ ratio: targetDensity > 0 ? Math.round(sourceDensity / targetDensity * 100) / 100 : 0
5183
+ };
5184
+ const gridSimilarity = sourceGrid.columnCount === 0 && targetGrid.columnCount === 0 ? 1 : 1 - Math.abs(gridDiff.columnDiff) / Math.max(sourceGrid.columnCount, targetGrid.columnCount, 1);
5185
+ const hierarchySimilarity = sourceDepth === 0 && targetDepth === 0 ? 1 : 1 - Math.abs(hierarchyDiff.depthDiff) / Math.max(sourceDepth, targetDepth, 1);
5186
+ const densitySimilarity = density.ratio > 0 ? Math.min(density.ratio, 1 / density.ratio) : 0;
5187
+ const similarity = Math.round((gridSimilarity * 0.4 + hierarchySimilarity * 0.3 + densitySimilarity * 0.3) * 100) / 100;
5188
+ return {
5189
+ gridDiff,
5190
+ hierarchyDiff,
5191
+ density,
5192
+ similarity
5193
+ };
5194
+ }
5195
+
5196
+ // src/ai/content-comparison.ts
5197
+ var DEFAULT_CONTENT_COMPARISON_CONFIG = {
5198
+ labelMatchThreshold: 0.8,
5199
+ headingMatchThreshold: 0.75,
5200
+ maxCellDifferences: 50
5201
+ };
5202
+ function getElementText2(el) {
5203
+ return (el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "").trim();
5204
+ }
5205
+ function getContentRole(el) {
5206
+ if (el.contentMetadata?.contentRole) {
5207
+ return el.contentMetadata.contentRole;
5208
+ }
5209
+ const t = (el.type || "").toLowerCase();
5210
+ if (t === "heading" || t.startsWith("h") && /^h[1-6]$/.test(t)) return "heading";
5211
+ if (t === "metric-value" || t === "metric") return "metric";
5212
+ if (t === "status-message" || t === "status") return "status";
5213
+ if (t === "label") return "label";
5214
+ if (t === "badge") return "badge";
5215
+ if (t === "table-cell") return "table-cell";
5216
+ if (t === "table-header") return "table-header";
5217
+ if (t === "caption") return "caption";
5218
+ return null;
5219
+ }
5220
+ function getHeadingLevel(el) {
5221
+ if (el.contentMetadata?.headingLevel) {
5222
+ return el.contentMetadata.headingLevel;
5223
+ }
5224
+ const tag = (el.tagName || el.type || "").toLowerCase();
5225
+ const match = /^h([1-6])$/.exec(tag);
5226
+ if (match) return parseInt(match[1], 10);
5227
+ return void 0;
5228
+ }
5229
+ function isContentElement2(el) {
5230
+ if (el.category === "content") return true;
5231
+ if (el.contentMetadata) return true;
5232
+ const role = getContentRole(el);
5233
+ return role !== null;
5234
+ }
5235
+ function normalizeText(text) {
5236
+ return normalizeString(text, { caseSensitive: false, ignoreWhitespace: true });
5237
+ }
5238
+ function parseMetricText(el) {
5239
+ const text = getElementText2(el);
5240
+ const colonMatch = text.match(/^(.+?):\s*(.+)$/);
5241
+ if (colonMatch) {
5242
+ return { label: colonMatch[1].trim(), value: colonMatch[2].trim() };
5243
+ }
5244
+ const dashMatch = text.match(/^(.+?)\s*[-]\s*(.+)$/);
5245
+ if (dashMatch) {
5246
+ return { label: dashMatch[1].trim(), value: dashMatch[2].trim() };
5247
+ }
5248
+ const elLabel = el.accessibleName || el.labelText || el.label || el.id;
5249
+ return { label: elLabel, value: text };
5250
+ }
5251
+ function filterHeadings(elements) {
5252
+ return elements.filter((el) => getContentRole(el) === "heading");
5253
+ }
5254
+ function filterMetrics(elements) {
5255
+ return elements.filter((el) => getContentRole(el) === "metric");
5256
+ }
5257
+ function filterStatuses(elements) {
5258
+ return elements.filter((el) => {
5259
+ const role = getContentRole(el);
5260
+ return role === "status" || role === "badge";
5261
+ });
5262
+ }
5263
+ function filterLabels(elements) {
5264
+ return elements.filter((el) => {
5265
+ const role = getContentRole(el);
5266
+ return role === "label" || role === "caption";
5267
+ });
5268
+ }
5269
+ function matchTexts(sourceTexts, targetTexts, threshold) {
5270
+ const candidates = [];
5271
+ for (let si = 0; si < sourceTexts.length; si++) {
5272
+ const sNorm = normalizeText(sourceTexts[si]);
5273
+ if (!sNorm) continue;
5274
+ for (let ti = 0; ti < targetTexts.length; ti++) {
5275
+ const tNorm = normalizeText(targetTexts[ti]);
5276
+ if (!tNorm) continue;
5277
+ const score = sNorm === tNorm ? 1 : jaroWinklerSimilarity(sNorm, tNorm);
5278
+ if (score >= threshold) {
5279
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5280
+ }
5281
+ }
5282
+ }
5283
+ candidates.sort((a, b) => b.score - a.score);
5284
+ const usedSource = /* @__PURE__ */ new Set();
5285
+ const usedTarget = /* @__PURE__ */ new Set();
5286
+ const matched = [];
5287
+ for (const c of candidates) {
5288
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5289
+ usedSource.add(c.sourceIdx);
5290
+ usedTarget.add(c.targetIdx);
5291
+ matched.push(c);
5292
+ }
5293
+ const unmatchedSource = sourceTexts.map((_, i) => i).filter((i) => !usedSource.has(i));
5294
+ const unmatchedTarget = targetTexts.map((_, i) => i).filter((i) => !usedTarget.has(i));
5295
+ return { matched, unmatchedSource, unmatchedTarget };
5296
+ }
5297
+ function compareHeadings(sourceElements, targetElements, config) {
5298
+ const srcHeadings = filterHeadings(sourceElements);
5299
+ const tgtHeadings = filterHeadings(targetElements);
5300
+ const srcTexts = srcHeadings.map(getElementText2);
5301
+ const tgtTexts = tgtHeadings.map(getElementText2);
5302
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5303
+ srcTexts,
5304
+ tgtTexts,
5305
+ config.headingMatchThreshold
5306
+ );
5307
+ const headingMatched = [];
5308
+ const headingChanged = [];
5309
+ for (const m of matched) {
5310
+ const srcText = srcTexts[m.sourceIdx];
5311
+ const tgtText = tgtTexts[m.targetIdx];
5312
+ const srcLevel = getHeadingLevel(srcHeadings[m.sourceIdx]);
5313
+ const tgtLevel = getHeadingLevel(tgtHeadings[m.targetIdx]);
5314
+ if (normalizeText(srcText) === normalizeText(tgtText)) {
5315
+ headingMatched.push({
5316
+ source: srcText,
5317
+ target: tgtText,
5318
+ level: srcLevel
5319
+ });
5320
+ } else {
5321
+ headingChanged.push({
5322
+ source: srcText,
5323
+ target: tgtText,
5324
+ level: srcLevel ?? tgtLevel
5325
+ });
5326
+ }
5327
+ }
5328
+ return {
5329
+ matched: headingMatched,
5330
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5331
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i]),
5332
+ changed: headingChanged
5333
+ };
5334
+ }
5335
+ function compareMetrics(sourceElements, targetElements, config) {
5336
+ const srcMetrics = filterMetrics(sourceElements);
5337
+ const tgtMetrics = filterMetrics(targetElements);
5338
+ const srcParsed = srcMetrics.map(parseMetricText);
5339
+ const tgtParsed = tgtMetrics.map(parseMetricText);
5340
+ const srcLabels = srcParsed.map((p) => p.label);
5341
+ const tgtLabels = tgtParsed.map((p) => p.label);
5342
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5343
+ srcLabels,
5344
+ tgtLabels,
5345
+ config.labelMatchThreshold
5346
+ );
5347
+ const metricMatched = [];
5348
+ const metricChanged = [];
5349
+ for (const m of matched) {
5350
+ const src = srcParsed[m.sourceIdx];
5351
+ const tgt = tgtParsed[m.targetIdx];
5352
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5353
+ metricMatched.push({
5354
+ label: src.label,
5355
+ sourceValue: src.value,
5356
+ targetValue: tgt.value
5357
+ });
5358
+ } else {
5359
+ metricChanged.push({
5360
+ label: src.label,
5361
+ sourceValue: src.value,
5362
+ targetValue: tgt.value
5363
+ });
5364
+ }
5365
+ }
5366
+ return {
5367
+ matched: metricMatched,
5368
+ changed: metricChanged,
5369
+ sourceOnly: unmatchedSource.map((i) => srcParsed[i].label),
5370
+ targetOnly: unmatchedTarget.map((i) => tgtParsed[i].label)
5371
+ };
5372
+ }
5373
+ function compareStatuses(sourceElements, targetElements, config) {
5374
+ const srcStatuses = filterStatuses(sourceElements);
5375
+ const tgtStatuses = filterStatuses(targetElements);
5376
+ const srcParsed = srcStatuses.map(parseMetricText);
5377
+ const tgtParsed = tgtStatuses.map(parseMetricText);
5378
+ const srcLabels = srcParsed.map((p) => p.label);
5379
+ const tgtLabels = tgtParsed.map((p) => p.label);
5380
+ const { matched } = matchTexts(srcLabels, tgtLabels, config.labelMatchThreshold);
5381
+ const statusMatched = [];
5382
+ const statusChanged = [];
5383
+ for (const m of matched) {
5384
+ const src = srcParsed[m.sourceIdx];
5385
+ const tgt = tgtParsed[m.targetIdx];
5386
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5387
+ statusMatched.push({
5388
+ label: src.label,
5389
+ sourceStatus: src.value,
5390
+ targetStatus: tgt.value
5391
+ });
5392
+ } else {
5393
+ statusChanged.push({
5394
+ label: src.label,
5395
+ sourceStatus: src.value,
5396
+ targetStatus: tgt.value
5397
+ });
5398
+ }
5399
+ }
5400
+ return {
5401
+ matched: statusMatched,
5402
+ changed: statusChanged
5403
+ };
5404
+ }
5405
+ function compareLabels(sourceElements, targetElements, config) {
5406
+ const srcLabels = filterLabels(sourceElements);
5407
+ const tgtLabels = filterLabels(targetElements);
5408
+ const srcTexts = srcLabels.map(getElementText2);
5409
+ const tgtTexts = tgtLabels.map(getElementText2);
5410
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5411
+ srcTexts,
5412
+ tgtTexts,
5413
+ config.labelMatchThreshold
5414
+ );
5415
+ return {
5416
+ matched: matched.map((m) => srcTexts[m.sourceIdx]),
5417
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5418
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i])
5419
+ };
5420
+ }
5421
+ function compareTables(sourceElements, targetElements, config) {
5422
+ const srcData = extractStructuredData(sourceElements);
5423
+ const tgtData = extractStructuredData(targetElements);
5424
+ const srcTables = srcData.tables;
5425
+ const tgtTables = tgtData.tables;
5426
+ if (srcTables.length === 0 || tgtTables.length === 0) {
5427
+ return [];
5428
+ }
5429
+ const srcTableLabels = srcTables.map((t) => t.label || "");
5430
+ const tgtTableLabels = tgtTables.map((t) => t.label || "");
5431
+ const { matched } = matchTexts(srcTableLabels, tgtTableLabels, config.labelMatchThreshold);
5432
+ const tablePairs = [];
5433
+ if (matched.length > 0) {
5434
+ for (const m of matched) {
5435
+ tablePairs.push({ srcIdx: m.sourceIdx, tgtIdx: m.targetIdx });
5436
+ }
5437
+ } else if (srcTables.length === 1 && tgtTables.length === 1) {
5438
+ tablePairs.push({ srcIdx: 0, tgtIdx: 0 });
5439
+ }
5440
+ const comparisons = [];
5441
+ for (const pair of tablePairs) {
5442
+ const srcTable = srcTables[pair.srcIdx];
5443
+ const tgtTable = tgtTables[pair.tgtIdx];
5444
+ const srcHeaders = srcTable.columns.map((c) => c.header);
5445
+ const tgtHeaders = tgtTable.columns.map((c) => c.header);
5446
+ const srcHeaderSet = new Set(srcHeaders.map(normalizeText));
5447
+ const tgtHeaderSet = new Set(tgtHeaders.map(normalizeText));
5448
+ const sourceOnlyColumns = srcHeaders.filter((h) => !tgtHeaderSet.has(normalizeText(h)));
5449
+ const targetOnlyColumns = tgtHeaders.filter((h) => !srcHeaderSet.has(normalizeText(h)));
5450
+ const columnsMatch = sourceOnlyColumns.length === 0 && targetOnlyColumns.length === 0;
5451
+ const cellDifferences = [];
5452
+ const commonHeaders = srcHeaders.filter((h) => tgtHeaderSet.has(normalizeText(h)));
5453
+ const minRows = Math.min(srcTable.rows.length, tgtTable.rows.length);
5454
+ for (let row = 0; row < minRows; row++) {
5455
+ if (cellDifferences.length >= config.maxCellDifferences) break;
5456
+ for (const header of commonHeaders) {
5457
+ const srcColIdx = srcHeaders.indexOf(header);
5458
+ const tgtColIdx = tgtHeaders.findIndex((h) => normalizeText(h) === normalizeText(header));
5459
+ if (srcColIdx < 0 || tgtColIdx < 0) continue;
5460
+ const srcValue = srcTable.rows[row]?.[srcColIdx] ?? "";
5461
+ const tgtValue = tgtTable.rows[row]?.[tgtColIdx] ?? "";
5462
+ if (normalizeText(srcValue) !== normalizeText(tgtValue)) {
5463
+ cellDifferences.push({
5464
+ row,
5465
+ column: header,
5466
+ sourceValue: srcValue,
5467
+ targetValue: tgtValue
5468
+ });
5469
+ }
5470
+ }
5471
+ }
5472
+ comparisons.push({
5473
+ sourceLabel: srcTable.label,
5474
+ targetLabel: tgtTable.label,
5475
+ columnsMatch,
5476
+ sourceOnlyColumns,
5477
+ targetOnlyColumns,
5478
+ sourceRowCount: srcTable.rows.length,
5479
+ targetRowCount: tgtTable.rows.length,
5480
+ cellDifferences
5481
+ });
5482
+ }
5483
+ return comparisons;
5484
+ }
5485
+ function compareHeadingHierarchy(sourceElements, targetElements) {
5486
+ const srcHeadings = filterHeadings(sourceElements);
5487
+ const tgtHeadings = filterHeadings(targetElements);
5488
+ const srcByLevel = /* @__PURE__ */ new Map();
5489
+ const tgtByLevel = /* @__PURE__ */ new Map();
5490
+ for (const el of srcHeadings) {
5491
+ const level = getHeadingLevel(el) ?? 0;
5492
+ srcByLevel.set(level, (srcByLevel.get(level) ?? 0) + 1);
5493
+ }
5494
+ for (const el of tgtHeadings) {
5495
+ const level = getHeadingLevel(el) ?? 0;
5496
+ tgtByLevel.set(level, (tgtByLevel.get(level) ?? 0) + 1);
5497
+ }
5498
+ const allLevels = /* @__PURE__ */ new Set([...srcByLevel.keys(), ...tgtByLevel.keys()]);
5499
+ const result = [];
5500
+ for (const level of [...allLevels].sort()) {
5501
+ result.push({
5502
+ level,
5503
+ sourceCount: srcByLevel.get(level) ?? 0,
5504
+ targetCount: tgtByLevel.get(level) ?? 0
5505
+ });
5506
+ }
5507
+ return result;
5508
+ }
5509
+ function compareContent(sourceElements, targetElements, config = DEFAULT_CONTENT_COMPARISON_CONFIG) {
5510
+ const srcContent = sourceElements.filter(isContentElement2);
5511
+ const tgtContent = targetElements.filter(isContentElement2);
5512
+ const headings = compareHeadings(srcContent, tgtContent, config);
5513
+ const metrics = compareMetrics(srcContent, tgtContent, config);
5514
+ const statuses = compareStatuses(srcContent, tgtContent, config);
5515
+ const labels = compareLabels(srcContent, tgtContent, config);
5516
+ const tables = compareTables(sourceElements, targetElements, config);
5517
+ const headingHierarchy = compareHeadingHierarchy(srcContent, tgtContent);
5518
+ const contentParity = calculateContentParity(headings, metrics, statuses, labels, tables);
5519
+ return {
5520
+ headings,
5521
+ metrics,
5522
+ statuses,
5523
+ labels,
5524
+ tables,
5525
+ headingHierarchy,
5526
+ contentParity
5527
+ };
5528
+ }
5529
+ function calculateContentParity(headings, metrics, statuses, labels, tables) {
5530
+ const scores = [];
5531
+ const totalHeadings = headings.matched.length + headings.changed.length + headings.sourceOnly.length + headings.targetOnly.length;
5532
+ if (totalHeadings > 0) {
5533
+ scores.push(headings.matched.length / totalHeadings);
5534
+ }
5535
+ const totalMetrics = metrics.matched.length + metrics.changed.length + metrics.sourceOnly.length + metrics.targetOnly.length;
5536
+ if (totalMetrics > 0) {
5537
+ const metricScore = (metrics.matched.length + metrics.changed.length * 0.5) / totalMetrics;
5538
+ scores.push(metricScore);
5539
+ }
5540
+ const totalStatuses = statuses.matched.length + statuses.changed.length;
5541
+ if (totalStatuses > 0) {
5542
+ scores.push(statuses.matched.length / totalStatuses);
5543
+ }
5544
+ const totalLabels = labels.matched.length + labels.sourceOnly.length + labels.targetOnly.length;
5545
+ if (totalLabels > 0) {
5546
+ scores.push(labels.matched.length / totalLabels);
5547
+ }
5548
+ if (tables.length > 0) {
5549
+ let tableScore = 0;
5550
+ for (const table of tables) {
5551
+ let tScore = table.columnsMatch ? 0.5 : 0;
5552
+ if (table.sourceRowCount > 0) {
5553
+ const rowRatio = Math.min(
5554
+ table.targetRowCount / table.sourceRowCount,
5555
+ table.sourceRowCount / table.targetRowCount
5556
+ );
5557
+ tScore += rowRatio * 0.3;
5558
+ } else {
5559
+ tScore += 0.3;
5560
+ }
5561
+ const totalCells = Math.max(table.sourceRowCount, 1) * Math.max(
5562
+ table.sourceOnlyColumns.length + table.targetOnlyColumns.length + (table.columnsMatch ? 1 : 0),
5563
+ 1
5564
+ );
5565
+ const diffRatio = totalCells > 0 ? 1 - Math.min(table.cellDifferences.length / totalCells, 1) : 1;
5566
+ tScore += diffRatio * 0.2;
5567
+ tableScore += tScore;
5568
+ }
5569
+ scores.push(tableScore / tables.length);
5570
+ }
5571
+ if (scores.length === 0) return 1;
5572
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100;
5573
+ }
5574
+
5575
+ // src/ai/comparison-report.ts
5576
+ var DEFAULT_COMPARISON_REPORT_CONFIG = {
5577
+ includeComponents: false
5578
+ };
5579
+ function generateComparisonReport(source, target, options) {
5580
+ const startTime = Date.now();
5581
+ const config = { ...DEFAULT_COMPARISON_REPORT_CONFIG, ...options?.config };
5582
+ const srcElements = source.elements;
5583
+ const tgtElements = target.elements;
5584
+ const diff = computeCrossAppDiff(srcElements, tgtElements);
5585
+ const navigation = buildNavigationMap(srcElements, tgtElements);
5586
+ const sourceRegions = segmentPageRegions(srcElements);
5587
+ const targetRegions = segmentPageRegions(tgtElements);
5588
+ const layout = compareLayouts(srcElements, tgtElements, sourceRegions, targetRegions);
5589
+ const actionParityResults = analyzeActionParity(diff.matchedPairs, srcElements, tgtElements);
5590
+ const componentComparison = config.includeComponents && options?.sourceComponents && options?.targetComponents ? compareComponents(options.sourceComponents, options.targetComponents) : null;
5591
+ const contentComparison = compareContent(srcElements, tgtElements);
5592
+ const sourceData = extractPageData(srcElements);
5593
+ extractPageData(tgtElements);
5594
+ const sourceFieldCount = Object.keys(sourceData.values).length;
5595
+ const matchedDataCount = diff.dataComparisons.length;
5596
+ const dataCompleteness = sourceFieldCount > 0 ? Math.round(matchedDataCount / sourceFieldCount * 100) / 100 : 1;
5597
+ const formatMatchCount = diff.dataComparisons.filter((c) => c.formatsMatch).length;
5598
+ const formatAlignment = matchedDataCount > 0 ? Math.round(formatMatchCount / matchedDataCount * 100) / 100 : 1;
5599
+ const presentationAlignment = layout.similarity;
5600
+ const totalNavItems = navigation.pairs.length + navigation.sourceOnly.length;
5601
+ const navigationParity = totalNavItems > 0 ? Math.round(navigation.pairs.length / totalNavItems * 100) / 100 : 1;
5602
+ const totalActionChecks = actionParityResults.length;
5603
+ const fullParityCount = actionParityResults.filter((r) => r.missingInTarget.length === 0).length;
5604
+ const actionParity = totalActionChecks > 0 ? Math.round(fullParityCount / totalActionChecks * 100) / 100 : 1;
5605
+ const contentParity = contentComparison.contentParity;
5606
+ const overallScore = Math.round(
5607
+ (dataCompleteness * 0.2 + formatAlignment * 0.1 + presentationAlignment * 0.15 + navigationParity * 0.15 + actionParity * 0.15 + contentParity * 0.25) * 100
5608
+ ) / 100;
5609
+ const issues = [];
5610
+ for (const srcId of diff.unmatchedSourceIds) {
5611
+ const srcVal = Object.values(sourceData.values).find((v) => v.elementId === srcId);
5612
+ if (srcVal) {
5613
+ issues.push({
5614
+ severity: "warning",
5615
+ category: "missing-data",
5616
+ description: `Data field "${srcVal.label}" (${srcVal.dataType}) exists in source but has no match in target`,
5617
+ sourceElementId: srcId
5618
+ });
5619
+ }
5620
+ }
5621
+ for (const comp of diff.dataComparisons) {
5622
+ if (!comp.valuesMatch) {
5623
+ issues.push({
5624
+ severity: "error",
5625
+ category: "value-mismatch",
5626
+ description: `Value mismatch for "${comp.label}": source="${comp.sourceValue}", target="${comp.targetValue}"`
5627
+ });
5628
+ }
5629
+ }
5630
+ for (const fm of diff.formatMismatches) {
5631
+ issues.push({
5632
+ severity: fm.severity,
5633
+ category: "format-mismatch",
5634
+ description: fm.description
5635
+ });
5636
+ }
5637
+ for (const ap of actionParityResults) {
5638
+ for (const action of ap.missingInTarget) {
5639
+ issues.push({
5640
+ severity: "warning",
5641
+ category: "missing-action",
5642
+ description: `Action "${action}" available on source element "${ap.pair.sourceLabel}" is missing in target`,
5643
+ sourceElementId: ap.pair.sourceId,
5644
+ targetElementId: ap.pair.targetId
5645
+ });
5646
+ }
5647
+ }
5648
+ for (const srcId of navigation.sourceOnly) {
5649
+ issues.push({
5650
+ severity: "warning",
5651
+ category: "navigation-gap",
5652
+ description: `Navigation item "${srcId}" in source has no match in target`,
5653
+ sourceElementId: srcId
5654
+ });
5655
+ }
5656
+ if (layout.similarity < 0.5) {
5657
+ issues.push({
5658
+ severity: "warning",
5659
+ category: "layout-difference",
5660
+ description: `Layout similarity is low (${layout.similarity}). Grid: ${layout.gridDiff.sourceGrid.columnCount} cols vs ${layout.gridDiff.targetGrid.columnCount} cols`
5661
+ });
5662
+ }
5663
+ if (componentComparison) {
5664
+ for (const src of componentComparison.sourceOnly) {
5665
+ issues.push({
5666
+ severity: "info",
5667
+ category: "component-mismatch",
5668
+ description: `Component "${src.name}" (${src.type}) exists in source but not target`
5669
+ });
5670
+ }
5671
+ for (const match of componentComparison.matches) {
5672
+ if (match.stateKeyDiff.missing.length > 0) {
5673
+ issues.push({
5674
+ severity: "warning",
5675
+ category: "component-mismatch",
5676
+ description: `Component "${match.source.name}": state keys missing in target: ${match.stateKeyDiff.missing.join(", ")}`
5677
+ });
5678
+ }
5679
+ }
5680
+ }
5681
+ for (const heading of contentComparison.headings.sourceOnly) {
5682
+ issues.push({
5683
+ severity: "warning",
5684
+ category: "content-difference",
5685
+ description: `Heading "${heading}" exists in source but not in target`
5686
+ });
5687
+ }
5688
+ for (const heading of contentComparison.headings.targetOnly) {
5689
+ issues.push({
5690
+ severity: "info",
5691
+ category: "content-difference",
5692
+ description: `Heading "${heading}" exists in target but not in source`
5693
+ });
5694
+ }
5695
+ for (const change of contentComparison.headings.changed) {
5696
+ issues.push({
5697
+ severity: "warning",
5698
+ category: "content-difference",
5699
+ description: `Heading changed: "${change.source}" -> "${change.target}"`
5700
+ });
5701
+ }
5702
+ for (const change of contentComparison.metrics.changed) {
5703
+ issues.push({
5704
+ severity: "warning",
5705
+ category: "content-difference",
5706
+ description: `Metric "${change.label}" value differs: "${change.sourceValue}" vs "${change.targetValue}"`
5707
+ });
5708
+ }
5709
+ for (const label of contentComparison.metrics.sourceOnly) {
5710
+ issues.push({
5711
+ severity: "warning",
5712
+ category: "content-difference",
5713
+ description: `Metric "${label}" exists in source but not in target`
5714
+ });
5715
+ }
5716
+ for (const change of contentComparison.statuses.changed) {
5717
+ issues.push({
5718
+ severity: "warning",
5719
+ category: "content-difference",
5720
+ description: `Status "${change.label}" differs: "${change.sourceStatus}" vs "${change.targetStatus}"`
5721
+ });
5722
+ }
5723
+ for (const table of contentComparison.tables) {
5724
+ if (!table.columnsMatch) {
5725
+ issues.push({
5726
+ severity: "warning",
5727
+ category: "content-difference",
5728
+ description: `Table "${table.sourceLabel}" column mismatch: source-only=[${table.sourceOnlyColumns.join(", ")}], target-only=[${table.targetOnlyColumns.join(", ")}]`
5729
+ });
5730
+ }
5731
+ if (table.sourceRowCount !== table.targetRowCount) {
5732
+ issues.push({
5733
+ severity: "info",
5734
+ category: "content-difference",
5735
+ description: `Table "${table.sourceLabel}" row count differs: ${table.sourceRowCount} vs ${table.targetRowCount}`
5736
+ });
5737
+ }
5738
+ if (table.cellDifferences.length > 0) {
5739
+ issues.push({
5740
+ severity: "warning",
5741
+ category: "content-difference",
5742
+ description: `Table "${table.sourceLabel}" has ${table.cellDifferences.length} cell value difference(s)`
5743
+ });
5744
+ }
5745
+ }
5746
+ const severityOrder = { error: 0, warning: 1, info: 2 };
5747
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
5748
+ const errorCount = issues.filter((i) => i.severity === "error").length;
5749
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
5750
+ const infoCount = issues.filter((i) => i.severity === "info").length;
5751
+ const summaryLines = [
5752
+ `Cross-app comparison: ${source.page.url} vs ${target.page.url}`,
5753
+ `Overall score: ${(overallScore * 100).toFixed(0)}%`,
5754
+ `Matched elements: ${diff.matchedPairs.length}`,
5755
+ `Unmatched: ${diff.unmatchedSourceIds.length} source, ${diff.unmatchedTargetIds.length} target`,
5756
+ `Navigation: ${navigation.pairs.length} matched, ${navigation.sourceOnly.length} source-only, ${navigation.targetOnly.length} target-only`
5757
+ ];
5758
+ if (componentComparison) {
5759
+ summaryLines.push(
5760
+ `Components: ${componentComparison.matches.length} matched, ${componentComparison.sourceOnly.length} source-only, ${componentComparison.targetOnly.length} target-only`
5761
+ );
5762
+ }
5763
+ const hMatched = contentComparison.headings.matched.length;
5764
+ const hChanged = contentComparison.headings.changed.length;
5765
+ const hSrcOnly = contentComparison.headings.sourceOnly.length;
5766
+ const hTgtOnly = contentComparison.headings.targetOnly.length;
5767
+ const mMatched = contentComparison.metrics.matched.length;
5768
+ const mChanged = contentComparison.metrics.changed.length;
5769
+ const sMatched = contentComparison.statuses.matched.length;
5770
+ const sChanged = contentComparison.statuses.changed.length;
5771
+ const totalContent = hMatched + hChanged + hSrcOnly + hTgtOnly + mMatched + mChanged + sMatched + sChanged;
5772
+ if (totalContent > 0) {
5773
+ summaryLines.push(
5774
+ `Content: headings=${hMatched} matched/${hChanged} changed/${hSrcOnly + hTgtOnly} unmatched, metrics=${mMatched} matched/${mChanged} changed, statuses=${sMatched} matched/${sChanged} changed, parity=${(contentParity * 100).toFixed(0)}%`
5775
+ );
5776
+ }
5777
+ summaryLines.push(`Issues: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
5778
+ const summary = summaryLines.join("\n");
5779
+ const report = {
5780
+ sourceUrl: source.page.url,
5781
+ targetUrl: target.page.url,
5782
+ timestamp: Date.now(),
5783
+ durationMs: Date.now() - startTime,
5784
+ scores: {
5785
+ dataCompleteness,
5786
+ formatAlignment,
5787
+ presentationAlignment,
5788
+ navigationParity,
5789
+ actionParity,
5790
+ overallScore
5791
+ },
5792
+ diff,
5793
+ navigation,
5794
+ layout,
5795
+ contentComparison,
5796
+ issues,
5797
+ summary
5798
+ };
5799
+ if (componentComparison) {
5800
+ report.components = componentComparison;
5801
+ }
5802
+ return report;
5803
+ }
5804
+
5805
+ // src/server/handlers.ts
5806
+ function success(data) {
5807
+ return {
5808
+ success: true,
5809
+ data,
5810
+ timestamp: Date.now()
5811
+ };
5812
+ }
5813
+ function error(message, code) {
5814
+ return {
5815
+ success: false,
5816
+ error: message,
5817
+ code,
5818
+ timestamp: Date.now()
5819
+ };
5820
+ }
5821
+ function getRecoverySuggestions(errorCode) {
5822
+ switch (errorCode) {
5823
+ case "ELEMENT_NOT_FOUND":
5824
+ return [
5825
+ {
5826
+ suggestion: "Wait for the page to fully load",
5827
+ command: "wait for page to load",
5828
+ confidence: 0.7,
5829
+ retryable: true
5830
+ },
5831
+ {
5832
+ suggestion: "Use a different description for the element",
5833
+ confidence: 0.8,
5834
+ retryable: false
5835
+ },
5836
+ {
5837
+ suggestion: "Scroll the page to reveal the element",
5838
+ command: "scroll down",
5839
+ confidence: 0.6,
5840
+ retryable: true
5841
+ }
5842
+ ];
5843
+ case "ELEMENT_NOT_VISIBLE":
5844
+ return [
5845
+ {
5846
+ suggestion: "Scroll to make the element visible",
5847
+ command: "scroll to element",
5848
+ confidence: 0.9,
5849
+ retryable: true
5850
+ },
5851
+ {
5852
+ suggestion: "Wait for any loading overlays to disappear",
5853
+ confidence: 0.7,
5854
+ retryable: true
5855
+ },
5856
+ {
5857
+ suggestion: "Close any blocking modals or popups",
5858
+ command: "click close button",
5859
+ confidence: 0.8,
5860
+ retryable: true
5861
+ }
5862
+ ];
5863
+ case "ELEMENT_NOT_ENABLED":
5864
+ return [
5865
+ { suggestion: "Fill in required fields first", confidence: 0.8, retryable: false },
5866
+ {
5867
+ suggestion: "Complete prerequisite steps in the form",
5868
+ confidence: 0.7,
5869
+ retryable: false
5870
+ },
5871
+ {
5872
+ suggestion: "Wait for the element to become enabled",
5873
+ command: "wait for element to be enabled",
5874
+ confidence: 0.6,
5875
+ retryable: true
5876
+ }
5877
+ ];
5878
+ case "ELEMENT_NOT_INTERACTABLE":
5879
+ return [
5880
+ {
5881
+ suggestion: "Close any modal or popup blocking the element",
5882
+ command: "click close button",
5883
+ confidence: 0.9,
5884
+ retryable: true
5885
+ },
5886
+ { suggestion: "Wait for animations to complete", confidence: 0.7, retryable: true },
5887
+ {
5888
+ suggestion: "Scroll the element into the viewport",
5889
+ command: "scroll to element",
5890
+ confidence: 0.8,
5891
+ retryable: true
5892
+ }
5893
+ ];
5894
+ case "ACTION_TIMEOUT":
5895
+ return [
5896
+ { suggestion: "Increase the timeout duration", confidence: 0.8, retryable: true },
5897
+ { suggestion: "Check if the condition can ever be met", confidence: 0.7, retryable: false },
5898
+ {
5899
+ suggestion: "Verify the page is responding",
5900
+ command: "check page status",
5901
+ confidence: 0.6,
5902
+ retryable: true
5903
+ }
5904
+ ];
5905
+ case "LOW_CONFIDENCE":
5906
+ return [
5907
+ {
5908
+ suggestion: "Use the exact text shown on the element",
5909
+ confidence: 0.9,
5910
+ retryable: false
5911
+ },
5912
+ {
5913
+ suggestion: "Try a different description that more closely matches the element",
5914
+ confidence: 0.8,
5915
+ retryable: false
5916
+ },
5917
+ {
5918
+ suggestion: "Lower the confidence threshold if the match is correct",
5919
+ confidence: 0.7,
5920
+ retryable: true
5921
+ }
5922
+ ];
5923
+ case "AMBIGUOUS_MATCH":
5924
+ return [
5925
+ {
5926
+ suggestion: "Be more specific about which element you mean",
5927
+ confidence: 0.9,
5928
+ retryable: false
5929
+ },
5930
+ {
5931
+ suggestion: "Include the section or form name in the description",
5932
+ confidence: 0.8,
5933
+ retryable: false
5934
+ },
5935
+ { suggestion: "Use the element ID directly", confidence: 0.7, retryable: false }
5936
+ ];
5937
+ default:
5938
+ return [
5939
+ {
5940
+ suggestion: "Try a different approach or check the page state",
5941
+ confidence: 0.5,
5942
+ retryable: false
5943
+ }
5944
+ ];
5945
+ }
5946
+ }
5947
+ function createFailureDetails(errorCode, message, options = {}) {
5948
+ const retryableErrors = [
5949
+ "ELEMENT_NOT_VISIBLE",
3632
5950
  "ACTION_TIMEOUT",
3633
5951
  "LOW_CONFIDENCE",
3634
5952
  "NETWORK_ERROR",
@@ -3651,6 +5969,9 @@ function createHandlers(registry, actionExecutor, config = {}) {
3651
5969
  const assertionExecutor = new AssertionExecutor();
3652
5970
  const snapshotManager = new SemanticSnapshotManager();
3653
5971
  const diffManager = new SemanticDiffManager();
5972
+ const intentRegistry = /* @__PURE__ */ new Map();
5973
+ const consoleCapture = config.consoleCapture ?? null;
5974
+ const annotationStore = config.annotationStore ?? getGlobalAnnotationStore();
3654
5975
  function refreshElements() {
3655
5976
  const elements = registry.getAllElements();
3656
5977
  searchEngine.updateElements(elements);
@@ -3717,10 +6038,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
3717
6038
  try {
3718
6039
  const element = registry.getElement(id);
3719
6040
  if (!element) {
3720
- const failureDetails = createFailureDetails("ELEMENT_NOT_FOUND", `Element not found: ${id}`, {
3721
- elementId: id,
3722
- selectorsTried: [id]
3723
- });
6041
+ const failureDetails = createFailureDetails(
6042
+ "ELEMENT_NOT_FOUND",
6043
+ `Element not found: ${id}`,
6044
+ {
6045
+ elementId: id,
6046
+ selectorsTried: [id]
6047
+ }
6048
+ );
3724
6049
  return {
3725
6050
  success: false,
3726
6051
  error: `Element not found: ${id}`,
@@ -3750,11 +6075,15 @@ function createHandlers(registry, actionExecutor, config = {}) {
3750
6075
  try {
3751
6076
  const element = registry.getElement(id);
3752
6077
  if (!element) {
3753
- const failureDetails = createFailureDetails("ELEMENT_NOT_FOUND", `Element not found: ${id}`, {
3754
- elementId: id,
3755
- selectorsTried: [id],
3756
- durationMs: Date.now() - startTime
3757
- });
6078
+ const failureDetails = createFailureDetails(
6079
+ "ELEMENT_NOT_FOUND",
6080
+ `Element not found: ${id}`,
6081
+ {
6082
+ elementId: id,
6083
+ selectorsTried: [id],
6084
+ durationMs: Date.now() - startTime
6085
+ }
6086
+ );
3758
6087
  return {
3759
6088
  success: false,
3760
6089
  error: `Element not found: ${id}`,
@@ -3789,10 +6118,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
3789
6118
  } else if (errorMsg.includes("blocked") || errorMsg.includes("interactable")) {
3790
6119
  errorCode = "ELEMENT_NOT_INTERACTABLE";
3791
6120
  }
3792
- const failureDetails = createFailureDetails(errorCode, actionResult.error || "Action failed", {
3793
- elementId: id,
3794
- durationMs: Date.now() - startTime
3795
- });
6121
+ const failureDetails = createFailureDetails(
6122
+ errorCode,
6123
+ actionResult.error || "Action failed",
6124
+ {
6125
+ elementId: id,
6126
+ durationMs: Date.now() - startTime
6127
+ }
6128
+ );
3796
6129
  return success({
3797
6130
  ...actionResult,
3798
6131
  failureDetails
@@ -3891,7 +6224,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
3891
6224
  try {
3892
6225
  const findRequest = request;
3893
6226
  const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
3894
- return success({ elements, timestamp: Date.now(), total: elements.length, durationMs: 0 });
6227
+ return success({
6228
+ elements,
6229
+ timestamp: Date.now(),
6230
+ total: elements.length,
6231
+ durationMs: 0
6232
+ });
3895
6233
  } catch (err) {
3896
6234
  return error(err.message, "FIND_ERROR");
3897
6235
  }
@@ -3900,7 +6238,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
3900
6238
  try {
3901
6239
  const findRequest = request;
3902
6240
  const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
3903
- return success({ elements, timestamp: Date.now(), total: elements.length, durationMs: 0 });
6241
+ return success({
6242
+ elements,
6243
+ timestamp: Date.now(),
6244
+ total: elements.length,
6245
+ durationMs: 0
6246
+ });
3904
6247
  } catch (err) {
3905
6248
  return error(err.message, "DISCOVER_ERROR");
3906
6249
  }
@@ -3997,6 +6340,28 @@ function createHandlers(registry, actionExecutor, config = {}) {
3997
6340
  return error(err.message, "ELEMENT_TREE_ERROR");
3998
6341
  }
3999
6342
  },
6343
+ getConsoleErrors: async (params) => {
6344
+ try {
6345
+ if (!consoleCapture) {
6346
+ return success({ errors: [], count: 0 });
6347
+ }
6348
+ const errors = params?.since ? consoleCapture.getConsoleSince(params.since) : consoleCapture.getConsoleRecent(params?.limit ?? 50);
6349
+ return success({ errors, count: errors.length });
6350
+ } catch (err) {
6351
+ return error(err.message, "CONSOLE_ERRORS_ERROR");
6352
+ }
6353
+ },
6354
+ clearConsoleErrors: async () => {
6355
+ try {
6356
+ if (!consoleCapture) {
6357
+ return success({ cleared: false });
6358
+ }
6359
+ consoleCapture.clear();
6360
+ return success({ cleared: true });
6361
+ } catch (err) {
6362
+ return error(err.message, "CONSOLE_CLEAR_ERROR");
6363
+ }
6364
+ },
4000
6365
  // =========================================================================
4001
6366
  // AI-Native Handlers
4002
6367
  // =========================================================================
@@ -4015,106 +6380,219 @@ function createHandlers(registry, actionExecutor, config = {}) {
4015
6380
  const response = await nlExecutor.execute(request);
4016
6381
  return success(response);
4017
6382
  } catch (err) {
4018
- return error(err.message, "AI_EXECUTE_ERROR");
6383
+ return error(err.message, "AI_EXECUTE_ERROR");
6384
+ }
6385
+ },
6386
+ aiAssert: async (request) => {
6387
+ try {
6388
+ refreshElements();
6389
+ const result = await assertionExecutor.assert(request);
6390
+ return success(result);
6391
+ } catch (err) {
6392
+ return error(err.message, "AI_ASSERT_ERROR");
6393
+ }
6394
+ },
6395
+ aiAssertBatch: async (request) => {
6396
+ try {
6397
+ refreshElements();
6398
+ const result = await assertionExecutor.assertBatch(request);
6399
+ return success(result);
6400
+ } catch (err) {
6401
+ return error(err.message, "AI_ASSERT_BATCH_ERROR");
6402
+ }
6403
+ },
6404
+ getSemanticSnapshot: async () => {
6405
+ try {
6406
+ const controlSnapshot = registry.createSnapshot();
6407
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
6408
+ return success(snapshot);
6409
+ } catch (err) {
6410
+ return error(err.message, "SEMANTIC_SNAPSHOT_ERROR");
6411
+ }
6412
+ },
6413
+ getSemanticDiff: async (_since) => {
6414
+ try {
6415
+ const controlSnapshot = registry.createSnapshot();
6416
+ const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
6417
+ const diff = diffManager.update(currentSnapshot);
6418
+ return success(diff);
6419
+ } catch (err) {
6420
+ return error(err.message, "SEMANTIC_DIFF_ERROR");
6421
+ }
6422
+ },
6423
+ getPageSummary: async () => {
6424
+ try {
6425
+ const snapshot = registry.createSnapshot();
6426
+ const elements = snapshot.elements.map((el) => ({
6427
+ ...el,
6428
+ description: el.label || el.id,
6429
+ aliases: [],
6430
+ suggestedActions: [],
6431
+ tagName: el.type,
6432
+ accessibleName: el.label,
6433
+ registered: true
6434
+ }));
6435
+ const summary = generatePageSummary(elements);
6436
+ return success(summary);
6437
+ } catch (err) {
6438
+ return error(err.message, "PAGE_SUMMARY_ERROR");
6439
+ }
6440
+ },
6441
+ // =========================================================================
6442
+ // Semantic Search Handler (Embedding-based)
6443
+ // =========================================================================
6444
+ // =========================================================================
6445
+ // Page Navigation Handlers
6446
+ // =========================================================================
6447
+ pageRefresh: async () => {
6448
+ try {
6449
+ window.location.reload();
6450
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6451
+ } catch (err) {
6452
+ return error(err.message, "PAGE_REFRESH_ERROR");
6453
+ }
6454
+ },
6455
+ pageNavigate: async (request) => {
6456
+ try {
6457
+ if (!request.url) {
6458
+ return error("URL is required", "INVALID_REQUEST");
6459
+ }
6460
+ window.location.href = request.url;
6461
+ return success({ success: true, url: request.url, timestamp: Date.now() });
6462
+ } catch (err) {
6463
+ return error(err.message, "PAGE_NAVIGATE_ERROR");
6464
+ }
6465
+ },
6466
+ pageGoBack: async () => {
6467
+ try {
6468
+ window.history.back();
6469
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6470
+ } catch (err) {
6471
+ return error(err.message, "PAGE_GO_BACK_ERROR");
6472
+ }
6473
+ },
6474
+ pageGoForward: async () => {
6475
+ try {
6476
+ window.history.forward();
6477
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6478
+ } catch (err) {
6479
+ return error(err.message, "PAGE_GO_FORWARD_ERROR");
6480
+ }
6481
+ },
6482
+ // =========================================================================
6483
+ // Annotation Handlers
6484
+ //
6485
+ // REST API endpoints for managing element annotations:
6486
+ // GET /annotations - List all annotations
6487
+ // GET /annotations/export - Export all annotations as AnnotationConfig
6488
+ // GET /annotations/coverage - Get annotation coverage statistics
6489
+ // GET /annotations/:id - Get annotation for a specific element
6490
+ // PUT /annotations/:id - Create or update an annotation
6491
+ // DELETE /annotations/:id - Delete an annotation
6492
+ // POST /annotations/import - Import annotations from AnnotationConfig
6493
+ // =========================================================================
6494
+ getAnnotations: async () => {
6495
+ try {
6496
+ return success(annotationStore.getAll());
6497
+ } catch (err) {
6498
+ return error(err.message, "ANNOTATIONS_ERROR");
6499
+ }
6500
+ },
6501
+ getAnnotation: async (id) => {
6502
+ try {
6503
+ const annotation = annotationStore.get(id);
6504
+ if (!annotation) {
6505
+ return error(`Annotation not found: ${id}`, "NOT_FOUND");
6506
+ }
6507
+ return success(annotation);
6508
+ } catch (err) {
6509
+ return error(err.message, "ANNOTATION_ERROR");
4019
6510
  }
4020
6511
  },
4021
- aiAssert: async (request) => {
6512
+ setAnnotation: async (id, annotation) => {
4022
6513
  try {
4023
- refreshElements();
4024
- const result = await assertionExecutor.assert(request);
4025
- return success(result);
6514
+ annotationStore.set(id, annotation);
6515
+ return success(annotationStore.get(id));
4026
6516
  } catch (err) {
4027
- return error(err.message, "AI_ASSERT_ERROR");
6517
+ return error(err.message, "ANNOTATION_SET_ERROR");
4028
6518
  }
4029
6519
  },
4030
- aiAssertBatch: async (request) => {
6520
+ deleteAnnotation: async (id) => {
4031
6521
  try {
4032
- refreshElements();
4033
- const result = await assertionExecutor.assertBatch(request);
4034
- return success(result);
6522
+ const existed = annotationStore.delete(id);
6523
+ if (!existed) {
6524
+ return error(`Annotation not found: ${id}`, "NOT_FOUND");
6525
+ }
6526
+ return success(void 0);
4035
6527
  } catch (err) {
4036
- return error(err.message, "AI_ASSERT_BATCH_ERROR");
6528
+ return error(err.message, "ANNOTATION_DELETE_ERROR");
4037
6529
  }
4038
6530
  },
4039
- getSemanticSnapshot: async () => {
6531
+ importAnnotations: async (config2) => {
4040
6532
  try {
4041
- const controlSnapshot = registry.createSnapshot();
4042
- const snapshot = snapshotManager.createSnapshot(controlSnapshot);
4043
- return success(snapshot);
6533
+ const count = annotationStore.importConfig(config2);
6534
+ return success({ count });
4044
6535
  } catch (err) {
4045
- return error(err.message, "SEMANTIC_SNAPSHOT_ERROR");
6536
+ return error(err.message, "ANNOTATION_IMPORT_ERROR");
4046
6537
  }
4047
6538
  },
4048
- getSemanticDiff: async (_since) => {
6539
+ exportAnnotations: async () => {
4049
6540
  try {
4050
- const controlSnapshot = registry.createSnapshot();
4051
- const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
4052
- const diff = diffManager.update(currentSnapshot);
4053
- return success(diff);
6541
+ return success(annotationStore.exportConfig());
4054
6542
  } catch (err) {
4055
- return error(err.message, "SEMANTIC_DIFF_ERROR");
6543
+ return error(err.message, "ANNOTATION_EXPORT_ERROR");
4056
6544
  }
4057
6545
  },
4058
- getPageSummary: async () => {
6546
+ getAnnotationCoverage: async () => {
4059
6547
  try {
4060
- const snapshot = registry.createSnapshot();
4061
- const elements = snapshot.elements.map((el) => ({
4062
- ...el,
4063
- description: el.label || el.id,
4064
- aliases: [],
4065
- suggestedActions: [],
4066
- tagName: el.type,
4067
- accessibleName: el.label,
4068
- registered: true
4069
- }));
4070
- const summary = generatePageSummary(elements);
4071
- return success(summary);
6548
+ const allElements = registry.getAllElements();
6549
+ const allIds = allElements.map((el) => el.id);
6550
+ return success(annotationStore.getCoverage(allIds));
4072
6551
  } catch (err) {
4073
- return error(err.message, "PAGE_SUMMARY_ERROR");
6552
+ return error(err.message, "ANNOTATION_COVERAGE_ERROR");
4074
6553
  }
4075
6554
  },
4076
- // =========================================================================
4077
- // Semantic Search Handler (Embedding-based)
4078
- // =========================================================================
4079
6555
  aiSemanticSearch: async (criteria) => {
4080
6556
  const startTime = performance.now();
4081
6557
  try {
4082
6558
  refreshElements();
4083
6559
  const allElements = registry.getAllElements();
4084
- const aiElements = allElements.map((el) => {
4085
- const textParts = [];
4086
- const state = "getState" in el ? el.getState() : el.state;
4087
- const textContent = state?.textContent || "";
4088
- const label = el.label || "";
4089
- const accessibleName = el.accessibleName || "";
4090
- const placeholder = el.placeholder || "";
4091
- const title = el.title || "";
4092
- if (label) textParts.push(label);
4093
- if (accessibleName && accessibleName !== label) textParts.push(accessibleName);
4094
- if (textContent && textContent !== label && textContent !== accessibleName) {
4095
- textParts.push(textContent);
6560
+ const aiElements = allElements.map(
6561
+ (el) => {
6562
+ const textParts = [];
6563
+ const state = "getState" in el ? el.getState() : el.state;
6564
+ const textContent = state?.textContent || "";
6565
+ const label = el.label || "";
6566
+ const accessibleName = el.accessibleName || "";
6567
+ const placeholder = el.placeholder || "";
6568
+ const title = el.title || "";
6569
+ if (label) textParts.push(label);
6570
+ if (accessibleName && accessibleName !== label) textParts.push(accessibleName);
6571
+ if (textContent && textContent !== label && textContent !== accessibleName) {
6572
+ textParts.push(textContent);
6573
+ }
6574
+ if (placeholder) textParts.push(`placeholder: ${placeholder}`);
6575
+ if (title) textParts.push(title);
6576
+ const combinedText = textParts.join(" ").trim() || el.id;
6577
+ return {
6578
+ element: {
6579
+ id: el.id,
6580
+ type: el.type,
6581
+ label: el.label,
6582
+ tagName: el.tagName || el.type,
6583
+ role: el.role,
6584
+ accessibleName: el.accessibleName,
6585
+ actions: el.actions || [],
6586
+ state: state || {},
6587
+ registered: true,
6588
+ description: label || el.id,
6589
+ aliases: [],
6590
+ suggestedActions: []
6591
+ },
6592
+ text: combinedText
6593
+ };
4096
6594
  }
4097
- if (placeholder) textParts.push(`placeholder: ${placeholder}`);
4098
- if (title) textParts.push(title);
4099
- const combinedText = textParts.join(" ").trim() || el.id;
4100
- return {
4101
- element: {
4102
- id: el.id,
4103
- type: el.type,
4104
- label: el.label,
4105
- tagName: el.tagName || el.type,
4106
- role: el.role,
4107
- accessibleName: el.accessibleName,
4108
- actions: el.actions || [],
4109
- state: state || {},
4110
- registered: true,
4111
- description: label || el.id,
4112
- aliases: [],
4113
- suggestedActions: []
4114
- },
4115
- text: combinedText
4116
- };
4117
- });
6595
+ );
4118
6596
  let filteredElements = aiElements;
4119
6597
  if (criteria.type) {
4120
6598
  filteredElements = filteredElements.filter(
@@ -4179,6 +6657,398 @@ function createHandlers(registry, actionExecutor, config = {}) {
4179
6657
  } catch (err) {
4180
6658
  return error(err.message, "AI_SEMANTIC_SEARCH_ERROR");
4181
6659
  }
6660
+ },
6661
+ // =========================================================================
6662
+ // State Management Handlers
6663
+ // =========================================================================
6664
+ getStates: async () => {
6665
+ try {
6666
+ const states = registry.getStates?.() ?? [];
6667
+ return success(states);
6668
+ } catch (err) {
6669
+ return error(err.message, "STATES_ERROR");
6670
+ }
6671
+ },
6672
+ getState: async (id) => {
6673
+ try {
6674
+ const state = registry.getState?.(id);
6675
+ if (!state) {
6676
+ return error(`State not found: ${id}`, "NOT_FOUND");
6677
+ }
6678
+ return success(state);
6679
+ } catch (err) {
6680
+ return error(err.message, "STATE_ERROR");
6681
+ }
6682
+ },
6683
+ getActiveStates: async () => {
6684
+ try {
6685
+ const states = registry.getActiveStates?.() ?? [];
6686
+ return success(states);
6687
+ } catch (err) {
6688
+ return error(err.message, "ACTIVE_STATES_ERROR");
6689
+ }
6690
+ },
6691
+ activateState: async (id) => {
6692
+ try {
6693
+ if (!registry.activateState) {
6694
+ return error("State management not available", "NOT_IMPLEMENTED");
6695
+ }
6696
+ registry.activateState(id);
6697
+ return success(void 0);
6698
+ } catch (err) {
6699
+ return error(err.message, "ACTIVATE_STATE_ERROR");
6700
+ }
6701
+ },
6702
+ deactivateState: async (id) => {
6703
+ try {
6704
+ if (!registry.deactivateState) {
6705
+ return error("State management not available", "NOT_IMPLEMENTED");
6706
+ }
6707
+ registry.deactivateState(id);
6708
+ return success(void 0);
6709
+ } catch (err) {
6710
+ return error(err.message, "DEACTIVATE_STATE_ERROR");
6711
+ }
6712
+ },
6713
+ getStateGroups: async () => {
6714
+ try {
6715
+ const groups = registry.getStateGroups?.() ?? [];
6716
+ return success(groups);
6717
+ } catch (err) {
6718
+ return error(err.message, "STATE_GROUPS_ERROR");
6719
+ }
6720
+ },
6721
+ activateStateGroup: async (id) => {
6722
+ try {
6723
+ if (!registry.activateStateGroup) {
6724
+ return error("State group management not available", "NOT_IMPLEMENTED");
6725
+ }
6726
+ registry.activateStateGroup(id);
6727
+ return success(void 0);
6728
+ } catch (err) {
6729
+ return error(err.message, "ACTIVATE_STATE_GROUP_ERROR");
6730
+ }
6731
+ },
6732
+ deactivateStateGroup: async (id) => {
6733
+ try {
6734
+ if (!registry.deactivateStateGroup) {
6735
+ return error("State group management not available", "NOT_IMPLEMENTED");
6736
+ }
6737
+ registry.deactivateStateGroup(id);
6738
+ return success(void 0);
6739
+ } catch (err) {
6740
+ return error(err.message, "DEACTIVATE_STATE_GROUP_ERROR");
6741
+ }
6742
+ },
6743
+ getTransitions: async () => {
6744
+ try {
6745
+ const transitions = registry.getTransitions?.() ?? [];
6746
+ return success(transitions);
6747
+ } catch (err) {
6748
+ return error(err.message, "TRANSITIONS_ERROR");
6749
+ }
6750
+ },
6751
+ canExecuteTransition: async (id) => {
6752
+ try {
6753
+ if (!registry.canExecuteTransition) {
6754
+ return error("Transition management not available", "NOT_IMPLEMENTED");
6755
+ }
6756
+ const result = registry.canExecuteTransition(id);
6757
+ return success(result);
6758
+ } catch (err) {
6759
+ return error(err.message, "CAN_EXECUTE_TRANSITION_ERROR");
6760
+ }
6761
+ },
6762
+ executeTransition: async (id) => {
6763
+ try {
6764
+ if (!registry.executeTransition) {
6765
+ return error("Transition execution not available", "NOT_IMPLEMENTED");
6766
+ }
6767
+ const result = await registry.executeTransition(id);
6768
+ return success(result);
6769
+ } catch (err) {
6770
+ return error(err.message, "EXECUTE_TRANSITION_ERROR");
6771
+ }
6772
+ },
6773
+ findPath: async (request) => {
6774
+ try {
6775
+ if (!registry.findPath) {
6776
+ return error("Pathfinding not available", "NOT_IMPLEMENTED");
6777
+ }
6778
+ const result = registry.findPath(request.targetStates);
6779
+ return success(result);
6780
+ } catch (err) {
6781
+ return error(err.message, "FIND_PATH_ERROR");
6782
+ }
6783
+ },
6784
+ navigateTo: async (request) => {
6785
+ try {
6786
+ if (!registry.navigateTo) {
6787
+ return error("Navigation not available", "NOT_IMPLEMENTED");
6788
+ }
6789
+ const result = await registry.navigateTo(request.targetStates);
6790
+ return success(result);
6791
+ } catch (err) {
6792
+ return error(err.message, "NAVIGATE_TO_ERROR");
6793
+ }
6794
+ },
6795
+ getStateSnapshot: async () => {
6796
+ try {
6797
+ if (!registry.getStateSnapshot) {
6798
+ const snapshot = {
6799
+ timestamp: Date.now(),
6800
+ activeStates: (registry.getActiveStates?.() ?? []).map((s) => s.id),
6801
+ states: registry.getStates?.() ?? [],
6802
+ groups: registry.getStateGroups?.() ?? [],
6803
+ transitions: registry.getTransitions?.() ?? []
6804
+ };
6805
+ return success(snapshot);
6806
+ }
6807
+ return success(registry.getStateSnapshot());
6808
+ } catch (err) {
6809
+ return error(err.message, "STATE_SNAPSHOT_ERROR");
6810
+ }
6811
+ },
6812
+ // =========================================================================
6813
+ // Intent Handlers
6814
+ // =========================================================================
6815
+ executeIntent: async (request) => {
6816
+ const startTime = Date.now();
6817
+ try {
6818
+ refreshElements();
6819
+ const intent = intentRegistry.get(request.intentId);
6820
+ if (!intent) {
6821
+ return error(`Intent not found: ${request.intentId}`, "NOT_FOUND");
6822
+ }
6823
+ const nlResponse = await nlExecutor.execute({
6824
+ instruction: intent.description,
6825
+ context: `Executing intent: ${intent.name}`
6826
+ });
6827
+ return success({
6828
+ success: nlResponse.success,
6829
+ intentId: request.intentId,
6830
+ result: nlResponse,
6831
+ error: nlResponse.error,
6832
+ durationMs: Date.now() - startTime
6833
+ });
6834
+ } catch (err) {
6835
+ return error(err.message, "EXECUTE_INTENT_ERROR");
6836
+ }
6837
+ },
6838
+ findIntent: async (request) => {
6839
+ try {
6840
+ const query = request.query.toLowerCase();
6841
+ const results = [];
6842
+ for (const intent of intentRegistry.values()) {
6843
+ let confidence = 0;
6844
+ const nameLower = intent.name.toLowerCase();
6845
+ const descLower = intent.description.toLowerCase();
6846
+ if (nameLower === query) {
6847
+ confidence = 1;
6848
+ } else if (nameLower.includes(query) || query.includes(nameLower)) {
6849
+ confidence = 0.8;
6850
+ } else if (descLower.includes(query)) {
6851
+ confidence = 0.6;
6852
+ } else if (intent.tags?.some((t) => t.toLowerCase().includes(query))) {
6853
+ confidence = 0.5;
6854
+ }
6855
+ if (confidence > 0) {
6856
+ results.push({ intent, confidence });
6857
+ }
6858
+ }
6859
+ results.sort((a, b) => b.confidence - a.confidence);
6860
+ return success({ intents: results });
6861
+ } catch (err) {
6862
+ return error(err.message, "FIND_INTENT_ERROR");
6863
+ }
6864
+ },
6865
+ listIntents: async () => {
6866
+ try {
6867
+ return success(Array.from(intentRegistry.values()));
6868
+ } catch (err) {
6869
+ return error(err.message, "LIST_INTENTS_ERROR");
6870
+ }
6871
+ },
6872
+ registerIntent: async (intent) => {
6873
+ try {
6874
+ intentRegistry.set(intent.id, intent);
6875
+ return success(intent);
6876
+ } catch (err) {
6877
+ return error(err.message, "REGISTER_INTENT_ERROR");
6878
+ }
6879
+ },
6880
+ executeIntentFromQuery: async (request) => {
6881
+ const startTime = Date.now();
6882
+ try {
6883
+ refreshElements();
6884
+ const query = request.query.toLowerCase();
6885
+ let bestIntent = null;
6886
+ let bestConfidence = 0;
6887
+ for (const intent of intentRegistry.values()) {
6888
+ let confidence = 0;
6889
+ const nameLower = intent.name.toLowerCase();
6890
+ const descLower = intent.description.toLowerCase();
6891
+ if (nameLower === query) {
6892
+ confidence = 1;
6893
+ } else if (nameLower.includes(query) || query.includes(nameLower)) {
6894
+ confidence = 0.8;
6895
+ } else if (descLower.includes(query)) {
6896
+ confidence = 0.6;
6897
+ }
6898
+ if (confidence > bestConfidence) {
6899
+ bestConfidence = confidence;
6900
+ bestIntent = intent;
6901
+ }
6902
+ }
6903
+ if (!bestIntent) {
6904
+ return success({
6905
+ success: false,
6906
+ intentId: "",
6907
+ error: `No intent found matching query: ${request.query}`,
6908
+ durationMs: Date.now() - startTime
6909
+ });
6910
+ }
6911
+ const nlResponse = await nlExecutor.execute({
6912
+ instruction: bestIntent.description,
6913
+ context: `Executing intent from query: ${request.query}`
6914
+ });
6915
+ return success({
6916
+ success: nlResponse.success,
6917
+ intentId: bestIntent.id,
6918
+ result: nlResponse,
6919
+ error: nlResponse.error,
6920
+ durationMs: Date.now() - startTime
6921
+ });
6922
+ } catch (err) {
6923
+ return error(err.message, "EXECUTE_INTENT_FROM_QUERY_ERROR");
6924
+ }
6925
+ },
6926
+ // =========================================================================
6927
+ // Recovery Handler
6928
+ // =========================================================================
6929
+ attemptRecovery: async (request) => {
6930
+ const startTime = Date.now();
6931
+ try {
6932
+ refreshElements();
6933
+ const strategiesAttempted = [];
6934
+ let lastResult;
6935
+ const suggestions = request.failure.suggestedActions ?? [];
6936
+ for (let i = 0; i < Math.min(suggestions.length, request.maxRetries); i++) {
6937
+ const suggestion = suggestions[i];
6938
+ strategiesAttempted.push(suggestion.suggestion || `strategy-${i}`);
6939
+ const instruction = suggestion.command || request.instruction;
6940
+ try {
6941
+ const result = await nlExecutor.execute({
6942
+ instruction,
6943
+ context: `Recovery attempt ${i + 1}: ${suggestion.suggestion}`
6944
+ });
6945
+ lastResult = result;
6946
+ if (result.success) {
6947
+ return success({
6948
+ recovered: true,
6949
+ strategiesAttempted,
6950
+ finalResult: result,
6951
+ durationMs: Date.now() - startTime
6952
+ });
6953
+ }
6954
+ } catch {
6955
+ }
6956
+ }
6957
+ if (strategiesAttempted.length === 0 || !lastResult?.success) {
6958
+ strategiesAttempted.push("direct-instruction");
6959
+ try {
6960
+ const result = await nlExecutor.execute({
6961
+ instruction: request.instruction,
6962
+ context: "Recovery: direct instruction attempt"
6963
+ });
6964
+ lastResult = result;
6965
+ if (result.success) {
6966
+ return success({
6967
+ recovered: true,
6968
+ strategiesAttempted,
6969
+ finalResult: result,
6970
+ durationMs: Date.now() - startTime
6971
+ });
6972
+ }
6973
+ } catch {
6974
+ }
6975
+ }
6976
+ return success({
6977
+ recovered: false,
6978
+ strategiesAttempted,
6979
+ finalResult: lastResult,
6980
+ error: "All recovery strategies exhausted",
6981
+ durationMs: Date.now() - startTime
6982
+ });
6983
+ } catch (err) {
6984
+ return error(err.message, "RECOVERY_ERROR");
6985
+ }
6986
+ },
6987
+ // =========================================================================
6988
+ // Cross-App Analysis Handlers
6989
+ // =========================================================================
6990
+ analyzePageData: async () => {
6991
+ try {
6992
+ const controlSnapshot = registry.createSnapshot();
6993
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
6994
+ const result = extractPageData(snapshot.elements);
6995
+ return success(result);
6996
+ } catch (err) {
6997
+ return error(err.message, "ANALYZE_DATA_ERROR");
6998
+ }
6999
+ },
7000
+ analyzePageRegions: async () => {
7001
+ try {
7002
+ const controlSnapshot = registry.createSnapshot();
7003
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
7004
+ const result = segmentPageRegions(snapshot.elements);
7005
+ return success(result);
7006
+ } catch (err) {
7007
+ return error(err.message, "ANALYZE_REGIONS_ERROR");
7008
+ }
7009
+ },
7010
+ analyzeStructuredData: async () => {
7011
+ try {
7012
+ const controlSnapshot = registry.createSnapshot();
7013
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
7014
+ const result = extractStructuredData(snapshot.elements);
7015
+ return success(result);
7016
+ } catch (err) {
7017
+ return error(err.message, "ANALYZE_STRUCTURED_DATA_ERROR");
7018
+ }
7019
+ },
7020
+ crossAppCompare: async (request) => {
7021
+ try {
7022
+ const hasComponents = request.sourceComponents && request.targetComponents;
7023
+ const report = generateComparisonReport(
7024
+ request.sourceSnapshot,
7025
+ request.targetSnapshot,
7026
+ hasComponents ? {
7027
+ config: { includeComponents: true },
7028
+ sourceComponents: request.sourceComponents,
7029
+ targetComponents: request.targetComponents
7030
+ } : void 0
7031
+ );
7032
+ return success(report);
7033
+ } catch (err) {
7034
+ return error(err.message, "CROSS_APP_COMPARE_ERROR");
7035
+ }
7036
+ },
7037
+ // =========================================================================
7038
+ // Performance Diagnostics Handlers
7039
+ // =========================================================================
7040
+ getPerformanceEntries: async () => {
7041
+ return {
7042
+ success: true,
7043
+ data: { navigation: null, resources: [], paint: [] },
7044
+ timestamp: Date.now()
7045
+ };
7046
+ },
7047
+ clearPerformanceEntries: async () => {
7048
+ return { success: true, data: { cleared: true }, timestamp: Date.now() };
7049
+ },
7050
+ getBrowserEvents: async (_params) => {
7051
+ return { success: true, data: { events: [], count: 0 }, timestamp: Date.now() };
4182
7052
  }
4183
7053
  };
4184
7054
  }
@@ -4202,7 +7072,12 @@ function createAIHandlers(registry, actionExecutor) {
4202
7072
  const response = searchEngine.search(criteria);
4203
7073
  return { success: true, data: response, timestamp: Date.now() };
4204
7074
  } catch (err) {
4205
- return { success: false, error: err.message, code: "AI_SEARCH_ERROR", timestamp: Date.now() };
7075
+ return {
7076
+ success: false,
7077
+ error: err.message,
7078
+ code: "AI_SEARCH_ERROR",
7079
+ timestamp: Date.now()
7080
+ };
4206
7081
  }
4207
7082
  },
4208
7083
  aiExecute: async (request) => {
@@ -4211,7 +7086,12 @@ function createAIHandlers(registry, actionExecutor) {
4211
7086
  const response = await nlExecutor.execute(request);
4212
7087
  return { success: true, data: response, timestamp: Date.now() };
4213
7088
  } catch (err) {
4214
- return { success: false, error: err.message, code: "AI_EXECUTE_ERROR", timestamp: Date.now() };
7089
+ return {
7090
+ success: false,
7091
+ error: err.message,
7092
+ code: "AI_EXECUTE_ERROR",
7093
+ timestamp: Date.now()
7094
+ };
4215
7095
  }
4216
7096
  },
4217
7097
  aiAssert: async (request) => {
@@ -4220,7 +7100,12 @@ function createAIHandlers(registry, actionExecutor) {
4220
7100
  const result = await assertionExecutor.assert(request);
4221
7101
  return { success: true, data: result, timestamp: Date.now() };
4222
7102
  } catch (err) {
4223
- return { success: false, error: err.message, code: "AI_ASSERT_ERROR", timestamp: Date.now() };
7103
+ return {
7104
+ success: false,
7105
+ error: err.message,
7106
+ code: "AI_ASSERT_ERROR",
7107
+ timestamp: Date.now()
7108
+ };
4224
7109
  }
4225
7110
  },
4226
7111
  aiAssertBatch: async (request) => {
@@ -4229,7 +7114,12 @@ function createAIHandlers(registry, actionExecutor) {
4229
7114
  const result = await assertionExecutor.assertBatch(request);
4230
7115
  return { success: true, data: result, timestamp: Date.now() };
4231
7116
  } catch (err) {
4232
- return { success: false, error: err.message, code: "AI_ASSERT_BATCH_ERROR", timestamp: Date.now() };
7117
+ return {
7118
+ success: false,
7119
+ error: err.message,
7120
+ code: "AI_ASSERT_BATCH_ERROR",
7121
+ timestamp: Date.now()
7122
+ };
4233
7123
  }
4234
7124
  },
4235
7125
  getSemanticSnapshot: async () => {
@@ -4238,7 +7128,12 @@ function createAIHandlers(registry, actionExecutor) {
4238
7128
  const snapshot = snapshotManager.createSnapshot(controlSnapshot);
4239
7129
  return { success: true, data: snapshot, timestamp: Date.now() };
4240
7130
  } catch (err) {
4241
- return { success: false, error: err.message, code: "SEMANTIC_SNAPSHOT_ERROR", timestamp: Date.now() };
7131
+ return {
7132
+ success: false,
7133
+ error: err.message,
7134
+ code: "SEMANTIC_SNAPSHOT_ERROR",
7135
+ timestamp: Date.now()
7136
+ };
4242
7137
  }
4243
7138
  },
4244
7139
  getSemanticDiff: async (_since) => {
@@ -4248,7 +7143,12 @@ function createAIHandlers(registry, actionExecutor) {
4248
7143
  const diff = diffManager.update(currentSnapshot);
4249
7144
  return { success: true, data: diff, timestamp: Date.now() };
4250
7145
  } catch (err) {
4251
- return { success: false, error: err.message, code: "SEMANTIC_DIFF_ERROR", timestamp: Date.now() };
7146
+ return {
7147
+ success: false,
7148
+ error: err.message,
7149
+ code: "SEMANTIC_DIFF_ERROR",
7150
+ timestamp: Date.now()
7151
+ };
4252
7152
  }
4253
7153
  },
4254
7154
  getPageSummary: async () => {
@@ -4266,7 +7166,12 @@ function createAIHandlers(registry, actionExecutor) {
4266
7166
  const summary = generatePageSummary(elements);
4267
7167
  return { success: true, data: summary, timestamp: Date.now() };
4268
7168
  } catch (err) {
4269
- return { success: false, error: err.message, code: "PAGE_SUMMARY_ERROR", timestamp: Date.now() };
7169
+ return {
7170
+ success: false,
7171
+ error: err.message,
7172
+ code: "PAGE_SUMMARY_ERROR",
7173
+ timestamp: Date.now()
7174
+ };
4270
7175
  }
4271
7176
  }
4272
7177
  };