@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
@@ -28,7 +28,12 @@ var UI_BRIDGE_ROUTES = [
28
28
  // Control - Components
29
29
  { method: "GET", path: "/control/components", handler: "getComponents" },
30
30
  { method: "GET", path: "/control/component/:id", handler: "getComponent", params: ["id"] },
31
- { method: "GET", path: "/control/component/:id/state", handler: "getComponentState", params: ["id"] },
31
+ {
32
+ method: "GET",
33
+ path: "/control/component/:id/state",
34
+ handler: "getComponentState",
35
+ params: ["id"]
36
+ },
32
37
  {
33
38
  method: "POST",
34
39
  path: "/control/component/:id/action/:actionId",
@@ -55,6 +60,8 @@ var UI_BRIDGE_ROUTES = [
55
60
  { method: "GET", path: "/debug/metrics", handler: "getMetrics" },
56
61
  { method: "POST", path: "/debug/highlight/:id", handler: "highlightElement", params: ["id"] },
57
62
  { method: "GET", path: "/debug/element-tree", handler: "getElementTree" },
63
+ { method: "GET", path: "/control/console-errors", handler: "getConsoleErrors" },
64
+ { method: "POST", path: "/control/console-errors/clear", handler: "clearConsoleErrors" },
58
65
  // AI-native endpoints
59
66
  { method: "POST", path: "/ai/search", handler: "aiSearch", bodyRequired: true },
60
67
  { method: "POST", path: "/ai/execute", handler: "aiExecute", bodyRequired: true },
@@ -63,7 +70,102 @@ var UI_BRIDGE_ROUTES = [
63
70
  { method: "GET", path: "/ai/snapshot", handler: "getSemanticSnapshot" },
64
71
  { method: "GET", path: "/ai/diff", handler: "getSemanticDiff" },
65
72
  { method: "GET", path: "/ai/summary", handler: "getPageSummary" },
66
- { method: "POST", path: "/ai/semantic-search", handler: "aiSemanticSearch", bodyRequired: true }
73
+ { method: "POST", path: "/ai/semantic-search", handler: "aiSemanticSearch", bodyRequired: true },
74
+ // State management (static routes before parameterized)
75
+ { method: "GET", path: "/control/states", handler: "getStates" },
76
+ { method: "GET", path: "/control/states/active", handler: "getActiveStates" },
77
+ { method: "GET", path: "/control/states/snapshot", handler: "getStateSnapshot" },
78
+ { method: "POST", path: "/control/states/find-path", handler: "findPath", bodyRequired: true },
79
+ { method: "POST", path: "/control/states/navigate", handler: "navigateTo", bodyRequired: true },
80
+ { method: "GET", path: "/control/state/:id", handler: "getState", params: ["id"] },
81
+ { method: "POST", path: "/control/state/:id/activate", handler: "activateState", params: ["id"] },
82
+ {
83
+ method: "POST",
84
+ path: "/control/state/:id/deactivate",
85
+ handler: "deactivateState",
86
+ params: ["id"]
87
+ },
88
+ { method: "GET", path: "/control/state-groups", handler: "getStateGroups" },
89
+ {
90
+ method: "POST",
91
+ path: "/control/state-group/:id/activate",
92
+ handler: "activateStateGroup",
93
+ params: ["id"]
94
+ },
95
+ {
96
+ method: "POST",
97
+ path: "/control/state-group/:id/deactivate",
98
+ handler: "deactivateStateGroup",
99
+ params: ["id"]
100
+ },
101
+ { method: "GET", path: "/control/transitions", handler: "getTransitions" },
102
+ {
103
+ method: "GET",
104
+ path: "/control/transition/:id/can-execute",
105
+ handler: "canExecuteTransition",
106
+ params: ["id"]
107
+ },
108
+ {
109
+ method: "POST",
110
+ path: "/control/transition/:id/execute",
111
+ handler: "executeTransition",
112
+ params: ["id"]
113
+ },
114
+ // Intent endpoints
115
+ { method: "GET", path: "/ai/intents", handler: "listIntents" },
116
+ { method: "POST", path: "/ai/intents/execute", handler: "executeIntent", bodyRequired: true },
117
+ { method: "POST", path: "/ai/intents/find", handler: "findIntent", bodyRequired: true },
118
+ { method: "POST", path: "/ai/intents/register", handler: "registerIntent", bodyRequired: true },
119
+ {
120
+ method: "POST",
121
+ path: "/ai/intents/execute-from-query",
122
+ handler: "executeIntentFromQuery",
123
+ bodyRequired: true
124
+ },
125
+ // Recovery endpoints
126
+ {
127
+ method: "POST",
128
+ path: "/ai/recovery/attempt",
129
+ handler: "attemptRecovery",
130
+ bodyRequired: true
131
+ },
132
+ // Cross-app analysis endpoints
133
+ { method: "GET", path: "/ai/analyze/data", handler: "analyzePageData" },
134
+ { method: "GET", path: "/ai/analyze/regions", handler: "analyzePageRegions" },
135
+ { method: "GET", path: "/ai/analyze/structured-data", handler: "analyzeStructuredData" },
136
+ {
137
+ method: "POST",
138
+ path: "/ai/analyze/cross-app-compare",
139
+ handler: "crossAppCompare",
140
+ bodyRequired: true
141
+ },
142
+ // Page navigation
143
+ { method: "POST", path: "/control/page/refresh", handler: "pageRefresh" },
144
+ { method: "POST", path: "/control/page/navigate", handler: "pageNavigate", bodyRequired: true },
145
+ { method: "POST", path: "/control/page/back", handler: "pageGoBack" },
146
+ { method: "POST", path: "/control/page/forward", handler: "pageGoForward" },
147
+ // Annotations (static routes before parameterized)
148
+ { method: "GET", path: "/annotations", handler: "getAnnotations" },
149
+ { method: "GET", path: "/annotations/export", handler: "exportAnnotations" },
150
+ { method: "GET", path: "/annotations/coverage", handler: "getAnnotationCoverage" },
151
+ { method: "POST", path: "/annotations/import", handler: "importAnnotations", bodyRequired: true },
152
+ { method: "GET", path: "/annotations/:id", handler: "getAnnotation", params: ["id"] },
153
+ {
154
+ method: "PUT",
155
+ path: "/annotations/:id",
156
+ handler: "setAnnotation",
157
+ params: ["id"],
158
+ bodyRequired: true
159
+ },
160
+ { method: "DELETE", path: "/annotations/:id", handler: "deleteAnnotation", params: ["id"] },
161
+ // Performance diagnostics
162
+ { method: "GET", path: "/control/performance-entries", handler: "getPerformanceEntries" },
163
+ {
164
+ method: "POST",
165
+ path: "/control/performance-entries/clear",
166
+ handler: "clearPerformanceEntries"
167
+ },
168
+ { method: "GET", path: "/control/browser-events", handler: "getBrowserEvents" }
67
169
  ];
68
170
 
69
171
  // src/ai/fuzzy-matcher.ts
@@ -489,17 +591,72 @@ function generateDescription(input) {
489
591
  }
490
592
  parts.push(`"${name}"`);
491
593
  }
492
- const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [input.elementType || "element"];
594
+ const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
595
+ input.elementType || "element"
596
+ ];
493
597
  parts.push(typeWords[0]);
494
598
  if (input.inputType && input.inputType !== "text") {
495
599
  parts.push(`(${input.inputType})`);
496
600
  }
497
601
  return parts.join(" ");
498
602
  }
603
+ var CONTENT_TYPES = /* @__PURE__ */ new Set([
604
+ "heading",
605
+ "paragraph",
606
+ "list-item",
607
+ "table-cell",
608
+ "table-header",
609
+ "label",
610
+ "caption",
611
+ "blockquote",
612
+ "code-block",
613
+ "badge",
614
+ "status-message",
615
+ "metric-value",
616
+ "description-text",
617
+ "nav-text",
618
+ "content-generic"
619
+ ]);
499
620
  function generatePurpose(input) {
500
621
  const text = (input.textContent || input.ariaLabel || input.title || "").toLowerCase();
501
622
  const type = input.elementType?.toLowerCase() || "";
502
623
  const inputType = input.inputType?.toLowerCase() || "";
624
+ if (CONTENT_TYPES.has(type)) {
625
+ switch (type) {
626
+ case "heading":
627
+ return "Section heading";
628
+ case "paragraph":
629
+ return "Body text content";
630
+ case "list-item":
631
+ return "List item";
632
+ case "table-cell":
633
+ return "Table data cell";
634
+ case "table-header":
635
+ return "Table column header";
636
+ case "label":
637
+ return "Field label or definition term";
638
+ case "caption":
639
+ return "Figure or table caption";
640
+ case "blockquote":
641
+ return "Quoted content";
642
+ case "code-block":
643
+ return "Code or preformatted text";
644
+ case "badge":
645
+ return "Status badge or tag";
646
+ case "status-message":
647
+ return "Dynamic status indicator";
648
+ case "metric-value":
649
+ return "Metric or statistic value";
650
+ case "description-text":
651
+ return "Description or definition";
652
+ case "nav-text":
653
+ return "Navigation label";
654
+ case "content-generic":
655
+ return "Text content";
656
+ default:
657
+ return "Static content";
658
+ }
659
+ }
503
660
  if (type === "button" || inputType === "submit") {
504
661
  if (text.match(/submit|send|save|confirm|ok|done|finish|apply/)) {
505
662
  return "Submits the form";
@@ -564,6 +721,10 @@ function generateSuggestedActions(input) {
564
721
  const inputType = input.inputType?.toLowerCase() || "";
565
722
  const text = (input.textContent || input.ariaLabel || "").toLowerCase();
566
723
  const actions = [];
724
+ if (CONTENT_TYPES.has(type)) {
725
+ actions.push("read text content", "verify text matches expected");
726
+ return actions;
727
+ }
567
728
  switch (type) {
568
729
  case "button":
569
730
  actions.push(`click "${text || "this button"}"`);
@@ -611,6 +772,241 @@ function areSynonyms(word1, word2) {
611
772
  return synonyms1.includes(w2) || synonyms2.includes(w1);
612
773
  }
613
774
 
775
+ // src/annotations/types.ts
776
+ var ANNOTATION_CONFIG_VERSION = "1.0.0";
777
+
778
+ // src/annotations/store.ts
779
+ var AnnotationStore = class {
780
+ constructor() {
781
+ this.store = /* @__PURE__ */ new Map();
782
+ this.listeners = /* @__PURE__ */ new Set();
783
+ }
784
+ /**
785
+ * Get an annotation by element ID.
786
+ */
787
+ get(elementId) {
788
+ return this.store.get(elementId);
789
+ }
790
+ /**
791
+ * Get all annotations as a record.
792
+ */
793
+ getAll() {
794
+ const result = {};
795
+ for (const [id, annotation] of this.store) {
796
+ result[id] = annotation;
797
+ }
798
+ return result;
799
+ }
800
+ /**
801
+ * Set an annotation for an element. Auto-sets `updatedAt`.
802
+ */
803
+ set(elementId, annotation) {
804
+ const updated = {
805
+ ...annotation,
806
+ updatedAt: Date.now()
807
+ };
808
+ this.store.set(elementId, updated);
809
+ this.emit({
810
+ type: "annotation:set",
811
+ elementId,
812
+ annotation: updated,
813
+ timestamp: Date.now()
814
+ });
815
+ }
816
+ /**
817
+ * Delete an annotation by element ID.
818
+ *
819
+ * @returns true if the annotation existed and was deleted
820
+ */
821
+ delete(elementId) {
822
+ const existed = this.store.delete(elementId);
823
+ if (existed) {
824
+ this.emit({
825
+ type: "annotation:deleted",
826
+ elementId,
827
+ timestamp: Date.now()
828
+ });
829
+ }
830
+ return existed;
831
+ }
832
+ /**
833
+ * Check if an annotation exists for an element.
834
+ */
835
+ has(elementId) {
836
+ return this.store.has(elementId);
837
+ }
838
+ /**
839
+ * Get the number of stored annotations.
840
+ */
841
+ get count() {
842
+ return this.store.size;
843
+ }
844
+ /**
845
+ * Clear all annotations.
846
+ */
847
+ clear() {
848
+ this.store.clear();
849
+ this.emit({
850
+ type: "annotation:cleared",
851
+ timestamp: Date.now()
852
+ });
853
+ }
854
+ /**
855
+ * Import annotations from a config object.
856
+ *
857
+ * Merges with existing annotations (new values overwrite per element ID).
858
+ *
859
+ * @returns Number of annotations imported
860
+ *
861
+ * @example
862
+ * ```ts
863
+ * const config: AnnotationConfig = {
864
+ * version: '1.0.0',
865
+ * annotations: {
866
+ * 'btn-1': { description: 'Submit button', tags: ['form'] },
867
+ * 'input-1': { description: 'Name field' },
868
+ * },
869
+ * };
870
+ * const count = store.importConfig(config); // 2
871
+ * ```
872
+ */
873
+ importConfig(config) {
874
+ let count = 0;
875
+ for (const [id, annotation] of Object.entries(config.annotations)) {
876
+ this.store.set(id, {
877
+ ...annotation,
878
+ updatedAt: annotation.updatedAt ?? Date.now()
879
+ });
880
+ count++;
881
+ }
882
+ this.emit({
883
+ type: "annotation:imported",
884
+ count,
885
+ timestamp: Date.now()
886
+ });
887
+ return count;
888
+ }
889
+ /**
890
+ * Export all annotations as a config object.
891
+ *
892
+ * The returned object can be serialized to JSON and saved to a file,
893
+ * then later re-imported with {@link importConfig}.
894
+ *
895
+ * @param metadata - Optional metadata to include (appName, description, etc.)
896
+ * @returns AnnotationConfig with all current annotations
897
+ *
898
+ * @example
899
+ * ```ts
900
+ * const config = store.exportConfig({ appName: 'MyApp' });
901
+ * // config.version === '1.0.0'
902
+ * // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
903
+ * // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
904
+ *
905
+ * // Save to file
906
+ * fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
907
+ * ```
908
+ */
909
+ exportConfig(metadata) {
910
+ return {
911
+ version: ANNOTATION_CONFIG_VERSION,
912
+ annotations: this.getAll(),
913
+ metadata: {
914
+ ...metadata,
915
+ exportedAt: Date.now()
916
+ }
917
+ };
918
+ }
919
+ /**
920
+ * Compute annotation coverage against a set of known element IDs.
921
+ *
922
+ * Compares the store's annotations against the provided list of element IDs
923
+ * to determine what percentage of elements have been annotated.
924
+ *
925
+ * @param allElementIds - Array of all known element IDs in the UI
926
+ * @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
927
+ *
928
+ * @example
929
+ * ```ts
930
+ * store.set('btn-1', { description: 'Submit' });
931
+ * store.set('input-1', { description: 'Name' });
932
+ *
933
+ * const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
934
+ * // coverage.totalElements === 4
935
+ * // coverage.annotatedElements === 2
936
+ * // coverage.coveragePercent === 50
937
+ * // coverage.annotatedIds === ['btn-1', 'input-1']
938
+ * // coverage.unannotatedIds === ['input-2', 'link-1']
939
+ * ```
940
+ */
941
+ getCoverage(allElementIds) {
942
+ const annotatedIds = [];
943
+ const unannotatedIds = [];
944
+ for (const id of allElementIds) {
945
+ if (this.store.has(id)) {
946
+ annotatedIds.push(id);
947
+ } else {
948
+ unannotatedIds.push(id);
949
+ }
950
+ }
951
+ const total = allElementIds.length;
952
+ return {
953
+ totalElements: total,
954
+ annotatedElements: annotatedIds.length,
955
+ coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
956
+ annotatedIds,
957
+ unannotatedIds,
958
+ timestamp: Date.now()
959
+ };
960
+ }
961
+ /**
962
+ * Subscribe to annotation events.
963
+ *
964
+ * The listener is called whenever annotations are set, deleted, imported,
965
+ * or cleared. Returns an unsubscribe function to stop listening.
966
+ *
967
+ * @param listener - Callback function receiving {@link AnnotationEvent} objects
968
+ * @returns Unsubscribe function - call it to remove the listener
969
+ *
970
+ * @example
971
+ * ```ts
972
+ * const unsubscribe = store.on((event) => {
973
+ * if (event.type === 'annotation:set') {
974
+ * console.log(`Element ${event.elementId} annotated:`, event.annotation);
975
+ * }
976
+ * });
977
+ *
978
+ * store.set('btn-1', { description: 'Submit' });
979
+ * // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
980
+ *
981
+ * unsubscribe(); // Stop listening
982
+ * ```
983
+ */
984
+ on(listener) {
985
+ this.listeners.add(listener);
986
+ return () => {
987
+ this.listeners.delete(listener);
988
+ };
989
+ }
990
+ /**
991
+ * Emit an event to all listeners.
992
+ */
993
+ emit(event) {
994
+ for (const listener of this.listeners) {
995
+ try {
996
+ listener(event);
997
+ } catch {
998
+ }
999
+ }
1000
+ }
1001
+ };
1002
+ var globalStore = null;
1003
+ function getGlobalAnnotationStore() {
1004
+ if (!globalStore) {
1005
+ globalStore = new AnnotationStore();
1006
+ }
1007
+ return globalStore;
1008
+ }
1009
+
614
1010
  // src/ai/search-engine.ts
615
1011
  var DEFAULT_SEARCH_CONFIG = {
616
1012
  fuzzyThreshold: 0.7,
@@ -653,17 +1049,40 @@ var SearchEngine = class {
653
1049
  if ("getState" in element && typeof element.getState === "function") {
654
1050
  state = getState ? getState(element) : element.getState();
655
1051
  textContent = state.textContent || void 0;
656
- tagName = element.element.tagName.toLowerCase();
657
- role = element.element.getAttribute("role") || void 0;
658
- ariaLabel = element.element.getAttribute("aria-label") || void 0;
659
- placeholder = element.element.getAttribute("placeholder") || void 0;
660
- title = element.element.getAttribute("title") || void 0;
661
- if (element.element.id) {
662
- const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
663
- labelText = labelEl?.textContent?.trim() || void 0;
664
- }
665
- if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
666
- value = element.element.value || void 0;
1052
+ try {
1053
+ tagName = element.element.tagName.toLowerCase();
1054
+ } catch {
1055
+ tagName = element.type || "unknown";
1056
+ }
1057
+ try {
1058
+ role = element.element.getAttribute("role") || void 0;
1059
+ ariaLabel = element.element.getAttribute("aria-label") || void 0;
1060
+ placeholder = element.element.getAttribute("placeholder") || void 0;
1061
+ title = element.element.getAttribute("title") || void 0;
1062
+ } catch {
1063
+ }
1064
+ if (!ariaLabel && element.label) {
1065
+ ariaLabel = element.label;
1066
+ }
1067
+ try {
1068
+ if (element.element.id) {
1069
+ const labelEl = document.querySelector(`label[for="${element.element.id}"]`);
1070
+ labelText = labelEl?.textContent?.trim() || void 0;
1071
+ }
1072
+ } catch {
1073
+ }
1074
+ if (!labelText && element.label) {
1075
+ labelText = element.label;
1076
+ }
1077
+ if (!textContent && element.label) {
1078
+ textContent = element.label;
1079
+ }
1080
+ try {
1081
+ if (element.element instanceof HTMLInputElement || element.element instanceof HTMLTextAreaElement || element.element instanceof HTMLSelectElement) {
1082
+ value = element.element.value || void 0;
1083
+ }
1084
+ } catch {
1085
+ value = state.value || void 0;
667
1086
  }
668
1087
  } else {
669
1088
  const discovered = element;
@@ -672,8 +1091,11 @@ var SearchEngine = class {
672
1091
  tagName = discovered.tagName;
673
1092
  role = discovered.role || void 0;
674
1093
  ariaLabel = discovered.accessibleName || void 0;
1094
+ if (!labelText && element.label) {
1095
+ labelText = element.label;
1096
+ }
675
1097
  }
676
- const aliases = generateAliases({
1098
+ let aliases = generateAliases({
677
1099
  textContent,
678
1100
  ariaLabel,
679
1101
  placeholder,
@@ -683,7 +1105,14 @@ var SearchEngine = class {
683
1105
  labelText,
684
1106
  value
685
1107
  });
686
- const description = generateDescription({
1108
+ if ("aliases" in element && Array.isArray(element.aliases) && element.aliases.length > 0) {
1109
+ const aliasSet = /* @__PURE__ */ new Set([
1110
+ ...aliases,
1111
+ ...element.aliases.map((a) => a.toLowerCase())
1112
+ ]);
1113
+ aliases = [...aliasSet];
1114
+ }
1115
+ let description = generateDescription({
687
1116
  textContent,
688
1117
  ariaLabel,
689
1118
  placeholder,
@@ -692,6 +1121,22 @@ var SearchEngine = class {
692
1121
  id: element.id,
693
1122
  labelText
694
1123
  });
1124
+ if (!description && "description" in element && element.description) {
1125
+ description = element.description;
1126
+ }
1127
+ const annotation = getGlobalAnnotationStore().get(element.id);
1128
+ if (annotation) {
1129
+ if (annotation.description) {
1130
+ description = annotation.description;
1131
+ }
1132
+ if (annotation.tags && annotation.tags.length > 0) {
1133
+ const tagSet = /* @__PURE__ */ new Set([...aliases, ...annotation.tags.map((t) => t.toLowerCase())]);
1134
+ aliases = [...tagSet];
1135
+ }
1136
+ if (annotation.notes) {
1137
+ aliases.push(annotation.notes.toLowerCase());
1138
+ }
1139
+ }
695
1140
  return {
696
1141
  id: element.id,
697
1142
  element,
@@ -794,7 +1239,12 @@ var SearchEngine = class {
794
1239
  threshold: criteria.fuzzyThreshold ?? this.config.fuzzyThreshold
795
1240
  };
796
1241
  if (criteria.text) {
797
- const textScore = this.scoreTextMatch(searchable, criteria.text, criteria.fuzzy !== false, fuzzyConfig.threshold);
1242
+ const textScore = this.scoreTextMatch(
1243
+ searchable,
1244
+ criteria.text,
1245
+ criteria.fuzzy !== false,
1246
+ fuzzyConfig.threshold
1247
+ );
798
1248
  scores.text = textScore.score;
799
1249
  if (textScore.score > 0) {
800
1250
  matchReasons.push(...textScore.reasons);
@@ -802,8 +1252,37 @@ var SearchEngine = class {
802
1252
  weightedScore += textScore.score * this.config.textWeight;
803
1253
  totalWeight += this.config.textWeight;
804
1254
  }
1255
+ if (criteria.textContent && !criteria.text) {
1256
+ const alternatives = criteria.textContent.includes("|") ? criteria.textContent.split("|").map((s) => s.trim()).filter(Boolean) : [criteria.textContent];
1257
+ let bestScore = 0;
1258
+ let bestReasons = [];
1259
+ for (const alt of alternatives) {
1260
+ const exactScore = this.scoreTextMatch(
1261
+ searchable,
1262
+ alt,
1263
+ criteria.fuzzy !== false,
1264
+ fuzzyConfig.threshold
1265
+ );
1266
+ const containsScore = this.scoreContainsMatch(searchable, alt, criteria.fuzzy !== false);
1267
+ const altBest = Math.max(exactScore.score, containsScore.score);
1268
+ if (altBest > bestScore) {
1269
+ bestScore = altBest;
1270
+ bestReasons = exactScore.score >= containsScore.score ? exactScore.reasons : containsScore.reasons;
1271
+ }
1272
+ }
1273
+ scores.text = bestScore;
1274
+ if (bestScore > 0) {
1275
+ matchReasons.push(...bestReasons);
1276
+ }
1277
+ weightedScore += bestScore * this.config.textWeight;
1278
+ totalWeight += this.config.textWeight;
1279
+ }
805
1280
  if (criteria.textContains) {
806
- const containsScore = this.scoreContainsMatch(searchable, criteria.textContains, criteria.fuzzy !== false);
1281
+ const containsScore = this.scoreContainsMatch(
1282
+ searchable,
1283
+ criteria.textContains,
1284
+ criteria.fuzzy !== false
1285
+ );
807
1286
  scores.text = Math.max(scores.text || 0, containsScore.score);
808
1287
  if (containsScore.score > 0 && containsScore.reasons.length > 0) {
809
1288
  matchReasons.push(...containsScore.reasons);
@@ -852,7 +1331,11 @@ var SearchEngine = class {
852
1331
  totalWeight += this.config.spatialWeight;
853
1332
  }
854
1333
  if (criteria.placeholder && searchable.placeholder) {
855
- const placeholderResult = fuzzyMatch(searchable.placeholder, criteria.placeholder, fuzzyConfig);
1334
+ const placeholderResult = fuzzyMatch(
1335
+ searchable.placeholder,
1336
+ criteria.placeholder,
1337
+ fuzzyConfig
1338
+ );
856
1339
  if (placeholderResult.isMatch) {
857
1340
  matchReasons.push(`placeholder matches`);
858
1341
  weightedScore += placeholderResult.similarity * this.config.textWeight;
@@ -897,11 +1380,9 @@ var SearchEngine = class {
897
1380
  scoreTextMatch(searchable, text, fuzzy, threshold) {
898
1381
  const reasons = [];
899
1382
  let maxScore = 0;
900
- const textsToMatch = [
901
- searchable.textContent,
902
- searchable.labelText,
903
- searchable.value
904
- ].filter(Boolean);
1383
+ const textsToMatch = [searchable.textContent, searchable.labelText, searchable.value].filter(
1384
+ Boolean
1385
+ );
905
1386
  for (const targetText of textsToMatch) {
906
1387
  if (targetText.toLowerCase() === text.toLowerCase()) {
907
1388
  maxScore = Math.max(maxScore, 1);
@@ -997,7 +1478,9 @@ var SearchEngine = class {
997
1478
  heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
998
1479
  };
999
1480
  const inferredRoles = tagRoleMap[normalizedRole] || [];
1000
- if (inferredRoles.some((r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole)) {
1481
+ if (inferredRoles.some(
1482
+ (r) => searchable.tagName === r || searchable.type.toLowerCase() === normalizedRole
1483
+ )) {
1001
1484
  return { score: 0.8, reasons: [`inferred role: ${role}`] };
1002
1485
  }
1003
1486
  return { score: 0, reasons };
@@ -1184,11 +1667,14 @@ function generatePageSummary(elements, pageContext, config = {}) {
1184
1667
  if (finalConfig.includeElementCounts) {
1185
1668
  const counts = countElementTypes(elements);
1186
1669
  const countParts = [];
1187
- if (counts.button > 0) countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1670
+ if (counts.button > 0)
1671
+ countParts.push(`${counts.button} button${counts.button > 1 ? "s" : ""}`);
1188
1672
  if (counts.input > 0) countParts.push(`${counts.input} input${counts.input > 1 ? "s" : ""}`);
1189
1673
  if (counts.link > 0) countParts.push(`${counts.link} link${counts.link > 1 ? "s" : ""}`);
1190
- if (counts.select > 0) countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1191
- if (counts.checkbox > 0) countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1674
+ if (counts.select > 0)
1675
+ countParts.push(`${counts.select} dropdown${counts.select > 1 ? "s" : ""}`);
1676
+ if (counts.checkbox > 0)
1677
+ countParts.push(`${counts.checkbox} checkbox${counts.checkbox > 1 ? "es" : ""}`);
1192
1678
  if (countParts.length > 0) {
1193
1679
  lines.push(`Contains: ${countParts.join(", ")}`);
1194
1680
  }
@@ -1231,7 +1717,9 @@ function generateFormSummary(form, verbosity) {
1231
1717
  if (verbosity === "brief") {
1232
1718
  const fieldCount = form.fields.length;
1233
1719
  const filledCount = form.fields.filter((f) => f.value).length;
1234
- lines.push(` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`);
1720
+ lines.push(
1721
+ ` ${filledCount}/${fieldCount} fields filled, ${form.isValid ? "valid" : "has errors"}`
1722
+ );
1235
1723
  } else {
1236
1724
  for (const field of form.fields) {
1237
1725
  let fieldLine = ` - ${field.label || field.id}`;
@@ -1266,7 +1754,9 @@ function generateDiffSummary(appeared, disappeared, modified) {
1266
1754
  if (modified.length > 0) {
1267
1755
  lines.push("Changed:");
1268
1756
  for (const mod of modified.slice(0, 5)) {
1269
- lines.push(` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`);
1757
+ lines.push(
1758
+ ` - ${mod.description}: ${mod.property} changed from "${mod.from}" to "${mod.to}"`
1759
+ );
1270
1760
  }
1271
1761
  if (modified.length > 5) {
1272
1762
  lines.push(` ... and ${modified.length - 5} more changes`);
@@ -1697,7 +2187,9 @@ function parseNLInstruction(instruction) {
1697
2187
  }
1698
2188
  }
1699
2189
  if (pattern.action === "assert") {
1700
- const assertMatch = trimmed.match(/(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i);
2190
+ const assertMatch = trimmed.match(
2191
+ /(visible|hidden|enabled|disabled|checked|unchecked|focused|contains|has)/i
2192
+ );
1701
2193
  if (assertMatch) {
1702
2194
  parsed.assertionType = ASSERTION_TYPE_MAP[assertMatch[1].toLowerCase()];
1703
2195
  }
@@ -2458,7 +2950,9 @@ var NLActionExecutor = class {
2458
2950
  switch (errorCode) {
2459
2951
  case "PARSE_ERROR":
2460
2952
  suggestions.push('Try using a simpler phrase like "click Submit button"');
2461
- suggestions.push('Ensure the instruction follows patterns like "click X" or "type Y into X"');
2953
+ suggestions.push(
2954
+ 'Ensure the instruction follows patterns like "click X" or "type Y into X"'
2955
+ );
2462
2956
  break;
2463
2957
  case "ELEMENT_NOT_FOUND":
2464
2958
  if (alternatives.length > 0) {
@@ -2526,9 +3020,15 @@ var AssertionExecutor = class {
2526
3020
  async assert(request) {
2527
3021
  const startTime = performance.now();
2528
3022
  const timeout = request.timeout ?? this.config.defaultTimeout;
2529
- const element = await this.findElement(request.target, request.fuzzy !== false);
3023
+ const searchResult = this.findElementDetailed(request.target, request.fuzzy !== false);
3024
+ const element = searchResult?.element ?? null;
3025
+ const searchDetails = searchResult ? {
3026
+ confidence: searchResult.confidence,
3027
+ matchReasons: searchResult.matchReasons,
3028
+ candidateCount: this.elements.length
3029
+ } : void 0;
2530
3030
  if (!element && request.type !== "notExists") {
2531
- return this.createResult(
3031
+ const result2 = this.createResult(
2532
3032
  false,
2533
3033
  typeof request.target === "string" ? request.target : JSON.stringify(request.target),
2534
3034
  "element not found",
@@ -2538,8 +3038,16 @@ var AssertionExecutor = class {
2538
3038
  this.config.includeSuggestions ? "Check if the element exists and is properly labeled" : void 0,
2539
3039
  startTime
2540
3040
  );
3041
+ if (searchDetails) {
3042
+ result2.searchDetails = searchDetails;
3043
+ }
3044
+ return result2;
2541
3045
  }
2542
- return this.executeAssertion(request, element, timeout, startTime);
3046
+ const result = await this.executeAssertion(request, element, timeout, startTime);
3047
+ if (searchDetails) {
3048
+ result.searchDetails = searchDetails;
3049
+ }
3050
+ return result;
2543
3051
  }
2544
3052
  /**
2545
3053
  * Execute multiple assertions
@@ -2644,16 +3152,26 @@ var AssertionExecutor = class {
2644
3152
  return this.assert({ target, type: "count", expected: expectedCount, timeout });
2645
3153
  }
2646
3154
  /**
2647
- * Find element by target (string or criteria)
3155
+ * Find element by target with full search metadata.
3156
+ * Returns the SearchResult (including confidence, matchReasons, scores)
3157
+ * or null if no match above the fuzzy threshold.
2648
3158
  */
2649
- async findElement(target, fuzzy = true) {
3159
+ findElementDetailed(target, fuzzy = true) {
2650
3160
  const criteria = typeof target === "string" ? { text: target, fuzzy } : { ...target, fuzzy };
2651
- const searchResult = this.searchEngine.findBest(criteria);
3161
+ const searchResult = this.searchEngine.findBest(criteria, this.elements);
2652
3162
  if (searchResult && searchResult.confidence >= this.config.fuzzyThreshold) {
2653
- return searchResult.element;
3163
+ return searchResult;
2654
3164
  }
2655
3165
  return null;
2656
3166
  }
3167
+ /**
3168
+ * Find element by target (string or criteria).
3169
+ * Public for use by condition evaluation in SpecExecutor.
3170
+ */
3171
+ async findElement(target, fuzzy = true) {
3172
+ const result = this.findElementDetailed(target, fuzzy);
3173
+ return result?.element ?? null;
3174
+ }
2657
3175
  /**
2658
3176
  * Execute the actual assertion
2659
3177
  */
@@ -2662,19 +3180,55 @@ var AssertionExecutor = class {
2662
3180
  const elementDescription = element?.description || targetStr;
2663
3181
  switch (request.type) {
2664
3182
  case "visible":
2665
- return this.assertVisibility(element, true, elementDescription, request.message, startTime);
3183
+ return this.assertVisibility(
3184
+ element,
3185
+ true,
3186
+ elementDescription,
3187
+ request.message,
3188
+ startTime
3189
+ );
2666
3190
  case "hidden":
2667
- return this.assertVisibility(element, false, elementDescription, request.message, startTime);
3191
+ return this.assertVisibility(
3192
+ element,
3193
+ false,
3194
+ elementDescription,
3195
+ request.message,
3196
+ startTime
3197
+ );
2668
3198
  case "enabled":
2669
- return this.assertEnabledState(element, true, elementDescription, request.message, startTime);
3199
+ return this.assertEnabledState(
3200
+ element,
3201
+ true,
3202
+ elementDescription,
3203
+ request.message,
3204
+ startTime
3205
+ );
2670
3206
  case "disabled":
2671
- return this.assertEnabledState(element, false, elementDescription, request.message, startTime);
3207
+ return this.assertEnabledState(
3208
+ element,
3209
+ false,
3210
+ elementDescription,
3211
+ request.message,
3212
+ startTime
3213
+ );
2672
3214
  case "focused":
2673
3215
  return this.assertFocused(element, elementDescription, request.message, startTime);
2674
3216
  case "checked":
2675
- return this.assertCheckedState(element, true, elementDescription, request.message, startTime);
3217
+ return this.assertCheckedState(
3218
+ element,
3219
+ true,
3220
+ elementDescription,
3221
+ request.message,
3222
+ startTime
3223
+ );
2676
3224
  case "unchecked":
2677
- return this.assertCheckedState(element, false, elementDescription, request.message, startTime);
3225
+ return this.assertCheckedState(
3226
+ element,
3227
+ false,
3228
+ elementDescription,
3229
+ request.message,
3230
+ startTime
3231
+ );
2678
3232
  case "hasText":
2679
3233
  return this.assertTextMatch(
2680
3234
  element,
@@ -3009,7 +3563,8 @@ var DEFAULT_SNAPSHOT_CONFIG = {
3009
3563
  detectModals: true,
3010
3564
  inferPageType: true,
3011
3565
  generateDescriptions: true,
3012
- maxElements: 500
3566
+ maxElements: 500,
3567
+ useAnnotations: true
3013
3568
  };
3014
3569
  var SemanticSnapshotManager = class {
3015
3570
  constructor(config = {}) {
@@ -3082,26 +3637,52 @@ var SemanticSnapshotManager = class {
3082
3637
  * Convert a single element to AI element
3083
3638
  */
3084
3639
  convertElement(element) {
3640
+ const isContent = element.category === "content";
3085
3641
  const aliases = generateAliases({
3086
3642
  textContent: element.state.textContent,
3087
3643
  elementType: element.type,
3088
3644
  id: element.id,
3089
3645
  labelText: element.label
3090
3646
  });
3091
- const description = this.config.generateDescriptions ? generateDescription({
3092
- textContent: element.state.textContent,
3093
- elementType: element.type,
3094
- id: element.id,
3095
- labelText: element.label
3096
- }) : element.label || element.id;
3097
- const purpose = generatePurpose({
3647
+ let description;
3648
+ if (isContent && element.contentMetadata) {
3649
+ description = this.generateContentDescription(element);
3650
+ } else if (this.config.generateDescriptions) {
3651
+ description = generateDescription({
3652
+ textContent: element.state.textContent,
3653
+ elementType: element.type,
3654
+ id: element.id,
3655
+ labelText: element.label
3656
+ });
3657
+ } else {
3658
+ description = element.label || element.id;
3659
+ }
3660
+ const purpose = isContent ? generatePurpose({ textContent: element.state.textContent, elementType: element.type }) : generatePurpose({ textContent: element.state.textContent, elementType: element.type });
3661
+ const suggestedActions = isContent ? generateSuggestedActions({
3098
3662
  textContent: element.state.textContent,
3099
3663
  elementType: element.type
3100
- });
3101
- const suggestedActions = generateSuggestedActions({
3664
+ }) : generateSuggestedActions({
3102
3665
  textContent: element.state.textContent,
3103
3666
  elementType: element.type
3104
3667
  });
3668
+ let finalDescription = description;
3669
+ let finalPurpose = purpose;
3670
+ let finalAliases = aliases;
3671
+ if (this.config.useAnnotations) {
3672
+ const annotation = getGlobalAnnotationStore().get(element.id);
3673
+ if (annotation) {
3674
+ if (annotation.description) {
3675
+ finalDescription = annotation.description;
3676
+ }
3677
+ if (annotation.purpose) {
3678
+ finalPurpose = annotation.purpose;
3679
+ }
3680
+ if (annotation.tags && annotation.tags.length > 0) {
3681
+ const tagSet = /* @__PURE__ */ new Set([...finalAliases, ...annotation.tags.map((t) => t.toLowerCase())]);
3682
+ finalAliases = [...tagSet];
3683
+ }
3684
+ }
3685
+ }
3105
3686
  return {
3106
3687
  id: element.id,
3107
3688
  type: element.type,
@@ -3112,13 +3693,56 @@ var SemanticSnapshotManager = class {
3112
3693
  actions: element.actions,
3113
3694
  state: element.state,
3114
3695
  registered: true,
3115
- description,
3116
- aliases,
3117
- purpose,
3696
+ description: finalDescription,
3697
+ aliases: finalAliases,
3698
+ purpose: finalPurpose,
3118
3699
  suggestedActions,
3119
- semanticType: this.inferSemanticType(element)
3700
+ semanticType: this.inferSemanticType(element),
3701
+ category: element.category,
3702
+ contentMetadata: element.contentMetadata
3120
3703
  };
3121
3704
  }
3705
+ /**
3706
+ * Generate a content-specific description
3707
+ */
3708
+ generateContentDescription(element) {
3709
+ const meta = element.contentMetadata;
3710
+ const text = element.state.textContent?.trim() || "";
3711
+ const truncatedText = text.length > 60 ? text.substring(0, 57) + "..." : text;
3712
+ if (!meta) return `"${truncatedText}"`;
3713
+ switch (meta.contentRole) {
3714
+ case "heading":
3715
+ return `Level ${meta.headingLevel || "?"} heading: '${truncatedText}'`;
3716
+ case "table-cell":
3717
+ return `Table cell${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3718
+ case "table-header":
3719
+ return `Table header${meta.structuralContext ? ` (${meta.structuralContext})` : ""}: '${truncatedText}'`;
3720
+ case "status":
3721
+ return `Status message: '${truncatedText}'`;
3722
+ case "badge":
3723
+ return `Badge: '${truncatedText}'`;
3724
+ case "metric":
3725
+ return `Metric value: '${truncatedText}'`;
3726
+ case "body-text":
3727
+ return `Text: '${truncatedText}'`;
3728
+ case "list-item":
3729
+ return `List item: '${truncatedText}'`;
3730
+ case "quote":
3731
+ return `Blockquote: '${truncatedText}'`;
3732
+ case "code":
3733
+ return `Code block: '${truncatedText}'`;
3734
+ case "caption":
3735
+ return `Caption: '${truncatedText}'`;
3736
+ case "label":
3737
+ return `Label: '${truncatedText}'`;
3738
+ case "description":
3739
+ return `Description: '${truncatedText}'`;
3740
+ case "navigation":
3741
+ return `Navigation text: '${truncatedText}'`;
3742
+ default:
3743
+ return `Content: '${truncatedText}'`;
3744
+ }
3745
+ }
3122
3746
  /**
3123
3747
  * Build full page context
3124
3748
  */
@@ -3222,9 +3846,7 @@ var SemanticSnapshotManager = class {
3222
3846
  */
3223
3847
  detectModals(elements) {
3224
3848
  const modals = [];
3225
- const dialogElements = elements.filter(
3226
- (el) => el.type === "dialog" && el.state.visible
3227
- );
3849
+ const dialogElements = elements.filter((el) => el.type === "dialog" && el.state.visible);
3228
3850
  for (const dialog of dialogElements) {
3229
3851
  const closeButton = elements.find(
3230
3852
  (el) => el.type === "button" && el.state.visible && (el.semanticType === "cancel-button" || el.state.textContent?.toLowerCase().match(/close|cancel|x|dismiss/))
@@ -3275,9 +3897,7 @@ var SemanticSnapshotManager = class {
3275
3897
  * Infer form purpose from fields
3276
3898
  */
3277
3899
  inferFormPurpose(fields) {
3278
- const labels = fields.map(
3279
- (f) => (f.accessibleName || f.label || "").toLowerCase()
3280
- );
3900
+ const labels = fields.map((f) => (f.accessibleName || f.label || "").toLowerCase());
3281
3901
  const allLabels = labels.join(" ");
3282
3902
  if (allLabels.includes("email") && allLabels.includes("password")) {
3283
3903
  if (allLabels.includes("confirm") || allLabels.includes("name")) {
@@ -3331,6 +3951,13 @@ var SemanticSnapshotManager = class {
3331
3951
  * Infer semantic type
3332
3952
  */
3333
3953
  inferSemanticType(element) {
3954
+ if (element.category === "content" && element.contentMetadata) {
3955
+ const role = element.contentMetadata.contentRole;
3956
+ if (role === "heading" && element.contentMetadata.headingLevel) {
3957
+ return `heading-${element.contentMetadata.headingLevel}`;
3958
+ }
3959
+ return role;
3960
+ }
3334
3961
  const text = (element.state.textContent || element.label || "").toLowerCase();
3335
3962
  const type = element.type.toLowerCase();
3336
3963
  if (type === "button") {
@@ -3412,6 +4039,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3412
4039
  const probableTrigger = detectTrigger(appeared, disappeared, limitedModifications);
3413
4040
  const suggestedActions = finalConfig.generateSuggestions ? generateSuggestedActionsFromDiff(appeared, disappeared, limitedModifications, probableTrigger) : void 0;
3414
4041
  const pageChanges = detectPageChanges(fromSnapshot, toSnapshot);
4042
+ const contentChanges = detectContentChanges(fromElements, toElements);
3415
4043
  const summary = generateDiffSummary(
3416
4044
  appeared.map((e) => e.description),
3417
4045
  disappeared.map((e) => e.description),
@@ -3426,6 +4054,7 @@ function computeDiff(fromSnapshot, toSnapshot, config = {}) {
3426
4054
  disappeared,
3427
4055
  modified: limitedModifications
3428
4056
  },
4057
+ contentChanges: contentChanges || void 0,
3429
4058
  probableTrigger,
3430
4059
  suggestedActions,
3431
4060
  pageChanges,
@@ -3560,9 +4189,7 @@ function generateSuggestedActionsFromDiff(appeared, disappeared, modified, trigg
3560
4189
  suggestions.push("Fix the validation errors before submitting");
3561
4190
  }
3562
4191
  if (trigger === "Modal opened") {
3563
- const modal = appeared.find(
3564
- (e) => e.type === "dialog" || e.semanticType?.includes("dialog")
3565
- );
4192
+ const modal = appeared.find((e) => e.type === "dialog" || e.semanticType?.includes("dialog"));
3566
4193
  if (modal) {
3567
4194
  suggestions.push(`Interact with the "${modal.description}" dialog`);
3568
4195
  }
@@ -3625,13 +4252,1730 @@ var SemanticDiffManager = class {
3625
4252
  return this.lastSnapshot;
3626
4253
  }
3627
4254
  };
3628
-
3629
- // src/server/handlers.ts
3630
- function success(data) {
4255
+ var METRIC_CONTENT_TYPES = /* @__PURE__ */ new Set(["metric-value"]);
4256
+ var STATUS_CONTENT_TYPES = /* @__PURE__ */ new Set(["status-message", "badge"]);
4257
+ var HEADING_CONTENT_TYPES = /* @__PURE__ */ new Set(["heading"]);
4258
+ function isContentElement(element) {
4259
+ return element.category === "content" || element.contentMetadata !== void 0;
4260
+ }
4261
+ function getContentType(element) {
4262
+ if (element.contentMetadata?.contentRole) {
4263
+ return element.contentMetadata.contentRole;
4264
+ }
4265
+ return element.type;
4266
+ }
4267
+ function detectContentChanges(fromElements, toElements) {
4268
+ const textChanges = [];
4269
+ const metricChanges = [];
4270
+ const statusChanges = [];
4271
+ for (const [id, toElement] of toElements) {
4272
+ const fromElement = fromElements.get(id);
4273
+ if (fromElement) {
4274
+ if (isContentElement(toElement) || isContentElement(fromElement)) {
4275
+ const fromText = (fromElement.state.textContent || "").trim();
4276
+ const toText = (toElement.state.textContent || "").trim();
4277
+ if (fromText !== toText) {
4278
+ const contentType = getContentType(toElement);
4279
+ const label = toElement.description || toElement.accessibleName || id;
4280
+ if (METRIC_CONTENT_TYPES.has(contentType) || contentType === "metric") {
4281
+ const parsed = parseMetricChange(fromText, toText, id, label);
4282
+ if (parsed) {
4283
+ metricChanges.push(parsed);
4284
+ }
4285
+ } else if (STATUS_CONTENT_TYPES.has(contentType) || contentType === "status") {
4286
+ statusChanges.push({
4287
+ elementId: id,
4288
+ label,
4289
+ oldStatus: fromText,
4290
+ newStatus: toText,
4291
+ direction: classifyStatusDirection(fromText, toText)
4292
+ });
4293
+ } else {
4294
+ textChanges.push({
4295
+ elementId: id,
4296
+ contentType,
4297
+ oldText: fromText,
4298
+ newText: toText,
4299
+ changeType: "modified"
4300
+ });
4301
+ }
4302
+ }
4303
+ }
4304
+ } else {
4305
+ if (isContentElement(toElement)) {
4306
+ const toText = (toElement.state.textContent || "").trim();
4307
+ if (toText) {
4308
+ textChanges.push({
4309
+ elementId: id,
4310
+ contentType: getContentType(toElement),
4311
+ oldText: "",
4312
+ newText: toText,
4313
+ changeType: "added"
4314
+ });
4315
+ }
4316
+ }
4317
+ }
4318
+ }
4319
+ for (const [id, fromElement] of fromElements) {
4320
+ if (!toElements.has(id) && isContentElement(fromElement)) {
4321
+ const fromText = (fromElement.state.textContent || "").trim();
4322
+ if (fromText) {
4323
+ textChanges.push({
4324
+ elementId: id,
4325
+ contentType: getContentType(fromElement),
4326
+ oldText: fromText,
4327
+ newText: "",
4328
+ changeType: "removed"
4329
+ });
4330
+ }
4331
+ }
4332
+ }
4333
+ if (textChanges.length === 0 && metricChanges.length === 0 && statusChanges.length === 0) {
4334
+ return null;
4335
+ }
3631
4336
  return {
3632
- success: true,
3633
- data,
3634
- timestamp: Date.now()
4337
+ textChanges,
4338
+ metricChanges,
4339
+ statusChanges,
4340
+ summary: generateContentChangeSummary(textChanges, metricChanges, statusChanges)
4341
+ };
4342
+ }
4343
+ function parseNumericValue(text) {
4344
+ const trimmed = text.trim();
4345
+ if (!trimmed) return null;
4346
+ let working = trimmed;
4347
+ let negate = false;
4348
+ if (working.startsWith("(") && working.endsWith(")")) {
4349
+ working = working.slice(1, -1).trim();
4350
+ negate = true;
4351
+ }
4352
+ if (working.startsWith("-")) {
4353
+ negate = !negate;
4354
+ working = working.slice(1).trim();
4355
+ }
4356
+ if (working.startsWith("+")) {
4357
+ working = working.slice(1).trim();
4358
+ }
4359
+ working = working.replace(/^[£€¥₹$]/, "").trim();
4360
+ const isPercent = working.endsWith("%");
4361
+ if (isPercent) {
4362
+ working = working.slice(0, -1).trim();
4363
+ }
4364
+ working = working.replace(/\s*(ms|s|m|h|d|hrs?|mins?|secs?|days?)$/i, "").trim();
4365
+ working = working.replace(/,/g, "");
4366
+ const num = Number(working);
4367
+ if (isNaN(num) || !isFinite(num) || working === "") {
4368
+ return null;
4369
+ }
4370
+ return negate ? -num : num;
4371
+ }
4372
+ function parseMetricChange(fromText, toText, elementId, label) {
4373
+ const fromNum = parseNumericValue(fromText);
4374
+ const toNum = parseNumericValue(toText);
4375
+ let numericDelta;
4376
+ let percentChange;
4377
+ let significant = false;
4378
+ if (fromNum !== null && toNum !== null) {
4379
+ numericDelta = toNum - fromNum;
4380
+ if (fromNum !== 0) {
4381
+ percentChange = (toNum - fromNum) / Math.abs(fromNum) * 100;
4382
+ }
4383
+ if (percentChange !== void 0 && Math.abs(percentChange) > 10) {
4384
+ significant = true;
4385
+ }
4386
+ if (fromNum > 0 && toNum < 0) significant = true;
4387
+ if (fromNum < 0 && toNum > 0) significant = true;
4388
+ if (fromNum === 0 && toNum !== 0) significant = true;
4389
+ if (fromNum !== 0 && toNum === 0) significant = true;
4390
+ } else {
4391
+ significant = fromText !== toText;
4392
+ }
4393
+ return {
4394
+ elementId,
4395
+ label,
4396
+ oldValue: fromText,
4397
+ newValue: toText,
4398
+ numericDelta,
4399
+ percentChange: percentChange !== void 0 ? Math.round(percentChange * 100) / 100 : void 0,
4400
+ significant
4401
+ };
4402
+ }
4403
+ var STATUS_PROGRESSIONS = [
4404
+ [
4405
+ "failed",
4406
+ "error",
4407
+ "pending",
4408
+ "queued",
4409
+ "running",
4410
+ "in progress",
4411
+ "completed",
4412
+ "success",
4413
+ "done"
4414
+ ],
4415
+ ["disconnected", "connecting", "connected"],
4416
+ ["unhealthy", "degraded", "healthy"],
4417
+ ["offline", "online"],
4418
+ ["inactive", "active"],
4419
+ ["disabled", "enabled"],
4420
+ ["down", "up"],
4421
+ ["stopped", "starting", "started", "running"],
4422
+ ["closed", "open"],
4423
+ ["blocked", "unblocked"],
4424
+ ["rejected", "pending", "approved"],
4425
+ ["critical", "warning", "info", "ok"],
4426
+ ["red", "yellow", "green"]
4427
+ ];
4428
+ function classifyStatusDirection(oldStatus, newStatus) {
4429
+ const oldLower = oldStatus.toLowerCase().trim();
4430
+ const newLower = newStatus.toLowerCase().trim();
4431
+ for (const progression of STATUS_PROGRESSIONS) {
4432
+ let oldIndex = -1;
4433
+ let newIndex = -1;
4434
+ for (let i = 0; i < progression.length; i++) {
4435
+ if (oldLower.includes(progression[i])) oldIndex = i;
4436
+ if (newLower.includes(progression[i])) newIndex = i;
4437
+ }
4438
+ if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
4439
+ return newIndex > oldIndex ? "improved" : "degraded";
4440
+ }
4441
+ }
4442
+ return "neutral";
4443
+ }
4444
+ function generateContentChangeSummary(textChanges, metricChanges, statusChanges) {
4445
+ const parts = [];
4446
+ const modified = textChanges.filter((t) => t.changeType === "modified").length;
4447
+ const added = textChanges.filter((t) => t.changeType === "added").length;
4448
+ const removed = textChanges.filter((t) => t.changeType === "removed").length;
4449
+ const headingChanges = textChanges.filter(
4450
+ (t) => HEADING_CONTENT_TYPES.has(t.contentType) || t.contentType === "heading"
4451
+ );
4452
+ if (headingChanges.length > 0) {
4453
+ parts.push(`${headingChanges.length} heading${headingChanges.length > 1 ? "s" : ""} changed`);
4454
+ }
4455
+ if (metricChanges.length > 0) {
4456
+ const significantMetrics = metricChanges.filter((m) => m.significant);
4457
+ if (significantMetrics.length > 0) {
4458
+ parts.push(
4459
+ `${significantMetrics.length} metric${significantMetrics.length > 1 ? "s" : ""} changed significantly`
4460
+ );
4461
+ } else {
4462
+ parts.push(`${metricChanges.length} metric${metricChanges.length > 1 ? "s" : ""} changed`);
4463
+ }
4464
+ }
4465
+ if (statusChanges.length > 0) {
4466
+ const degraded = statusChanges.filter((s) => s.direction === "degraded");
4467
+ const improved = statusChanges.filter((s) => s.direction === "improved");
4468
+ if (degraded.length > 0) {
4469
+ parts.push(`${degraded.length} status${degraded.length > 1 ? "es" : ""} degraded`);
4470
+ }
4471
+ if (improved.length > 0) {
4472
+ parts.push(`${improved.length} status${improved.length > 1 ? "es" : ""} improved`);
4473
+ }
4474
+ const neutral = statusChanges.length - degraded.length - improved.length;
4475
+ if (neutral > 0 && degraded.length === 0 && improved.length === 0) {
4476
+ parts.push(`${neutral} status${neutral > 1 ? "es" : ""} changed`);
4477
+ }
4478
+ }
4479
+ const otherModified = modified - headingChanges.filter((h) => h.changeType === "modified").length;
4480
+ if (otherModified > 0) {
4481
+ parts.push(`${otherModified} text${otherModified > 1 ? " values" : " value"} modified`);
4482
+ }
4483
+ if (added > 0) {
4484
+ parts.push(`${added} content${added > 1 ? " elements" : " element"} added`);
4485
+ }
4486
+ if (removed > 0) {
4487
+ parts.push(`${removed} content${removed > 1 ? " elements" : " element"} removed`);
4488
+ }
4489
+ if (parts.length === 0) {
4490
+ return "No content changes";
4491
+ }
4492
+ return parts.join(", ");
4493
+ }
4494
+
4495
+ // src/ai/data-extraction.ts
4496
+ var DEFAULT_DATA_EXTRACTION_CONFIG = {
4497
+ minConfidence: 0.3,
4498
+ normalizeWhitespace: true
4499
+ };
4500
+ function classifyDataType(value) {
4501
+ const trimmed = value.trim();
4502
+ if (!trimmed) return { type: "unknown", confidence: 0 };
4503
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmed)) {
4504
+ return { type: "boolean", confidence: 0.95 };
4505
+ }
4506
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
4507
+ return { type: "email", confidence: 0.95 };
4508
+ }
4509
+ if (/^https?:\/\/\S+/.test(trimmed)) {
4510
+ return { type: "url", confidence: 0.95 };
4511
+ }
4512
+ if (/^[+]?[\d\s\-().]{7,20}$/.test(trimmed) && /\d{3,}/.test(trimmed)) {
4513
+ return { type: "phone", confidence: 0.7 };
4514
+ }
4515
+ if (/^[£$€¥₹][\s]?[\d,.]+$/.test(trimmed) || /^[\d,.]+[\s]?[£$€¥₹]$/.test(trimmed)) {
4516
+ return { type: "currency", confidence: 0.9 };
4517
+ }
4518
+ if (/^[\d,.]+\s?%$/.test(trimmed)) {
4519
+ return { type: "percentage", confidence: 0.95 };
4520
+ }
4521
+ 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)) {
4522
+ return { type: "date", confidence: 0.85 };
4523
+ }
4524
+ if (/^-?[\d,]+\.?\d*$/.test(trimmed) && trimmed !== "") {
4525
+ return { type: "number", confidence: 0.9 };
4526
+ }
4527
+ return { type: "text", confidence: 0.5 };
4528
+ }
4529
+ function normalizeValue(value, dataType) {
4530
+ const trimmed = value.trim();
4531
+ switch (dataType) {
4532
+ case "number":
4533
+ case "currency":
4534
+ case "percentage": {
4535
+ const numeric = trimmed.replace(/[^0-9.-]/g, "");
4536
+ const parsed = parseFloat(numeric);
4537
+ return isNaN(parsed) ? trimmed.toLowerCase() : parsed.toString();
4538
+ }
4539
+ case "date": {
4540
+ const d = new Date(trimmed);
4541
+ return isNaN(d.getTime()) ? trimmed.toLowerCase() : d.toISOString().split("T")[0];
4542
+ }
4543
+ case "boolean":
4544
+ return /^(true|yes|on)$/i.test(trimmed) ? "true" : "false";
4545
+ case "email":
4546
+ return trimmed.toLowerCase();
4547
+ case "url":
4548
+ return trimmed.replace(/\/+$/, "").toLowerCase();
4549
+ case "phone":
4550
+ return trimmed.replace(/[^\d+]/g, "");
4551
+ default:
4552
+ return trimmed.toLowerCase().replace(/\s+/g, " ");
4553
+ }
4554
+ }
4555
+ function extractElementValue(element) {
4556
+ const state = element.state;
4557
+ if (state?.value !== void 0 && state.value !== "") {
4558
+ return String(state.value);
4559
+ }
4560
+ if (state?.textContent !== void 0 && state.textContent !== "") {
4561
+ return String(state.textContent);
4562
+ }
4563
+ return "";
4564
+ }
4565
+ function extractLabel(element) {
4566
+ return element.accessibleName || element.labelText || element.label || element.description || element.id;
4567
+ }
4568
+ function extractPageData(elements, config = DEFAULT_DATA_EXTRACTION_CONFIG) {
4569
+ const values = {};
4570
+ let extractedCount = 0;
4571
+ for (const element of elements) {
4572
+ const rawValue = extractElementValue(element);
4573
+ if (!rawValue) continue;
4574
+ const label = extractLabel(element);
4575
+ const { type: dataType, confidence } = classifyDataType(rawValue);
4576
+ if (confidence < config.minConfidence) continue;
4577
+ const normalizedValue = normalizeValue(rawValue, dataType);
4578
+ values[label] = {
4579
+ elementId: element.id,
4580
+ label,
4581
+ rawValue: config.normalizeWhitespace ? rawValue.replace(/\s+/g, " ").trim() : rawValue,
4582
+ normalizedValue,
4583
+ dataType,
4584
+ confidence
4585
+ };
4586
+ extractedCount++;
4587
+ }
4588
+ return {
4589
+ values,
4590
+ scannedCount: elements.length,
4591
+ extractedCount
4592
+ };
4593
+ }
4594
+
4595
+ // src/ai/region-segmentation.ts
4596
+ var DEFAULT_REGION_SEGMENTATION_CONFIG = {
4597
+ minRegionElements: 1,
4598
+ headerFraction: 0.12,
4599
+ footerFraction: 0.9,
4600
+ sidebarFraction: 0.2
4601
+ };
4602
+ function toBounded(el) {
4603
+ const rect = el.state?.rect;
4604
+ if (!rect) return null;
4605
+ return {
4606
+ element: el,
4607
+ x: rect.x ?? 0,
4608
+ y: rect.y ?? 0,
4609
+ width: rect.width ?? 0,
4610
+ height: rect.height ?? 0
4611
+ };
4612
+ }
4613
+ function classifyRegionType(el, relativeY, relativeX, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4614
+ const role = (el.role || "").toLowerCase();
4615
+ const semanticType = (el.semanticType || "").toLowerCase();
4616
+ const tag = (el.tagName || "").toLowerCase();
4617
+ if (role === "navigation" || role === "nav" || tag === "nav") {
4618
+ return { type: "navigation", confidence: 0.95 };
4619
+ }
4620
+ if (role === "banner" || tag === "header") {
4621
+ return { type: "header", confidence: 0.95 };
4622
+ }
4623
+ if (role === "contentinfo" || tag === "footer") {
4624
+ return { type: "footer", confidence: 0.95 };
4625
+ }
4626
+ if (role === "main" || tag === "main") {
4627
+ return { type: "main-content", confidence: 0.95 };
4628
+ }
4629
+ if (role === "complementary" || tag === "aside") {
4630
+ return { type: "sidebar", confidence: 0.9 };
4631
+ }
4632
+ if (role === "form" || tag === "form") {
4633
+ return { type: "form", confidence: 0.9 };
4634
+ }
4635
+ if (role === "table" || tag === "table") {
4636
+ return { type: "table", confidence: 0.9 };
4637
+ }
4638
+ if (role === "dialog" || role === "alertdialog") {
4639
+ return { type: "modal", confidence: 0.95 };
4640
+ }
4641
+ if (role === "toolbar") {
4642
+ return { type: "toolbar", confidence: 0.9 };
4643
+ }
4644
+ if (semanticType.includes("card")) {
4645
+ return { type: "card", confidence: 0.8 };
4646
+ }
4647
+ if (relativeY < config.headerFraction) {
4648
+ return { type: "header", confidence: 0.6 };
4649
+ }
4650
+ if (relativeY > config.footerFraction) {
4651
+ return { type: "footer", confidence: 0.6 };
4652
+ }
4653
+ if (relativeX < config.sidebarFraction) {
4654
+ return { type: "sidebar", confidence: 0.5 };
4655
+ }
4656
+ return { type: "main-content", confidence: 0.3 };
4657
+ }
4658
+ function segmentPageRegions(elements, config = DEFAULT_REGION_SEGMENTATION_CONFIG) {
4659
+ const bounded = elements.map(toBounded).filter((b) => b !== null);
4660
+ if (bounded.length === 0) {
4661
+ return { regions: [], assignedCount: 0, unassignedIds: elements.map((e) => e.id) };
4662
+ }
4663
+ let maxX = 0;
4664
+ let maxY = 0;
4665
+ for (const b of bounded) {
4666
+ maxX = Math.max(maxX, b.x + b.width);
4667
+ maxY = Math.max(maxY, b.y + b.height);
4668
+ }
4669
+ if (maxX === 0) maxX = 1;
4670
+ if (maxY === 0) maxY = 1;
4671
+ const regionGroups = /* @__PURE__ */ new Map();
4672
+ const unassignedIds = [];
4673
+ for (const b of bounded) {
4674
+ const relativeX = b.x / maxX;
4675
+ const relativeY = b.y / maxY;
4676
+ const { type, confidence } = classifyRegionType(b.element, relativeY, relativeX, config);
4677
+ if (!regionGroups.has(type)) {
4678
+ regionGroups.set(type, { elements: [], confidences: [] });
4679
+ }
4680
+ regionGroups.get(type).elements.push(b);
4681
+ regionGroups.get(type).confidences.push(confidence);
4682
+ }
4683
+ const regions = [];
4684
+ let assignedCount = 0;
4685
+ for (const [type, group] of regionGroups) {
4686
+ if (group.elements.length < config.minRegionElements) {
4687
+ for (const b of group.elements) unassignedIds.push(b.element.id);
4688
+ continue;
4689
+ }
4690
+ let minX = Infinity, minY = Infinity, maxRX = 0, maxRY = 0;
4691
+ const elementIds = [];
4692
+ for (const b of group.elements) {
4693
+ minX = Math.min(minX, b.x);
4694
+ minY = Math.min(minY, b.y);
4695
+ maxRX = Math.max(maxRX, b.x + b.width);
4696
+ maxRY = Math.max(maxRY, b.y + b.height);
4697
+ elementIds.push(b.element.id);
4698
+ }
4699
+ const avgConfidence = group.confidences.reduce((a, b) => a + b, 0) / group.confidences.length;
4700
+ regions.push({
4701
+ type,
4702
+ bounds: { x: minX, y: minY, width: maxRX - minX, height: maxRY - minY },
4703
+ elementIds,
4704
+ label: type.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
4705
+ confidence: Math.round(avgConfidence * 100) / 100
4706
+ });
4707
+ assignedCount += elementIds.length;
4708
+ }
4709
+ return { regions, assignedCount, unassignedIds };
4710
+ }
4711
+
4712
+ // src/ai/table-extraction.ts
4713
+ var DEFAULT_TABLE_EXTRACTION_CONFIG = {
4714
+ minTableColumns: 2,
4715
+ minTableRows: 2,
4716
+ minListItems: 2,
4717
+ columnTolerance: 20,
4718
+ rowTolerance: 10
4719
+ };
4720
+ function getElementBounds(el) {
4721
+ const rect = el.state?.rect;
4722
+ if (!rect || rect.width === 0) return null;
4723
+ const text = el.state?.textContent ?? el.state?.value ?? "";
4724
+ if (!text) return null;
4725
+ return {
4726
+ element: el,
4727
+ x: rect.x ?? 0,
4728
+ y: rect.y ?? 0,
4729
+ width: rect.width ?? 0,
4730
+ height: rect.height ?? 0,
4731
+ text: text.trim()
4732
+ };
4733
+ }
4734
+ function clusterPositions(values, tolerance) {
4735
+ if (values.length === 0) return [];
4736
+ const sorted = [...values].sort((a, b) => a - b);
4737
+ const clusters = [sorted[0]];
4738
+ for (let i = 1; i < sorted.length; i++) {
4739
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
4740
+ clusters.push(sorted[i]);
4741
+ }
4742
+ }
4743
+ return clusters;
4744
+ }
4745
+ function assignToCluster(value, clusters, tolerance) {
4746
+ let best = 0;
4747
+ let bestDist = Math.abs(value - clusters[0]);
4748
+ for (let i = 1; i < clusters.length; i++) {
4749
+ const dist = Math.abs(value - clusters[i]);
4750
+ if (dist < bestDist) {
4751
+ bestDist = dist;
4752
+ best = i;
4753
+ }
4754
+ }
4755
+ return bestDist <= tolerance ? best : -1;
4756
+ }
4757
+ function detectTable(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4758
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4759
+ if (withBounds.length < config.minTableColumns * config.minTableRows) return null;
4760
+ const xPositions = withBounds.map((b) => b.x);
4761
+ const yPositions = withBounds.map((b) => b.y);
4762
+ const columnClusters = clusterPositions(xPositions, config.columnTolerance);
4763
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4764
+ if (columnClusters.length < config.minTableColumns || rowClusters.length < config.minTableRows) {
4765
+ return null;
4766
+ }
4767
+ const grid = Array.from(
4768
+ { length: rowClusters.length },
4769
+ () => Array(columnClusters.length).fill(null)
4770
+ );
4771
+ for (const b of withBounds) {
4772
+ const col = assignToCluster(b.x, columnClusters, config.columnTolerance);
4773
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4774
+ if (col >= 0 && row >= 0 && grid[row][col] === null) {
4775
+ grid[row][col] = b.text;
4776
+ }
4777
+ }
4778
+ const headers = grid[0].map((h) => h ?? "");
4779
+ const columns = headers.map((header, index) => {
4780
+ const bodyCells = grid.slice(1).map((r) => r[index]).filter((c) => c !== null);
4781
+ const types = bodyCells.map((c) => classifyDataType(c).type);
4782
+ const mostCommon = mode(types) ?? "text";
4783
+ return { header, index, dataType: mostCommon };
4784
+ });
4785
+ const rows = grid.slice(1).map((row) => row.map((cell) => cell ?? ""));
4786
+ return {
4787
+ label: headers[0] || "Table",
4788
+ columns,
4789
+ rows
4790
+ };
4791
+ }
4792
+ function detectList(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4793
+ const withBounds = elements.map(getElementBounds).filter((b) => b !== null);
4794
+ if (withBounds.length < config.minListItems) return null;
4795
+ const sorted = [...withBounds].sort((a, b) => a.y - b.y);
4796
+ const yPositions = sorted.map((b) => b.y);
4797
+ const rowClusters = clusterPositions(yPositions, config.rowTolerance);
4798
+ if (rowClusters.length < config.minListItems) return null;
4799
+ const rowGroups = /* @__PURE__ */ new Map();
4800
+ for (const b of sorted) {
4801
+ const row = assignToCluster(b.y, rowClusters, config.rowTolerance);
4802
+ if (row >= 0) {
4803
+ if (!rowGroups.has(row)) rowGroups.set(row, []);
4804
+ rowGroups.get(row).push(b);
4805
+ }
4806
+ }
4807
+ const items = [];
4808
+ const fieldLabels = [];
4809
+ let fieldLabelsInitialized = false;
4810
+ for (const [, rowElements] of [...rowGroups.entries()].sort(([a], [b]) => a - b)) {
4811
+ const sortedRow = [...rowElements].sort((a, b) => a.x - b.x);
4812
+ const item = {};
4813
+ for (let i = 0; i < sortedRow.length; i++) {
4814
+ const label = `field_${i}`;
4815
+ if (!fieldLabelsInitialized) fieldLabels.push(label);
4816
+ item[label] = sortedRow[i].text;
4817
+ }
4818
+ fieldLabelsInitialized = true;
4819
+ items.push(item);
4820
+ }
4821
+ if (items.length < config.minListItems) return null;
4822
+ const fields = fieldLabels.map((label) => {
4823
+ const values = items.map((item) => item[label]).filter(Boolean);
4824
+ const types = values.map((v) => classifyDataType(v).type);
4825
+ return { label, dataType: mode(types) ?? "text" };
4826
+ });
4827
+ return {
4828
+ label: "List",
4829
+ fields,
4830
+ items
4831
+ };
4832
+ }
4833
+ function extractStructuredData(elements, config = DEFAULT_TABLE_EXTRACTION_CONFIG) {
4834
+ const tables = [];
4835
+ const lists = [];
4836
+ const table = detectTable(elements, config);
4837
+ if (table) {
4838
+ tables.push(table);
4839
+ }
4840
+ const listCandidates = elements.filter((el) => {
4841
+ const role = el.role || el.type;
4842
+ return ["listitem", "row", "option", "link", "button"].includes(role);
4843
+ });
4844
+ if (listCandidates.length >= config.minListItems) {
4845
+ const list = detectList(listCandidates, config);
4846
+ if (list) {
4847
+ lists.push(list);
4848
+ }
4849
+ }
4850
+ return { tables, lists };
4851
+ }
4852
+ function mode(arr) {
4853
+ if (arr.length === 0) return void 0;
4854
+ const counts = /* @__PURE__ */ new Map();
4855
+ let best = arr[0];
4856
+ let bestCount = 0;
4857
+ for (const v of arr) {
4858
+ const c = (counts.get(v) ?? 0) + 1;
4859
+ counts.set(v, c);
4860
+ if (c > bestCount) {
4861
+ bestCount = c;
4862
+ best = v;
4863
+ }
4864
+ }
4865
+ return best;
4866
+ }
4867
+
4868
+ // src/ai/format-analysis.ts
4869
+ var DEFAULT_FORMAT_ANALYSIS_CONFIG = {
4870
+ lenientFormatting: true
4871
+ };
4872
+ function detectFormatPattern(value, dataType) {
4873
+ const trimmed = value.trim();
4874
+ switch (dataType) {
4875
+ case "currency": {
4876
+ const hasLeadingSymbol = /^[£$€¥₹]/.test(trimmed);
4877
+ const hasTrailingSymbol = /[£$€¥₹]$/.test(trimmed);
4878
+ const usesCommaThousands = /\d{1,3}(,\d{3})+/.test(trimmed);
4879
+ const usesPeriodThousands = /\d{1,3}(\.\d{3})+,/.test(trimmed);
4880
+ let pattern = hasLeadingSymbol ? "$" : "";
4881
+ if (usesCommaThousands) pattern += "#,###";
4882
+ else if (usesPeriodThousands) pattern += "#.###";
4883
+ else pattern += "#";
4884
+ if (/\.\d{2}$/.test(trimmed)) pattern += ".##";
4885
+ else if (/,\d{2}$/.test(trimmed)) pattern += ",##";
4886
+ if (hasTrailingSymbol) pattern += "$";
4887
+ return pattern;
4888
+ }
4889
+ case "date": {
4890
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return "YYYY-MM-DD";
4891
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(trimmed)) return "MM/DD/YYYY";
4892
+ if (/^\d{2}\.\d{2}\.\d{4}$/.test(trimmed)) return "DD.MM.YYYY";
4893
+ if (/^\d{1,2}\/\d{1,2}\/\d{2}$/.test(trimmed)) return "M/D/YY";
4894
+ if (/^\w{3,9}\s+\d{1,2},?\s+\d{4}$/.test(trimmed)) return "Month DD, YYYY";
4895
+ return "date";
4896
+ }
4897
+ case "percentage":
4898
+ return /\s%$/.test(trimmed) ? "#.## %" : "#.##%";
4899
+ case "number": {
4900
+ const hasCommas = /,/.test(trimmed);
4901
+ const decimalPlaces = trimmed.includes(".") ? trimmed.split(".")[1]?.length || 0 : 0;
4902
+ return (hasCommas ? "#,###" : "#") + (decimalPlaces > 0 ? "." + "#".repeat(decimalPlaces) : "");
4903
+ }
4904
+ case "phone": {
4905
+ if (/^\(\d{3}\)\s?\d{3}-\d{4}$/.test(trimmed)) return "(###) ###-####";
4906
+ if (/^\d{3}-\d{3}-\d{4}$/.test(trimmed)) return "###-###-####";
4907
+ if (/^\+\d/.test(trimmed)) return "+# ###...";
4908
+ return "phone";
4909
+ }
4910
+ default:
4911
+ return dataType;
4912
+ }
4913
+ }
4914
+ function analyzeFormat(elementId, label, rawValue) {
4915
+ const { type: dataType } = classifyDataType(rawValue);
4916
+ const pattern = detectFormatPattern(rawValue, dataType);
4917
+ return {
4918
+ elementId,
4919
+ label,
4920
+ dataType,
4921
+ pattern,
4922
+ example: rawValue.trim()
4923
+ };
4924
+ }
4925
+ function analyzePageFormats(elements) {
4926
+ const descriptors = [];
4927
+ for (const el of elements) {
4928
+ const rawValue = el.state?.value ?? el.state?.textContent ?? "";
4929
+ if (!rawValue) continue;
4930
+ const label = el.accessibleName || el.labelText || el.label || el.description || el.id;
4931
+ descriptors.push(analyzeFormat(el.id, label, rawValue));
4932
+ }
4933
+ return descriptors;
4934
+ }
4935
+ function compareFormats(sourceFormats, targetFormats, config = DEFAULT_FORMAT_ANALYSIS_CONFIG) {
4936
+ const mismatches = [];
4937
+ const targetByLabel = /* @__PURE__ */ new Map();
4938
+ for (const t of targetFormats) {
4939
+ targetByLabel.set(t.label.toLowerCase(), t);
4940
+ }
4941
+ for (const source of sourceFormats) {
4942
+ const target = targetByLabel.get(source.label.toLowerCase());
4943
+ if (!target) continue;
4944
+ if (source.dataType !== target.dataType) {
4945
+ mismatches.push({
4946
+ label: source.label,
4947
+ sourceFormat: source,
4948
+ targetFormat: target,
4949
+ severity: "error",
4950
+ description: `Data type mismatch: source is ${source.dataType}, target is ${target.dataType}`
4951
+ });
4952
+ continue;
4953
+ }
4954
+ if (source.pattern !== target.pattern) {
4955
+ const severity = config.lenientFormatting ? "warning" : "error";
4956
+ mismatches.push({
4957
+ label: source.label,
4958
+ sourceFormat: source,
4959
+ targetFormat: target,
4960
+ severity,
4961
+ description: `Format differs: source uses "${source.pattern}", target uses "${target.pattern}"`
4962
+ });
4963
+ }
4964
+ }
4965
+ return mismatches;
4966
+ }
4967
+
4968
+ // src/ai/cross-app-diff.ts
4969
+ var DEFAULT_CROSS_APP_DIFF_CONFIG = {
4970
+ matchThreshold: 0.5,
4971
+ accessibleNameWeight: 1,
4972
+ textWeight: 0.95,
4973
+ rolePositionWeight: 0.7
4974
+ };
4975
+ function getElementText(el) {
4976
+ return el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "";
4977
+ }
4978
+ function getRole(el) {
4979
+ return (el.role || el.type || "").toLowerCase();
4980
+ }
4981
+ function getCenter(el) {
4982
+ const rect = el.state?.rect;
4983
+ if (!rect) return null;
4984
+ return {
4985
+ x: rect.x + rect.width / 2,
4986
+ y: rect.y + rect.height / 2
4987
+ };
4988
+ }
4989
+ function computeMatchScore(source, target, config) {
4990
+ let bestScore = 0;
4991
+ let bestStrategy = "none";
4992
+ const srcName = (source.accessibleName || "").trim();
4993
+ const tgtName = (target.accessibleName || "").trim();
4994
+ if (srcName && tgtName && srcName.toLowerCase() === tgtName.toLowerCase()) {
4995
+ return { score: config.accessibleNameWeight, strategy: "accessible-name-exact" };
4996
+ }
4997
+ const srcText = getElementText(source);
4998
+ const tgtText = getElementText(target);
4999
+ if (srcText && tgtText && srcText.toLowerCase() === tgtText.toLowerCase()) {
5000
+ const score = config.textWeight;
5001
+ if (score > bestScore) {
5002
+ bestScore = score;
5003
+ bestStrategy = "text-exact";
5004
+ }
5005
+ }
5006
+ if (srcText && tgtText) {
5007
+ const srcNorm = normalizeString(srcText);
5008
+ const tgtNorm = normalizeString(tgtText);
5009
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
5010
+ const score = similarity * 0.85;
5011
+ if (score > bestScore) {
5012
+ bestScore = score;
5013
+ bestStrategy = "text-fuzzy";
5014
+ }
5015
+ }
5016
+ const srcRole = getRole(source);
5017
+ const tgtRole = getRole(target);
5018
+ if (srcRole && srcRole === tgtRole) {
5019
+ const srcCenter = getCenter(source);
5020
+ const tgtCenter = getCenter(target);
5021
+ if (srcCenter && tgtCenter) {
5022
+ const dx = Math.abs(srcCenter.x - tgtCenter.x) / 1920;
5023
+ const dy = Math.abs(srcCenter.y - tgtCenter.y) / 1080;
5024
+ const posSimilarity = 1 - Math.min(1, Math.sqrt(dx * dx + dy * dy));
5025
+ const score = config.rolePositionWeight * posSimilarity;
5026
+ if (score > bestScore) {
5027
+ bestScore = score;
5028
+ bestStrategy = "role-position";
5029
+ }
5030
+ }
5031
+ }
5032
+ const srcVal = source.state?.value ?? source.state?.textContent ?? "";
5033
+ const tgtVal = target.state?.value ?? target.state?.textContent ?? "";
5034
+ if (srcVal && tgtVal) {
5035
+ const srcType = classifyDataType(srcVal).type;
5036
+ const tgtType = classifyDataType(tgtVal).type;
5037
+ const srcNorm = normalizeValue(srcVal, srcType);
5038
+ const tgtNorm = normalizeValue(tgtVal, tgtType);
5039
+ if (srcNorm === tgtNorm && srcNorm !== "") {
5040
+ const score = 0.6;
5041
+ if (score > bestScore) {
5042
+ bestScore = score;
5043
+ bestStrategy = "data-overlap";
5044
+ }
5045
+ }
5046
+ }
5047
+ return { score: bestScore, strategy: bestStrategy };
5048
+ }
5049
+ function matchElements(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
5050
+ const candidates = [];
5051
+ for (let si = 0; si < sourceElements.length; si++) {
5052
+ for (let ti = 0; ti < targetElements.length; ti++) {
5053
+ const { score, strategy } = computeMatchScore(sourceElements[si], targetElements[ti], config);
5054
+ if (score >= config.matchThreshold) {
5055
+ candidates.push({ sourceIdx: si, targetIdx: ti, score, strategy });
5056
+ }
5057
+ }
5058
+ }
5059
+ candidates.sort((a, b) => b.score - a.score);
5060
+ const usedSource = /* @__PURE__ */ new Set();
5061
+ const usedTarget = /* @__PURE__ */ new Set();
5062
+ const pairs = [];
5063
+ for (const c of candidates) {
5064
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5065
+ usedSource.add(c.sourceIdx);
5066
+ usedTarget.add(c.targetIdx);
5067
+ const src = sourceElements[c.sourceIdx];
5068
+ const tgt = targetElements[c.targetIdx];
5069
+ pairs.push({
5070
+ sourceId: src.id,
5071
+ targetId: tgt.id,
5072
+ sourceLabel: getElementText(src) || src.id,
5073
+ targetLabel: getElementText(tgt) || tgt.id,
5074
+ confidence: Math.round(c.score * 100) / 100,
5075
+ matchStrategy: c.strategy
5076
+ });
5077
+ }
5078
+ return pairs;
5079
+ }
5080
+ function computeCrossAppDiff(sourceElements, targetElements, config = DEFAULT_CROSS_APP_DIFF_CONFIG) {
5081
+ const matchedPairs = matchElements(sourceElements, targetElements, config);
5082
+ const matchedSourceIds = new Set(matchedPairs.map((p) => p.sourceId));
5083
+ const matchedTargetIds = new Set(matchedPairs.map((p) => p.targetId));
5084
+ const unmatchedSourceIds = sourceElements.filter((e) => !matchedSourceIds.has(e.id)).map((e) => e.id);
5085
+ const unmatchedTargetIds = targetElements.filter((e) => !matchedTargetIds.has(e.id)).map((e) => e.id);
5086
+ const sourceData = extractPageData(sourceElements);
5087
+ const targetData = extractPageData(targetElements);
5088
+ const dataComparisons = [];
5089
+ for (const pair of matchedPairs) {
5090
+ const srcEntry = Object.values(sourceData.values).find((v) => v.elementId === pair.sourceId);
5091
+ const tgtEntry = Object.values(targetData.values).find((v) => v.elementId === pair.targetId);
5092
+ if (srcEntry && tgtEntry) {
5093
+ dataComparisons.push({
5094
+ label: pair.sourceLabel,
5095
+ sourceValue: srcEntry.rawValue,
5096
+ targetValue: tgtEntry.rawValue,
5097
+ valuesMatch: srcEntry.normalizedValue === tgtEntry.normalizedValue,
5098
+ formatsMatch: srcEntry.dataType === tgtEntry.dataType
5099
+ });
5100
+ }
5101
+ }
5102
+ const sourceFormats = analyzePageFormats(sourceElements);
5103
+ const targetFormats = analyzePageFormats(targetElements);
5104
+ const formatMismatches = compareFormats(sourceFormats, targetFormats);
5105
+ return {
5106
+ matchedPairs,
5107
+ unmatchedSourceIds,
5108
+ unmatchedTargetIds,
5109
+ dataComparisons,
5110
+ formatMismatches
5111
+ };
5112
+ }
5113
+
5114
+ // src/ai/action-parity.ts
5115
+ var DEFAULT_ACTION_PARITY_CONFIG = {
5116
+ ignoreActions: []
5117
+ };
5118
+ function getActions(el, ignoreActions) {
5119
+ const actions = el.actions || el.suggestedActions || [];
5120
+ const ignoreSet = new Set(ignoreActions.map((a) => a.toLowerCase()));
5121
+ return actions.map(
5122
+ (a) => typeof a === "string" ? a : a.action || a.name || ""
5123
+ ).filter((a) => a && !ignoreSet.has(a.toLowerCase()));
5124
+ }
5125
+ function analyzeActionParity(matchedPairs, sourceElements, targetElements, config = DEFAULT_ACTION_PARITY_CONFIG) {
5126
+ const sourceById = new Map(sourceElements.map((e) => [e.id, e]));
5127
+ const targetById = new Map(targetElements.map((e) => [e.id, e]));
5128
+ const results = [];
5129
+ for (const pair of matchedPairs) {
5130
+ const src = sourceById.get(pair.sourceId);
5131
+ const tgt = targetById.get(pair.targetId);
5132
+ if (!src || !tgt) continue;
5133
+ const sourceActions = getActions(src, config.ignoreActions);
5134
+ const targetActions = getActions(tgt, config.ignoreActions);
5135
+ const sourceSet = new Set(sourceActions.map((a) => a.toLowerCase()));
5136
+ const targetSet = new Set(targetActions.map((a) => a.toLowerCase()));
5137
+ const missingInTarget = sourceActions.filter((a) => !targetSet.has(a.toLowerCase()));
5138
+ const missingInSource = targetActions.filter((a) => !sourceSet.has(a.toLowerCase()));
5139
+ results.push({
5140
+ pair,
5141
+ sourceActions,
5142
+ targetActions,
5143
+ missingInTarget,
5144
+ missingInSource
5145
+ });
5146
+ }
5147
+ return results;
5148
+ }
5149
+
5150
+ // src/ai/navigation-map.ts
5151
+ var DEFAULT_NAVIGATION_MAP_CONFIG = {
5152
+ labelMatchThreshold: 0.8
5153
+ };
5154
+ function isNavigationElement(el) {
5155
+ const role = (el.role || "").toLowerCase();
5156
+ const type = (el.type || "").toLowerCase();
5157
+ const semanticType = (el.semanticType || "").toLowerCase();
5158
+ if (["link", "menuitem", "tab"].includes(role)) return true;
5159
+ if (["link", "menuitem"].includes(type)) return true;
5160
+ if (semanticType.includes("nav") || semanticType.includes("menu") || semanticType.includes("tab")) {
5161
+ return true;
5162
+ }
5163
+ const context = (el.parentContext || "").toLowerCase();
5164
+ if (context.includes("nav") || context.includes("menu") || context.includes("sidebar")) {
5165
+ if (role === "button" || type === "button" || role === "link" || type === "link") {
5166
+ return true;
5167
+ }
5168
+ }
5169
+ return false;
5170
+ }
5171
+ function getNavLabel(el) {
5172
+ return el.accessibleName || el.labelText || el.label || el.description || el.id;
5173
+ }
5174
+ function getHref(el) {
5175
+ const state = el.state;
5176
+ return state?.href || void 0;
5177
+ }
5178
+ function hrefsMatch(a, b) {
5179
+ if (!a || !b) return false;
5180
+ const normalize = (h) => h.replace(/^https?:\/\//, "").replace(/localhost:\d+/, "").replace(/\/+$/, "").toLowerCase();
5181
+ return normalize(a) === normalize(b);
5182
+ }
5183
+ function buildNavigationMap(sourceElements, targetElements, config = DEFAULT_NAVIGATION_MAP_CONFIG) {
5184
+ const sourceNav = sourceElements.filter(isNavigationElement);
5185
+ const targetNav = targetElements.filter(isNavigationElement);
5186
+ const pairs = [];
5187
+ const matchedTargetIds = /* @__PURE__ */ new Set();
5188
+ for (const src of sourceNav) {
5189
+ const srcLabel = getNavLabel(src);
5190
+ const srcNorm = normalizeString(srcLabel);
5191
+ let bestTarget = null;
5192
+ let bestScore = 0;
5193
+ for (const tgt of targetNav) {
5194
+ if (matchedTargetIds.has(tgt.id)) continue;
5195
+ const tgtLabel = getNavLabel(tgt);
5196
+ const tgtNorm = normalizeString(tgtLabel);
5197
+ if (srcNorm === tgtNorm) {
5198
+ bestTarget = tgt;
5199
+ bestScore = 1;
5200
+ break;
5201
+ }
5202
+ const similarity = jaroWinklerSimilarity(srcNorm, tgtNorm);
5203
+ if (similarity > bestScore && similarity >= config.labelMatchThreshold) {
5204
+ bestScore = similarity;
5205
+ bestTarget = tgt;
5206
+ }
5207
+ }
5208
+ if (bestTarget) {
5209
+ matchedTargetIds.add(bestTarget.id);
5210
+ const srcHref = getHref(src);
5211
+ const tgtHref = getHref(bestTarget);
5212
+ pairs.push({
5213
+ sourceId: src.id,
5214
+ targetId: bestTarget.id,
5215
+ label: srcLabel,
5216
+ sourceHref: srcHref,
5217
+ targetHref: tgtHref,
5218
+ destinationMatch: hrefsMatch(srcHref, tgtHref)
5219
+ });
5220
+ }
5221
+ }
5222
+ const sourceOnly = sourceNav.filter((s) => !pairs.some((p) => p.sourceId === s.id)).map((s) => s.id);
5223
+ const targetOnly = targetNav.filter((t) => !matchedTargetIds.has(t.id)).map((t) => t.id);
5224
+ return { pairs, sourceOnly, targetOnly };
5225
+ }
5226
+
5227
+ // src/ai/component-comparison.ts
5228
+ var DEFAULT_COMPONENT_COMPARISON_CONFIG = {
5229
+ nameMatchThreshold: 0.75
5230
+ };
5231
+ function computeComponentMatchScore(source, target) {
5232
+ if (source.name.toLowerCase() === target.name.toLowerCase()) return 1;
5233
+ let score = 0;
5234
+ if (source.type === target.type) {
5235
+ score += 0.3;
5236
+ }
5237
+ const nameSimilarity = jaroWinklerSimilarity(
5238
+ normalizeString(source.name),
5239
+ normalizeString(target.name)
5240
+ );
5241
+ score += nameSimilarity * 0.7;
5242
+ return score;
5243
+ }
5244
+ function compareComponents(sourceComponents, targetComponents, config = DEFAULT_COMPONENT_COMPARISON_CONFIG) {
5245
+ const candidates = [];
5246
+ for (let si = 0; si < sourceComponents.length; si++) {
5247
+ for (let ti = 0; ti < targetComponents.length; ti++) {
5248
+ const score = computeComponentMatchScore(sourceComponents[si], targetComponents[ti]);
5249
+ if (score >= config.nameMatchThreshold) {
5250
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5251
+ }
5252
+ }
5253
+ }
5254
+ candidates.sort((a, b) => b.score - a.score);
5255
+ const usedSource = /* @__PURE__ */ new Set();
5256
+ const usedTarget = /* @__PURE__ */ new Set();
5257
+ const matches = [];
5258
+ for (const c of candidates) {
5259
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5260
+ usedSource.add(c.sourceIdx);
5261
+ usedTarget.add(c.targetIdx);
5262
+ const src = sourceComponents[c.sourceIdx];
5263
+ const tgt = targetComponents[c.targetIdx];
5264
+ const srcKeys = new Set(src.stateKeys);
5265
+ const tgtKeys = new Set(tgt.stateKeys);
5266
+ const missingKeys = src.stateKeys.filter((k) => !tgtKeys.has(k));
5267
+ const extraKeys = tgt.stateKeys.filter((k) => !srcKeys.has(k));
5268
+ const srcActions = new Set(src.actions.map((a) => a.toLowerCase()));
5269
+ const tgtActions = new Set(tgt.actions.map((a) => a.toLowerCase()));
5270
+ const missingActions = src.actions.filter((a) => !tgtActions.has(a.toLowerCase()));
5271
+ const extraActions = tgt.actions.filter((a) => !srcActions.has(a.toLowerCase()));
5272
+ matches.push({
5273
+ source: src,
5274
+ target: tgt,
5275
+ confidence: Math.round(c.score * 100) / 100,
5276
+ stateKeyDiff: { missing: missingKeys, extra: extraKeys },
5277
+ actionDiff: { missing: missingActions, extra: extraActions }
5278
+ });
5279
+ }
5280
+ const sourceOnly = sourceComponents.filter((_, i) => !usedSource.has(i));
5281
+ const targetOnly = targetComponents.filter((_, i) => !usedTarget.has(i));
5282
+ return { matches, sourceOnly, targetOnly };
5283
+ }
5284
+
5285
+ // src/ai/layout-comparison.ts
5286
+ var DEFAULT_LAYOUT_COMPARISON_CONFIG = {
5287
+ gridTolerance: 20
5288
+ };
5289
+ function getRect(el) {
5290
+ const rect = el.state?.rect;
5291
+ if (!rect || !rect.width) return null;
5292
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
5293
+ }
5294
+ function clusterValues(values, tolerance) {
5295
+ if (values.length === 0) return [];
5296
+ const sorted = [...values].sort((a, b) => a - b);
5297
+ const clusters = [sorted[0]];
5298
+ for (let i = 1; i < sorted.length; i++) {
5299
+ if (sorted[i] - clusters[clusters.length - 1] > tolerance) {
5300
+ clusters.push(sorted[i]);
5301
+ }
5302
+ }
5303
+ return clusters;
5304
+ }
5305
+ function detectGridStructure(elements, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5306
+ const rects = elements.map(getRect).filter((r) => r !== null);
5307
+ const xPositions = rects.map((r) => r.x);
5308
+ const yPositions = rects.map((r) => r.y);
5309
+ const columns = clusterValues(xPositions, config.gridTolerance);
5310
+ const rows = clusterValues(yPositions, config.gridTolerance);
5311
+ return {
5312
+ columns,
5313
+ rows,
5314
+ columnCount: columns.length,
5315
+ rowCount: rows.length
5316
+ };
5317
+ }
5318
+ function computeMaxDepth(elements) {
5319
+ let maxDepth = 0;
5320
+ for (const el of elements) {
5321
+ const context = el.parentContext || "";
5322
+ const depth = context ? context.split(">").length : 1;
5323
+ maxDepth = Math.max(maxDepth, depth);
5324
+ }
5325
+ return maxDepth;
5326
+ }
5327
+ function compareLayouts(sourceElements, targetElements, sourceRegions, targetRegions, config = DEFAULT_LAYOUT_COMPARISON_CONFIG) {
5328
+ const sourceGrid = detectGridStructure(sourceElements, config);
5329
+ const targetGrid = detectGridStructure(targetElements, config);
5330
+ const gridDiff = {
5331
+ sourceGrid,
5332
+ targetGrid,
5333
+ columnDiff: sourceGrid.columnCount - targetGrid.columnCount,
5334
+ rowDiff: sourceGrid.rowCount - targetGrid.rowCount
5335
+ };
5336
+ const sourceDepth = computeMaxDepth(sourceElements);
5337
+ const targetDepth = computeMaxDepth(targetElements);
5338
+ const hierarchyDiff = {
5339
+ sourceDepth,
5340
+ targetDepth,
5341
+ depthDiff: sourceDepth - targetDepth
5342
+ };
5343
+ const sourceRegionCount = sourceRegions?.regions.length || 1;
5344
+ const targetRegionCount = targetRegions?.regions.length || 1;
5345
+ const sourceDensity = sourceElements.length / sourceRegionCount;
5346
+ const targetDensity = targetElements.length / targetRegionCount;
5347
+ const density = {
5348
+ sourceDensity: Math.round(sourceDensity * 100) / 100,
5349
+ targetDensity: Math.round(targetDensity * 100) / 100,
5350
+ ratio: targetDensity > 0 ? Math.round(sourceDensity / targetDensity * 100) / 100 : 0
5351
+ };
5352
+ const gridSimilarity = sourceGrid.columnCount === 0 && targetGrid.columnCount === 0 ? 1 : 1 - Math.abs(gridDiff.columnDiff) / Math.max(sourceGrid.columnCount, targetGrid.columnCount, 1);
5353
+ const hierarchySimilarity = sourceDepth === 0 && targetDepth === 0 ? 1 : 1 - Math.abs(hierarchyDiff.depthDiff) / Math.max(sourceDepth, targetDepth, 1);
5354
+ const densitySimilarity = density.ratio > 0 ? Math.min(density.ratio, 1 / density.ratio) : 0;
5355
+ const similarity = Math.round((gridSimilarity * 0.4 + hierarchySimilarity * 0.3 + densitySimilarity * 0.3) * 100) / 100;
5356
+ return {
5357
+ gridDiff,
5358
+ hierarchyDiff,
5359
+ density,
5360
+ similarity
5361
+ };
5362
+ }
5363
+
5364
+ // src/ai/content-comparison.ts
5365
+ var DEFAULT_CONTENT_COMPARISON_CONFIG = {
5366
+ labelMatchThreshold: 0.8,
5367
+ headingMatchThreshold: 0.75,
5368
+ maxCellDifferences: 50
5369
+ };
5370
+ function getElementText2(el) {
5371
+ return (el.accessibleName || el.labelText || el.label || el.state?.textContent || el.description || "").trim();
5372
+ }
5373
+ function getContentRole(el) {
5374
+ if (el.contentMetadata?.contentRole) {
5375
+ return el.contentMetadata.contentRole;
5376
+ }
5377
+ const t = (el.type || "").toLowerCase();
5378
+ if (t === "heading" || t.startsWith("h") && /^h[1-6]$/.test(t)) return "heading";
5379
+ if (t === "metric-value" || t === "metric") return "metric";
5380
+ if (t === "status-message" || t === "status") return "status";
5381
+ if (t === "label") return "label";
5382
+ if (t === "badge") return "badge";
5383
+ if (t === "table-cell") return "table-cell";
5384
+ if (t === "table-header") return "table-header";
5385
+ if (t === "caption") return "caption";
5386
+ return null;
5387
+ }
5388
+ function getHeadingLevel(el) {
5389
+ if (el.contentMetadata?.headingLevel) {
5390
+ return el.contentMetadata.headingLevel;
5391
+ }
5392
+ const tag = (el.tagName || el.type || "").toLowerCase();
5393
+ const match = /^h([1-6])$/.exec(tag);
5394
+ if (match) return parseInt(match[1], 10);
5395
+ return void 0;
5396
+ }
5397
+ function isContentElement2(el) {
5398
+ if (el.category === "content") return true;
5399
+ if (el.contentMetadata) return true;
5400
+ const role = getContentRole(el);
5401
+ return role !== null;
5402
+ }
5403
+ function normalizeText(text) {
5404
+ return normalizeString(text, { caseSensitive: false, ignoreWhitespace: true });
5405
+ }
5406
+ function parseMetricText(el) {
5407
+ const text = getElementText2(el);
5408
+ const colonMatch = text.match(/^(.+?):\s*(.+)$/);
5409
+ if (colonMatch) {
5410
+ return { label: colonMatch[1].trim(), value: colonMatch[2].trim() };
5411
+ }
5412
+ const dashMatch = text.match(/^(.+?)\s*[-]\s*(.+)$/);
5413
+ if (dashMatch) {
5414
+ return { label: dashMatch[1].trim(), value: dashMatch[2].trim() };
5415
+ }
5416
+ const elLabel = el.accessibleName || el.labelText || el.label || el.id;
5417
+ return { label: elLabel, value: text };
5418
+ }
5419
+ function filterHeadings(elements) {
5420
+ return elements.filter((el) => getContentRole(el) === "heading");
5421
+ }
5422
+ function filterMetrics(elements) {
5423
+ return elements.filter((el) => getContentRole(el) === "metric");
5424
+ }
5425
+ function filterStatuses(elements) {
5426
+ return elements.filter((el) => {
5427
+ const role = getContentRole(el);
5428
+ return role === "status" || role === "badge";
5429
+ });
5430
+ }
5431
+ function filterLabels(elements) {
5432
+ return elements.filter((el) => {
5433
+ const role = getContentRole(el);
5434
+ return role === "label" || role === "caption";
5435
+ });
5436
+ }
5437
+ function matchTexts(sourceTexts, targetTexts, threshold) {
5438
+ const candidates = [];
5439
+ for (let si = 0; si < sourceTexts.length; si++) {
5440
+ const sNorm = normalizeText(sourceTexts[si]);
5441
+ if (!sNorm) continue;
5442
+ for (let ti = 0; ti < targetTexts.length; ti++) {
5443
+ const tNorm = normalizeText(targetTexts[ti]);
5444
+ if (!tNorm) continue;
5445
+ const score = sNorm === tNorm ? 1 : jaroWinklerSimilarity(sNorm, tNorm);
5446
+ if (score >= threshold) {
5447
+ candidates.push({ sourceIdx: si, targetIdx: ti, score });
5448
+ }
5449
+ }
5450
+ }
5451
+ candidates.sort((a, b) => b.score - a.score);
5452
+ const usedSource = /* @__PURE__ */ new Set();
5453
+ const usedTarget = /* @__PURE__ */ new Set();
5454
+ const matched = [];
5455
+ for (const c of candidates) {
5456
+ if (usedSource.has(c.sourceIdx) || usedTarget.has(c.targetIdx)) continue;
5457
+ usedSource.add(c.sourceIdx);
5458
+ usedTarget.add(c.targetIdx);
5459
+ matched.push(c);
5460
+ }
5461
+ const unmatchedSource = sourceTexts.map((_, i) => i).filter((i) => !usedSource.has(i));
5462
+ const unmatchedTarget = targetTexts.map((_, i) => i).filter((i) => !usedTarget.has(i));
5463
+ return { matched, unmatchedSource, unmatchedTarget };
5464
+ }
5465
+ function compareHeadings(sourceElements, targetElements, config) {
5466
+ const srcHeadings = filterHeadings(sourceElements);
5467
+ const tgtHeadings = filterHeadings(targetElements);
5468
+ const srcTexts = srcHeadings.map(getElementText2);
5469
+ const tgtTexts = tgtHeadings.map(getElementText2);
5470
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5471
+ srcTexts,
5472
+ tgtTexts,
5473
+ config.headingMatchThreshold
5474
+ );
5475
+ const headingMatched = [];
5476
+ const headingChanged = [];
5477
+ for (const m of matched) {
5478
+ const srcText = srcTexts[m.sourceIdx];
5479
+ const tgtText = tgtTexts[m.targetIdx];
5480
+ const srcLevel = getHeadingLevel(srcHeadings[m.sourceIdx]);
5481
+ const tgtLevel = getHeadingLevel(tgtHeadings[m.targetIdx]);
5482
+ if (normalizeText(srcText) === normalizeText(tgtText)) {
5483
+ headingMatched.push({
5484
+ source: srcText,
5485
+ target: tgtText,
5486
+ level: srcLevel
5487
+ });
5488
+ } else {
5489
+ headingChanged.push({
5490
+ source: srcText,
5491
+ target: tgtText,
5492
+ level: srcLevel ?? tgtLevel
5493
+ });
5494
+ }
5495
+ }
5496
+ return {
5497
+ matched: headingMatched,
5498
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5499
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i]),
5500
+ changed: headingChanged
5501
+ };
5502
+ }
5503
+ function compareMetrics(sourceElements, targetElements, config) {
5504
+ const srcMetrics = filterMetrics(sourceElements);
5505
+ const tgtMetrics = filterMetrics(targetElements);
5506
+ const srcParsed = srcMetrics.map(parseMetricText);
5507
+ const tgtParsed = tgtMetrics.map(parseMetricText);
5508
+ const srcLabels = srcParsed.map((p) => p.label);
5509
+ const tgtLabels = tgtParsed.map((p) => p.label);
5510
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5511
+ srcLabels,
5512
+ tgtLabels,
5513
+ config.labelMatchThreshold
5514
+ );
5515
+ const metricMatched = [];
5516
+ const metricChanged = [];
5517
+ for (const m of matched) {
5518
+ const src = srcParsed[m.sourceIdx];
5519
+ const tgt = tgtParsed[m.targetIdx];
5520
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5521
+ metricMatched.push({
5522
+ label: src.label,
5523
+ sourceValue: src.value,
5524
+ targetValue: tgt.value
5525
+ });
5526
+ } else {
5527
+ metricChanged.push({
5528
+ label: src.label,
5529
+ sourceValue: src.value,
5530
+ targetValue: tgt.value
5531
+ });
5532
+ }
5533
+ }
5534
+ return {
5535
+ matched: metricMatched,
5536
+ changed: metricChanged,
5537
+ sourceOnly: unmatchedSource.map((i) => srcParsed[i].label),
5538
+ targetOnly: unmatchedTarget.map((i) => tgtParsed[i].label)
5539
+ };
5540
+ }
5541
+ function compareStatuses(sourceElements, targetElements, config) {
5542
+ const srcStatuses = filterStatuses(sourceElements);
5543
+ const tgtStatuses = filterStatuses(targetElements);
5544
+ const srcParsed = srcStatuses.map(parseMetricText);
5545
+ const tgtParsed = tgtStatuses.map(parseMetricText);
5546
+ const srcLabels = srcParsed.map((p) => p.label);
5547
+ const tgtLabels = tgtParsed.map((p) => p.label);
5548
+ const { matched } = matchTexts(srcLabels, tgtLabels, config.labelMatchThreshold);
5549
+ const statusMatched = [];
5550
+ const statusChanged = [];
5551
+ for (const m of matched) {
5552
+ const src = srcParsed[m.sourceIdx];
5553
+ const tgt = tgtParsed[m.targetIdx];
5554
+ if (normalizeText(src.value) === normalizeText(tgt.value)) {
5555
+ statusMatched.push({
5556
+ label: src.label,
5557
+ sourceStatus: src.value,
5558
+ targetStatus: tgt.value
5559
+ });
5560
+ } else {
5561
+ statusChanged.push({
5562
+ label: src.label,
5563
+ sourceStatus: src.value,
5564
+ targetStatus: tgt.value
5565
+ });
5566
+ }
5567
+ }
5568
+ return {
5569
+ matched: statusMatched,
5570
+ changed: statusChanged
5571
+ };
5572
+ }
5573
+ function compareLabels(sourceElements, targetElements, config) {
5574
+ const srcLabels = filterLabels(sourceElements);
5575
+ const tgtLabels = filterLabels(targetElements);
5576
+ const srcTexts = srcLabels.map(getElementText2);
5577
+ const tgtTexts = tgtLabels.map(getElementText2);
5578
+ const { matched, unmatchedSource, unmatchedTarget } = matchTexts(
5579
+ srcTexts,
5580
+ tgtTexts,
5581
+ config.labelMatchThreshold
5582
+ );
5583
+ return {
5584
+ matched: matched.map((m) => srcTexts[m.sourceIdx]),
5585
+ sourceOnly: unmatchedSource.map((i) => srcTexts[i]),
5586
+ targetOnly: unmatchedTarget.map((i) => tgtTexts[i])
5587
+ };
5588
+ }
5589
+ function compareTables(sourceElements, targetElements, config) {
5590
+ const srcData = extractStructuredData(sourceElements);
5591
+ const tgtData = extractStructuredData(targetElements);
5592
+ const srcTables = srcData.tables;
5593
+ const tgtTables = tgtData.tables;
5594
+ if (srcTables.length === 0 || tgtTables.length === 0) {
5595
+ return [];
5596
+ }
5597
+ const srcTableLabels = srcTables.map((t) => t.label || "");
5598
+ const tgtTableLabels = tgtTables.map((t) => t.label || "");
5599
+ const { matched } = matchTexts(srcTableLabels, tgtTableLabels, config.labelMatchThreshold);
5600
+ const tablePairs = [];
5601
+ if (matched.length > 0) {
5602
+ for (const m of matched) {
5603
+ tablePairs.push({ srcIdx: m.sourceIdx, tgtIdx: m.targetIdx });
5604
+ }
5605
+ } else if (srcTables.length === 1 && tgtTables.length === 1) {
5606
+ tablePairs.push({ srcIdx: 0, tgtIdx: 0 });
5607
+ }
5608
+ const comparisons = [];
5609
+ for (const pair of tablePairs) {
5610
+ const srcTable = srcTables[pair.srcIdx];
5611
+ const tgtTable = tgtTables[pair.tgtIdx];
5612
+ const srcHeaders = srcTable.columns.map((c) => c.header);
5613
+ const tgtHeaders = tgtTable.columns.map((c) => c.header);
5614
+ const srcHeaderSet = new Set(srcHeaders.map(normalizeText));
5615
+ const tgtHeaderSet = new Set(tgtHeaders.map(normalizeText));
5616
+ const sourceOnlyColumns = srcHeaders.filter((h) => !tgtHeaderSet.has(normalizeText(h)));
5617
+ const targetOnlyColumns = tgtHeaders.filter((h) => !srcHeaderSet.has(normalizeText(h)));
5618
+ const columnsMatch = sourceOnlyColumns.length === 0 && targetOnlyColumns.length === 0;
5619
+ const cellDifferences = [];
5620
+ const commonHeaders = srcHeaders.filter((h) => tgtHeaderSet.has(normalizeText(h)));
5621
+ const minRows = Math.min(srcTable.rows.length, tgtTable.rows.length);
5622
+ for (let row = 0; row < minRows; row++) {
5623
+ if (cellDifferences.length >= config.maxCellDifferences) break;
5624
+ for (const header of commonHeaders) {
5625
+ const srcColIdx = srcHeaders.indexOf(header);
5626
+ const tgtColIdx = tgtHeaders.findIndex((h) => normalizeText(h) === normalizeText(header));
5627
+ if (srcColIdx < 0 || tgtColIdx < 0) continue;
5628
+ const srcValue = srcTable.rows[row]?.[srcColIdx] ?? "";
5629
+ const tgtValue = tgtTable.rows[row]?.[tgtColIdx] ?? "";
5630
+ if (normalizeText(srcValue) !== normalizeText(tgtValue)) {
5631
+ cellDifferences.push({
5632
+ row,
5633
+ column: header,
5634
+ sourceValue: srcValue,
5635
+ targetValue: tgtValue
5636
+ });
5637
+ }
5638
+ }
5639
+ }
5640
+ comparisons.push({
5641
+ sourceLabel: srcTable.label,
5642
+ targetLabel: tgtTable.label,
5643
+ columnsMatch,
5644
+ sourceOnlyColumns,
5645
+ targetOnlyColumns,
5646
+ sourceRowCount: srcTable.rows.length,
5647
+ targetRowCount: tgtTable.rows.length,
5648
+ cellDifferences
5649
+ });
5650
+ }
5651
+ return comparisons;
5652
+ }
5653
+ function compareHeadingHierarchy(sourceElements, targetElements) {
5654
+ const srcHeadings = filterHeadings(sourceElements);
5655
+ const tgtHeadings = filterHeadings(targetElements);
5656
+ const srcByLevel = /* @__PURE__ */ new Map();
5657
+ const tgtByLevel = /* @__PURE__ */ new Map();
5658
+ for (const el of srcHeadings) {
5659
+ const level = getHeadingLevel(el) ?? 0;
5660
+ srcByLevel.set(level, (srcByLevel.get(level) ?? 0) + 1);
5661
+ }
5662
+ for (const el of tgtHeadings) {
5663
+ const level = getHeadingLevel(el) ?? 0;
5664
+ tgtByLevel.set(level, (tgtByLevel.get(level) ?? 0) + 1);
5665
+ }
5666
+ const allLevels = /* @__PURE__ */ new Set([...srcByLevel.keys(), ...tgtByLevel.keys()]);
5667
+ const result = [];
5668
+ for (const level of [...allLevels].sort()) {
5669
+ result.push({
5670
+ level,
5671
+ sourceCount: srcByLevel.get(level) ?? 0,
5672
+ targetCount: tgtByLevel.get(level) ?? 0
5673
+ });
5674
+ }
5675
+ return result;
5676
+ }
5677
+ function compareContent(sourceElements, targetElements, config = DEFAULT_CONTENT_COMPARISON_CONFIG) {
5678
+ const srcContent = sourceElements.filter(isContentElement2);
5679
+ const tgtContent = targetElements.filter(isContentElement2);
5680
+ const headings = compareHeadings(srcContent, tgtContent, config);
5681
+ const metrics = compareMetrics(srcContent, tgtContent, config);
5682
+ const statuses = compareStatuses(srcContent, tgtContent, config);
5683
+ const labels = compareLabels(srcContent, tgtContent, config);
5684
+ const tables = compareTables(sourceElements, targetElements, config);
5685
+ const headingHierarchy = compareHeadingHierarchy(srcContent, tgtContent);
5686
+ const contentParity = calculateContentParity(headings, metrics, statuses, labels, tables);
5687
+ return {
5688
+ headings,
5689
+ metrics,
5690
+ statuses,
5691
+ labels,
5692
+ tables,
5693
+ headingHierarchy,
5694
+ contentParity
5695
+ };
5696
+ }
5697
+ function calculateContentParity(headings, metrics, statuses, labels, tables) {
5698
+ const scores = [];
5699
+ const totalHeadings = headings.matched.length + headings.changed.length + headings.sourceOnly.length + headings.targetOnly.length;
5700
+ if (totalHeadings > 0) {
5701
+ scores.push(headings.matched.length / totalHeadings);
5702
+ }
5703
+ const totalMetrics = metrics.matched.length + metrics.changed.length + metrics.sourceOnly.length + metrics.targetOnly.length;
5704
+ if (totalMetrics > 0) {
5705
+ const metricScore = (metrics.matched.length + metrics.changed.length * 0.5) / totalMetrics;
5706
+ scores.push(metricScore);
5707
+ }
5708
+ const totalStatuses = statuses.matched.length + statuses.changed.length;
5709
+ if (totalStatuses > 0) {
5710
+ scores.push(statuses.matched.length / totalStatuses);
5711
+ }
5712
+ const totalLabels = labels.matched.length + labels.sourceOnly.length + labels.targetOnly.length;
5713
+ if (totalLabels > 0) {
5714
+ scores.push(labels.matched.length / totalLabels);
5715
+ }
5716
+ if (tables.length > 0) {
5717
+ let tableScore = 0;
5718
+ for (const table of tables) {
5719
+ let tScore = table.columnsMatch ? 0.5 : 0;
5720
+ if (table.sourceRowCount > 0) {
5721
+ const rowRatio = Math.min(
5722
+ table.targetRowCount / table.sourceRowCount,
5723
+ table.sourceRowCount / table.targetRowCount
5724
+ );
5725
+ tScore += rowRatio * 0.3;
5726
+ } else {
5727
+ tScore += 0.3;
5728
+ }
5729
+ const totalCells = Math.max(table.sourceRowCount, 1) * Math.max(
5730
+ table.sourceOnlyColumns.length + table.targetOnlyColumns.length + (table.columnsMatch ? 1 : 0),
5731
+ 1
5732
+ );
5733
+ const diffRatio = totalCells > 0 ? 1 - Math.min(table.cellDifferences.length / totalCells, 1) : 1;
5734
+ tScore += diffRatio * 0.2;
5735
+ tableScore += tScore;
5736
+ }
5737
+ scores.push(tableScore / tables.length);
5738
+ }
5739
+ if (scores.length === 0) return 1;
5740
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100;
5741
+ }
5742
+
5743
+ // src/ai/comparison-report.ts
5744
+ var DEFAULT_COMPARISON_REPORT_CONFIG = {
5745
+ includeComponents: false
5746
+ };
5747
+ function generateComparisonReport(source, target, options) {
5748
+ const startTime = Date.now();
5749
+ const config = { ...DEFAULT_COMPARISON_REPORT_CONFIG, ...options?.config };
5750
+ const srcElements = source.elements;
5751
+ const tgtElements = target.elements;
5752
+ const diff = computeCrossAppDiff(srcElements, tgtElements);
5753
+ const navigation = buildNavigationMap(srcElements, tgtElements);
5754
+ const sourceRegions = segmentPageRegions(srcElements);
5755
+ const targetRegions = segmentPageRegions(tgtElements);
5756
+ const layout = compareLayouts(srcElements, tgtElements, sourceRegions, targetRegions);
5757
+ const actionParityResults = analyzeActionParity(diff.matchedPairs, srcElements, tgtElements);
5758
+ const componentComparison = config.includeComponents && options?.sourceComponents && options?.targetComponents ? compareComponents(options.sourceComponents, options.targetComponents) : null;
5759
+ const contentComparison = compareContent(srcElements, tgtElements);
5760
+ const sourceData = extractPageData(srcElements);
5761
+ extractPageData(tgtElements);
5762
+ const sourceFieldCount = Object.keys(sourceData.values).length;
5763
+ const matchedDataCount = diff.dataComparisons.length;
5764
+ const dataCompleteness = sourceFieldCount > 0 ? Math.round(matchedDataCount / sourceFieldCount * 100) / 100 : 1;
5765
+ const formatMatchCount = diff.dataComparisons.filter((c) => c.formatsMatch).length;
5766
+ const formatAlignment = matchedDataCount > 0 ? Math.round(formatMatchCount / matchedDataCount * 100) / 100 : 1;
5767
+ const presentationAlignment = layout.similarity;
5768
+ const totalNavItems = navigation.pairs.length + navigation.sourceOnly.length;
5769
+ const navigationParity = totalNavItems > 0 ? Math.round(navigation.pairs.length / totalNavItems * 100) / 100 : 1;
5770
+ const totalActionChecks = actionParityResults.length;
5771
+ const fullParityCount = actionParityResults.filter((r) => r.missingInTarget.length === 0).length;
5772
+ const actionParity = totalActionChecks > 0 ? Math.round(fullParityCount / totalActionChecks * 100) / 100 : 1;
5773
+ const contentParity = contentComparison.contentParity;
5774
+ const overallScore = Math.round(
5775
+ (dataCompleteness * 0.2 + formatAlignment * 0.1 + presentationAlignment * 0.15 + navigationParity * 0.15 + actionParity * 0.15 + contentParity * 0.25) * 100
5776
+ ) / 100;
5777
+ const issues = [];
5778
+ for (const srcId of diff.unmatchedSourceIds) {
5779
+ const srcVal = Object.values(sourceData.values).find((v) => v.elementId === srcId);
5780
+ if (srcVal) {
5781
+ issues.push({
5782
+ severity: "warning",
5783
+ category: "missing-data",
5784
+ description: `Data field "${srcVal.label}" (${srcVal.dataType}) exists in source but has no match in target`,
5785
+ sourceElementId: srcId
5786
+ });
5787
+ }
5788
+ }
5789
+ for (const comp of diff.dataComparisons) {
5790
+ if (!comp.valuesMatch) {
5791
+ issues.push({
5792
+ severity: "error",
5793
+ category: "value-mismatch",
5794
+ description: `Value mismatch for "${comp.label}": source="${comp.sourceValue}", target="${comp.targetValue}"`
5795
+ });
5796
+ }
5797
+ }
5798
+ for (const fm of diff.formatMismatches) {
5799
+ issues.push({
5800
+ severity: fm.severity,
5801
+ category: "format-mismatch",
5802
+ description: fm.description
5803
+ });
5804
+ }
5805
+ for (const ap of actionParityResults) {
5806
+ for (const action of ap.missingInTarget) {
5807
+ issues.push({
5808
+ severity: "warning",
5809
+ category: "missing-action",
5810
+ description: `Action "${action}" available on source element "${ap.pair.sourceLabel}" is missing in target`,
5811
+ sourceElementId: ap.pair.sourceId,
5812
+ targetElementId: ap.pair.targetId
5813
+ });
5814
+ }
5815
+ }
5816
+ for (const srcId of navigation.sourceOnly) {
5817
+ issues.push({
5818
+ severity: "warning",
5819
+ category: "navigation-gap",
5820
+ description: `Navigation item "${srcId}" in source has no match in target`,
5821
+ sourceElementId: srcId
5822
+ });
5823
+ }
5824
+ if (layout.similarity < 0.5) {
5825
+ issues.push({
5826
+ severity: "warning",
5827
+ category: "layout-difference",
5828
+ description: `Layout similarity is low (${layout.similarity}). Grid: ${layout.gridDiff.sourceGrid.columnCount} cols vs ${layout.gridDiff.targetGrid.columnCount} cols`
5829
+ });
5830
+ }
5831
+ if (componentComparison) {
5832
+ for (const src of componentComparison.sourceOnly) {
5833
+ issues.push({
5834
+ severity: "info",
5835
+ category: "component-mismatch",
5836
+ description: `Component "${src.name}" (${src.type}) exists in source but not target`
5837
+ });
5838
+ }
5839
+ for (const match of componentComparison.matches) {
5840
+ if (match.stateKeyDiff.missing.length > 0) {
5841
+ issues.push({
5842
+ severity: "warning",
5843
+ category: "component-mismatch",
5844
+ description: `Component "${match.source.name}": state keys missing in target: ${match.stateKeyDiff.missing.join(", ")}`
5845
+ });
5846
+ }
5847
+ }
5848
+ }
5849
+ for (const heading of contentComparison.headings.sourceOnly) {
5850
+ issues.push({
5851
+ severity: "warning",
5852
+ category: "content-difference",
5853
+ description: `Heading "${heading}" exists in source but not in target`
5854
+ });
5855
+ }
5856
+ for (const heading of contentComparison.headings.targetOnly) {
5857
+ issues.push({
5858
+ severity: "info",
5859
+ category: "content-difference",
5860
+ description: `Heading "${heading}" exists in target but not in source`
5861
+ });
5862
+ }
5863
+ for (const change of contentComparison.headings.changed) {
5864
+ issues.push({
5865
+ severity: "warning",
5866
+ category: "content-difference",
5867
+ description: `Heading changed: "${change.source}" -> "${change.target}"`
5868
+ });
5869
+ }
5870
+ for (const change of contentComparison.metrics.changed) {
5871
+ issues.push({
5872
+ severity: "warning",
5873
+ category: "content-difference",
5874
+ description: `Metric "${change.label}" value differs: "${change.sourceValue}" vs "${change.targetValue}"`
5875
+ });
5876
+ }
5877
+ for (const label of contentComparison.metrics.sourceOnly) {
5878
+ issues.push({
5879
+ severity: "warning",
5880
+ category: "content-difference",
5881
+ description: `Metric "${label}" exists in source but not in target`
5882
+ });
5883
+ }
5884
+ for (const change of contentComparison.statuses.changed) {
5885
+ issues.push({
5886
+ severity: "warning",
5887
+ category: "content-difference",
5888
+ description: `Status "${change.label}" differs: "${change.sourceStatus}" vs "${change.targetStatus}"`
5889
+ });
5890
+ }
5891
+ for (const table of contentComparison.tables) {
5892
+ if (!table.columnsMatch) {
5893
+ issues.push({
5894
+ severity: "warning",
5895
+ category: "content-difference",
5896
+ description: `Table "${table.sourceLabel}" column mismatch: source-only=[${table.sourceOnlyColumns.join(", ")}], target-only=[${table.targetOnlyColumns.join(", ")}]`
5897
+ });
5898
+ }
5899
+ if (table.sourceRowCount !== table.targetRowCount) {
5900
+ issues.push({
5901
+ severity: "info",
5902
+ category: "content-difference",
5903
+ description: `Table "${table.sourceLabel}" row count differs: ${table.sourceRowCount} vs ${table.targetRowCount}`
5904
+ });
5905
+ }
5906
+ if (table.cellDifferences.length > 0) {
5907
+ issues.push({
5908
+ severity: "warning",
5909
+ category: "content-difference",
5910
+ description: `Table "${table.sourceLabel}" has ${table.cellDifferences.length} cell value difference(s)`
5911
+ });
5912
+ }
5913
+ }
5914
+ const severityOrder = { error: 0, warning: 1, info: 2 };
5915
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
5916
+ const errorCount = issues.filter((i) => i.severity === "error").length;
5917
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
5918
+ const infoCount = issues.filter((i) => i.severity === "info").length;
5919
+ const summaryLines = [
5920
+ `Cross-app comparison: ${source.page.url} vs ${target.page.url}`,
5921
+ `Overall score: ${(overallScore * 100).toFixed(0)}%`,
5922
+ `Matched elements: ${diff.matchedPairs.length}`,
5923
+ `Unmatched: ${diff.unmatchedSourceIds.length} source, ${diff.unmatchedTargetIds.length} target`,
5924
+ `Navigation: ${navigation.pairs.length} matched, ${navigation.sourceOnly.length} source-only, ${navigation.targetOnly.length} target-only`
5925
+ ];
5926
+ if (componentComparison) {
5927
+ summaryLines.push(
5928
+ `Components: ${componentComparison.matches.length} matched, ${componentComparison.sourceOnly.length} source-only, ${componentComparison.targetOnly.length} target-only`
5929
+ );
5930
+ }
5931
+ const hMatched = contentComparison.headings.matched.length;
5932
+ const hChanged = contentComparison.headings.changed.length;
5933
+ const hSrcOnly = contentComparison.headings.sourceOnly.length;
5934
+ const hTgtOnly = contentComparison.headings.targetOnly.length;
5935
+ const mMatched = contentComparison.metrics.matched.length;
5936
+ const mChanged = contentComparison.metrics.changed.length;
5937
+ const sMatched = contentComparison.statuses.matched.length;
5938
+ const sChanged = contentComparison.statuses.changed.length;
5939
+ const totalContent = hMatched + hChanged + hSrcOnly + hTgtOnly + mMatched + mChanged + sMatched + sChanged;
5940
+ if (totalContent > 0) {
5941
+ summaryLines.push(
5942
+ `Content: headings=${hMatched} matched/${hChanged} changed/${hSrcOnly + hTgtOnly} unmatched, metrics=${mMatched} matched/${mChanged} changed, statuses=${sMatched} matched/${sChanged} changed, parity=${(contentParity * 100).toFixed(0)}%`
5943
+ );
5944
+ }
5945
+ summaryLines.push(`Issues: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
5946
+ const summary = summaryLines.join("\n");
5947
+ const report = {
5948
+ sourceUrl: source.page.url,
5949
+ targetUrl: target.page.url,
5950
+ timestamp: Date.now(),
5951
+ durationMs: Date.now() - startTime,
5952
+ scores: {
5953
+ dataCompleteness,
5954
+ formatAlignment,
5955
+ presentationAlignment,
5956
+ navigationParity,
5957
+ actionParity,
5958
+ overallScore
5959
+ },
5960
+ diff,
5961
+ navigation,
5962
+ layout,
5963
+ contentComparison,
5964
+ issues,
5965
+ summary
5966
+ };
5967
+ if (componentComparison) {
5968
+ report.components = componentComparison;
5969
+ }
5970
+ return report;
5971
+ }
5972
+
5973
+ // src/server/handlers.ts
5974
+ function success(data) {
5975
+ return {
5976
+ success: true,
5977
+ data,
5978
+ timestamp: Date.now()
3635
5979
  };
3636
5980
  }
3637
5981
  function error(message, code) {
@@ -3646,49 +5990,125 @@ function getRecoverySuggestions(errorCode) {
3646
5990
  switch (errorCode) {
3647
5991
  case "ELEMENT_NOT_FOUND":
3648
5992
  return [
3649
- { suggestion: "Wait for the page to fully load", command: "wait for page to load", confidence: 0.7, retryable: true },
3650
- { suggestion: "Use a different description for the element", confidence: 0.8, retryable: false },
3651
- { suggestion: "Scroll the page to reveal the element", command: "scroll down", confidence: 0.6, retryable: true }
5993
+ {
5994
+ suggestion: "Wait for the page to fully load",
5995
+ command: "wait for page to load",
5996
+ confidence: 0.7,
5997
+ retryable: true
5998
+ },
5999
+ {
6000
+ suggestion: "Use a different description for the element",
6001
+ confidence: 0.8,
6002
+ retryable: false
6003
+ },
6004
+ {
6005
+ suggestion: "Scroll the page to reveal the element",
6006
+ command: "scroll down",
6007
+ confidence: 0.6,
6008
+ retryable: true
6009
+ }
3652
6010
  ];
3653
6011
  case "ELEMENT_NOT_VISIBLE":
3654
6012
  return [
3655
- { suggestion: "Scroll to make the element visible", command: "scroll to element", confidence: 0.9, retryable: true },
3656
- { suggestion: "Wait for any loading overlays to disappear", confidence: 0.7, retryable: true },
3657
- { suggestion: "Close any blocking modals or popups", command: "click close button", confidence: 0.8, retryable: true }
6013
+ {
6014
+ suggestion: "Scroll to make the element visible",
6015
+ command: "scroll to element",
6016
+ confidence: 0.9,
6017
+ retryable: true
6018
+ },
6019
+ {
6020
+ suggestion: "Wait for any loading overlays to disappear",
6021
+ confidence: 0.7,
6022
+ retryable: true
6023
+ },
6024
+ {
6025
+ suggestion: "Close any blocking modals or popups",
6026
+ command: "click close button",
6027
+ confidence: 0.8,
6028
+ retryable: true
6029
+ }
3658
6030
  ];
3659
6031
  case "ELEMENT_NOT_ENABLED":
3660
6032
  return [
3661
6033
  { suggestion: "Fill in required fields first", confidence: 0.8, retryable: false },
3662
- { suggestion: "Complete prerequisite steps in the form", confidence: 0.7, retryable: false },
3663
- { suggestion: "Wait for the element to become enabled", command: "wait for element to be enabled", confidence: 0.6, retryable: true }
6034
+ {
6035
+ suggestion: "Complete prerequisite steps in the form",
6036
+ confidence: 0.7,
6037
+ retryable: false
6038
+ },
6039
+ {
6040
+ suggestion: "Wait for the element to become enabled",
6041
+ command: "wait for element to be enabled",
6042
+ confidence: 0.6,
6043
+ retryable: true
6044
+ }
3664
6045
  ];
3665
6046
  case "ELEMENT_NOT_INTERACTABLE":
3666
6047
  return [
3667
- { suggestion: "Close any modal or popup blocking the element", command: "click close button", confidence: 0.9, retryable: true },
6048
+ {
6049
+ suggestion: "Close any modal or popup blocking the element",
6050
+ command: "click close button",
6051
+ confidence: 0.9,
6052
+ retryable: true
6053
+ },
3668
6054
  { suggestion: "Wait for animations to complete", confidence: 0.7, retryable: true },
3669
- { suggestion: "Scroll the element into the viewport", command: "scroll to element", confidence: 0.8, retryable: true }
6055
+ {
6056
+ suggestion: "Scroll the element into the viewport",
6057
+ command: "scroll to element",
6058
+ confidence: 0.8,
6059
+ retryable: true
6060
+ }
3670
6061
  ];
3671
6062
  case "ACTION_TIMEOUT":
3672
6063
  return [
3673
6064
  { suggestion: "Increase the timeout duration", confidence: 0.8, retryable: true },
3674
6065
  { suggestion: "Check if the condition can ever be met", confidence: 0.7, retryable: false },
3675
- { suggestion: "Verify the page is responding", command: "check page status", confidence: 0.6, retryable: true }
6066
+ {
6067
+ suggestion: "Verify the page is responding",
6068
+ command: "check page status",
6069
+ confidence: 0.6,
6070
+ retryable: true
6071
+ }
3676
6072
  ];
3677
6073
  case "LOW_CONFIDENCE":
3678
6074
  return [
3679
- { suggestion: "Use the exact text shown on the element", confidence: 0.9, retryable: false },
3680
- { suggestion: "Try a different description that more closely matches the element", confidence: 0.8, retryable: false },
3681
- { suggestion: "Lower the confidence threshold if the match is correct", confidence: 0.7, retryable: true }
6075
+ {
6076
+ suggestion: "Use the exact text shown on the element",
6077
+ confidence: 0.9,
6078
+ retryable: false
6079
+ },
6080
+ {
6081
+ suggestion: "Try a different description that more closely matches the element",
6082
+ confidence: 0.8,
6083
+ retryable: false
6084
+ },
6085
+ {
6086
+ suggestion: "Lower the confidence threshold if the match is correct",
6087
+ confidence: 0.7,
6088
+ retryable: true
6089
+ }
3682
6090
  ];
3683
6091
  case "AMBIGUOUS_MATCH":
3684
6092
  return [
3685
- { suggestion: "Be more specific about which element you mean", confidence: 0.9, retryable: false },
3686
- { suggestion: "Include the section or form name in the description", confidence: 0.8, retryable: false },
6093
+ {
6094
+ suggestion: "Be more specific about which element you mean",
6095
+ confidence: 0.9,
6096
+ retryable: false
6097
+ },
6098
+ {
6099
+ suggestion: "Include the section or form name in the description",
6100
+ confidence: 0.8,
6101
+ retryable: false
6102
+ },
3687
6103
  { suggestion: "Use the element ID directly", confidence: 0.7, retryable: false }
3688
6104
  ];
3689
6105
  default:
3690
6106
  return [
3691
- { suggestion: "Try a different approach or check the page state", confidence: 0.5, retryable: false }
6107
+ {
6108
+ suggestion: "Try a different approach or check the page state",
6109
+ confidence: 0.5,
6110
+ retryable: false
6111
+ }
3692
6112
  ];
3693
6113
  }
3694
6114
  }
@@ -3717,6 +6137,9 @@ function createHandlers(registry, actionExecutor, config = {}) {
3717
6137
  const assertionExecutor = new AssertionExecutor();
3718
6138
  const snapshotManager = new SemanticSnapshotManager();
3719
6139
  const diffManager = new SemanticDiffManager();
6140
+ const intentRegistry = /* @__PURE__ */ new Map();
6141
+ const consoleCapture = config.consoleCapture ?? null;
6142
+ const annotationStore = config.annotationStore ?? getGlobalAnnotationStore();
3720
6143
  function refreshElements() {
3721
6144
  const elements = registry.getAllElements();
3722
6145
  searchEngine.updateElements(elements);
@@ -3783,10 +6206,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
3783
6206
  try {
3784
6207
  const element = registry.getElement(id);
3785
6208
  if (!element) {
3786
- const failureDetails = createFailureDetails("ELEMENT_NOT_FOUND", `Element not found: ${id}`, {
3787
- elementId: id,
3788
- selectorsTried: [id]
3789
- });
6209
+ const failureDetails = createFailureDetails(
6210
+ "ELEMENT_NOT_FOUND",
6211
+ `Element not found: ${id}`,
6212
+ {
6213
+ elementId: id,
6214
+ selectorsTried: [id]
6215
+ }
6216
+ );
3790
6217
  return {
3791
6218
  success: false,
3792
6219
  error: `Element not found: ${id}`,
@@ -3816,11 +6243,15 @@ function createHandlers(registry, actionExecutor, config = {}) {
3816
6243
  try {
3817
6244
  const element = registry.getElement(id);
3818
6245
  if (!element) {
3819
- const failureDetails = createFailureDetails("ELEMENT_NOT_FOUND", `Element not found: ${id}`, {
3820
- elementId: id,
3821
- selectorsTried: [id],
3822
- durationMs: Date.now() - startTime
3823
- });
6246
+ const failureDetails = createFailureDetails(
6247
+ "ELEMENT_NOT_FOUND",
6248
+ `Element not found: ${id}`,
6249
+ {
6250
+ elementId: id,
6251
+ selectorsTried: [id],
6252
+ durationMs: Date.now() - startTime
6253
+ }
6254
+ );
3824
6255
  return {
3825
6256
  success: false,
3826
6257
  error: `Element not found: ${id}`,
@@ -3855,10 +6286,14 @@ function createHandlers(registry, actionExecutor, config = {}) {
3855
6286
  } else if (errorMsg.includes("blocked") || errorMsg.includes("interactable")) {
3856
6287
  errorCode = "ELEMENT_NOT_INTERACTABLE";
3857
6288
  }
3858
- const failureDetails = createFailureDetails(errorCode, actionResult.error || "Action failed", {
3859
- elementId: id,
3860
- durationMs: Date.now() - startTime
3861
- });
6289
+ const failureDetails = createFailureDetails(
6290
+ errorCode,
6291
+ actionResult.error || "Action failed",
6292
+ {
6293
+ elementId: id,
6294
+ durationMs: Date.now() - startTime
6295
+ }
6296
+ );
3862
6297
  return success({
3863
6298
  ...actionResult,
3864
6299
  failureDetails
@@ -3957,7 +6392,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
3957
6392
  try {
3958
6393
  const findRequest = request;
3959
6394
  const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
3960
- return success({ elements, timestamp: Date.now(), total: elements.length, durationMs: 0 });
6395
+ return success({
6396
+ elements,
6397
+ timestamp: Date.now(),
6398
+ total: elements.length,
6399
+ durationMs: 0
6400
+ });
3961
6401
  } catch (err) {
3962
6402
  return error(err.message, "FIND_ERROR");
3963
6403
  }
@@ -3966,7 +6406,12 @@ function createHandlers(registry, actionExecutor, config = {}) {
3966
6406
  try {
3967
6407
  const findRequest = request;
3968
6408
  const elements = registry.findElements?.(findRequest) ?? registry.getAllElements();
3969
- return success({ elements, timestamp: Date.now(), total: elements.length, durationMs: 0 });
6409
+ return success({
6410
+ elements,
6411
+ timestamp: Date.now(),
6412
+ total: elements.length,
6413
+ durationMs: 0
6414
+ });
3970
6415
  } catch (err) {
3971
6416
  return error(err.message, "DISCOVER_ERROR");
3972
6417
  }
@@ -4063,6 +6508,28 @@ function createHandlers(registry, actionExecutor, config = {}) {
4063
6508
  return error(err.message, "ELEMENT_TREE_ERROR");
4064
6509
  }
4065
6510
  },
6511
+ getConsoleErrors: async (params) => {
6512
+ try {
6513
+ if (!consoleCapture) {
6514
+ return success({ errors: [], count: 0 });
6515
+ }
6516
+ const errors = params?.since ? consoleCapture.getConsoleSince(params.since) : consoleCapture.getConsoleRecent(params?.limit ?? 50);
6517
+ return success({ errors, count: errors.length });
6518
+ } catch (err) {
6519
+ return error(err.message, "CONSOLE_ERRORS_ERROR");
6520
+ }
6521
+ },
6522
+ clearConsoleErrors: async () => {
6523
+ try {
6524
+ if (!consoleCapture) {
6525
+ return success({ cleared: false });
6526
+ }
6527
+ consoleCapture.clear();
6528
+ return success({ cleared: true });
6529
+ } catch (err) {
6530
+ return error(err.message, "CONSOLE_CLEAR_ERROR");
6531
+ }
6532
+ },
4066
6533
  // =========================================================================
4067
6534
  // AI-Native Handlers
4068
6535
  // =========================================================================
@@ -4093,94 +6560,207 @@ function createHandlers(registry, actionExecutor, config = {}) {
4093
6560
  return error(err.message, "AI_ASSERT_ERROR");
4094
6561
  }
4095
6562
  },
4096
- aiAssertBatch: async (request) => {
6563
+ aiAssertBatch: async (request) => {
6564
+ try {
6565
+ refreshElements();
6566
+ const result = await assertionExecutor.assertBatch(request);
6567
+ return success(result);
6568
+ } catch (err) {
6569
+ return error(err.message, "AI_ASSERT_BATCH_ERROR");
6570
+ }
6571
+ },
6572
+ getSemanticSnapshot: async () => {
6573
+ try {
6574
+ const controlSnapshot = registry.createSnapshot();
6575
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
6576
+ return success(snapshot);
6577
+ } catch (err) {
6578
+ return error(err.message, "SEMANTIC_SNAPSHOT_ERROR");
6579
+ }
6580
+ },
6581
+ getSemanticDiff: async (_since) => {
6582
+ try {
6583
+ const controlSnapshot = registry.createSnapshot();
6584
+ const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
6585
+ const diff = diffManager.update(currentSnapshot);
6586
+ return success(diff);
6587
+ } catch (err) {
6588
+ return error(err.message, "SEMANTIC_DIFF_ERROR");
6589
+ }
6590
+ },
6591
+ getPageSummary: async () => {
6592
+ try {
6593
+ const snapshot = registry.createSnapshot();
6594
+ const elements = snapshot.elements.map((el) => ({
6595
+ ...el,
6596
+ description: el.label || el.id,
6597
+ aliases: [],
6598
+ suggestedActions: [],
6599
+ tagName: el.type,
6600
+ accessibleName: el.label,
6601
+ registered: true
6602
+ }));
6603
+ const summary = generatePageSummary(elements);
6604
+ return success(summary);
6605
+ } catch (err) {
6606
+ return error(err.message, "PAGE_SUMMARY_ERROR");
6607
+ }
6608
+ },
6609
+ // =========================================================================
6610
+ // Semantic Search Handler (Embedding-based)
6611
+ // =========================================================================
6612
+ // =========================================================================
6613
+ // Page Navigation Handlers
6614
+ // =========================================================================
6615
+ pageRefresh: async () => {
6616
+ try {
6617
+ window.location.reload();
6618
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6619
+ } catch (err) {
6620
+ return error(err.message, "PAGE_REFRESH_ERROR");
6621
+ }
6622
+ },
6623
+ pageNavigate: async (request) => {
6624
+ try {
6625
+ if (!request.url) {
6626
+ return error("URL is required", "INVALID_REQUEST");
6627
+ }
6628
+ window.location.href = request.url;
6629
+ return success({ success: true, url: request.url, timestamp: Date.now() });
6630
+ } catch (err) {
6631
+ return error(err.message, "PAGE_NAVIGATE_ERROR");
6632
+ }
6633
+ },
6634
+ pageGoBack: async () => {
6635
+ try {
6636
+ window.history.back();
6637
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6638
+ } catch (err) {
6639
+ return error(err.message, "PAGE_GO_BACK_ERROR");
6640
+ }
6641
+ },
6642
+ pageGoForward: async () => {
6643
+ try {
6644
+ window.history.forward();
6645
+ return success({ success: true, url: window.location.href, timestamp: Date.now() });
6646
+ } catch (err) {
6647
+ return error(err.message, "PAGE_GO_FORWARD_ERROR");
6648
+ }
6649
+ },
6650
+ // =========================================================================
6651
+ // Annotation Handlers
6652
+ //
6653
+ // REST API endpoints for managing element annotations:
6654
+ // GET /annotations - List all annotations
6655
+ // GET /annotations/export - Export all annotations as AnnotationConfig
6656
+ // GET /annotations/coverage - Get annotation coverage statistics
6657
+ // GET /annotations/:id - Get annotation for a specific element
6658
+ // PUT /annotations/:id - Create or update an annotation
6659
+ // DELETE /annotations/:id - Delete an annotation
6660
+ // POST /annotations/import - Import annotations from AnnotationConfig
6661
+ // =========================================================================
6662
+ getAnnotations: async () => {
6663
+ try {
6664
+ return success(annotationStore.getAll());
6665
+ } catch (err) {
6666
+ return error(err.message, "ANNOTATIONS_ERROR");
6667
+ }
6668
+ },
6669
+ getAnnotation: async (id) => {
6670
+ try {
6671
+ const annotation = annotationStore.get(id);
6672
+ if (!annotation) {
6673
+ return error(`Annotation not found: ${id}`, "NOT_FOUND");
6674
+ }
6675
+ return success(annotation);
6676
+ } catch (err) {
6677
+ return error(err.message, "ANNOTATION_ERROR");
6678
+ }
6679
+ },
6680
+ setAnnotation: async (id, annotation) => {
6681
+ try {
6682
+ annotationStore.set(id, annotation);
6683
+ return success(annotationStore.get(id));
6684
+ } catch (err) {
6685
+ return error(err.message, "ANNOTATION_SET_ERROR");
6686
+ }
6687
+ },
6688
+ deleteAnnotation: async (id) => {
4097
6689
  try {
4098
- refreshElements();
4099
- const result = await assertionExecutor.assertBatch(request);
4100
- return success(result);
6690
+ const existed = annotationStore.delete(id);
6691
+ if (!existed) {
6692
+ return error(`Annotation not found: ${id}`, "NOT_FOUND");
6693
+ }
6694
+ return success(void 0);
4101
6695
  } catch (err) {
4102
- return error(err.message, "AI_ASSERT_BATCH_ERROR");
6696
+ return error(err.message, "ANNOTATION_DELETE_ERROR");
4103
6697
  }
4104
6698
  },
4105
- getSemanticSnapshot: async () => {
6699
+ importAnnotations: async (config2) => {
4106
6700
  try {
4107
- const controlSnapshot = registry.createSnapshot();
4108
- const snapshot = snapshotManager.createSnapshot(controlSnapshot);
4109
- return success(snapshot);
6701
+ const count = annotationStore.importConfig(config2);
6702
+ return success({ count });
4110
6703
  } catch (err) {
4111
- return error(err.message, "SEMANTIC_SNAPSHOT_ERROR");
6704
+ return error(err.message, "ANNOTATION_IMPORT_ERROR");
4112
6705
  }
4113
6706
  },
4114
- getSemanticDiff: async (_since) => {
6707
+ exportAnnotations: async () => {
4115
6708
  try {
4116
- const controlSnapshot = registry.createSnapshot();
4117
- const currentSnapshot = snapshotManager.createSnapshot(controlSnapshot);
4118
- const diff = diffManager.update(currentSnapshot);
4119
- return success(diff);
6709
+ return success(annotationStore.exportConfig());
4120
6710
  } catch (err) {
4121
- return error(err.message, "SEMANTIC_DIFF_ERROR");
6711
+ return error(err.message, "ANNOTATION_EXPORT_ERROR");
4122
6712
  }
4123
6713
  },
4124
- getPageSummary: async () => {
6714
+ getAnnotationCoverage: async () => {
4125
6715
  try {
4126
- const snapshot = registry.createSnapshot();
4127
- const elements = snapshot.elements.map((el) => ({
4128
- ...el,
4129
- description: el.label || el.id,
4130
- aliases: [],
4131
- suggestedActions: [],
4132
- tagName: el.type,
4133
- accessibleName: el.label,
4134
- registered: true
4135
- }));
4136
- const summary = generatePageSummary(elements);
4137
- return success(summary);
6716
+ const allElements = registry.getAllElements();
6717
+ const allIds = allElements.map((el) => el.id);
6718
+ return success(annotationStore.getCoverage(allIds));
4138
6719
  } catch (err) {
4139
- return error(err.message, "PAGE_SUMMARY_ERROR");
6720
+ return error(err.message, "ANNOTATION_COVERAGE_ERROR");
4140
6721
  }
4141
6722
  },
4142
- // =========================================================================
4143
- // Semantic Search Handler (Embedding-based)
4144
- // =========================================================================
4145
6723
  aiSemanticSearch: async (criteria) => {
4146
6724
  const startTime = performance.now();
4147
6725
  try {
4148
6726
  refreshElements();
4149
6727
  const allElements = registry.getAllElements();
4150
- const aiElements = allElements.map((el) => {
4151
- const textParts = [];
4152
- const state = "getState" in el ? el.getState() : el.state;
4153
- const textContent = state?.textContent || "";
4154
- const label = el.label || "";
4155
- const accessibleName = el.accessibleName || "";
4156
- const placeholder = el.placeholder || "";
4157
- const title = el.title || "";
4158
- if (label) textParts.push(label);
4159
- if (accessibleName && accessibleName !== label) textParts.push(accessibleName);
4160
- if (textContent && textContent !== label && textContent !== accessibleName) {
4161
- textParts.push(textContent);
6728
+ const aiElements = allElements.map(
6729
+ (el) => {
6730
+ const textParts = [];
6731
+ const state = "getState" in el ? el.getState() : el.state;
6732
+ const textContent = state?.textContent || "";
6733
+ const label = el.label || "";
6734
+ const accessibleName = el.accessibleName || "";
6735
+ const placeholder = el.placeholder || "";
6736
+ const title = el.title || "";
6737
+ if (label) textParts.push(label);
6738
+ if (accessibleName && accessibleName !== label) textParts.push(accessibleName);
6739
+ if (textContent && textContent !== label && textContent !== accessibleName) {
6740
+ textParts.push(textContent);
6741
+ }
6742
+ if (placeholder) textParts.push(`placeholder: ${placeholder}`);
6743
+ if (title) textParts.push(title);
6744
+ const combinedText = textParts.join(" ").trim() || el.id;
6745
+ return {
6746
+ element: {
6747
+ id: el.id,
6748
+ type: el.type,
6749
+ label: el.label,
6750
+ tagName: el.tagName || el.type,
6751
+ role: el.role,
6752
+ accessibleName: el.accessibleName,
6753
+ actions: el.actions || [],
6754
+ state: state || {},
6755
+ registered: true,
6756
+ description: label || el.id,
6757
+ aliases: [],
6758
+ suggestedActions: []
6759
+ },
6760
+ text: combinedText
6761
+ };
4162
6762
  }
4163
- if (placeholder) textParts.push(`placeholder: ${placeholder}`);
4164
- if (title) textParts.push(title);
4165
- const combinedText = textParts.join(" ").trim() || el.id;
4166
- return {
4167
- element: {
4168
- id: el.id,
4169
- type: el.type,
4170
- label: el.label,
4171
- tagName: el.tagName || el.type,
4172
- role: el.role,
4173
- accessibleName: el.accessibleName,
4174
- actions: el.actions || [],
4175
- state: state || {},
4176
- registered: true,
4177
- description: label || el.id,
4178
- aliases: [],
4179
- suggestedActions: []
4180
- },
4181
- text: combinedText
4182
- };
4183
- });
6763
+ );
4184
6764
  let filteredElements = aiElements;
4185
6765
  if (criteria.type) {
4186
6766
  filteredElements = filteredElements.filter(
@@ -4245,6 +6825,398 @@ function createHandlers(registry, actionExecutor, config = {}) {
4245
6825
  } catch (err) {
4246
6826
  return error(err.message, "AI_SEMANTIC_SEARCH_ERROR");
4247
6827
  }
6828
+ },
6829
+ // =========================================================================
6830
+ // State Management Handlers
6831
+ // =========================================================================
6832
+ getStates: async () => {
6833
+ try {
6834
+ const states = registry.getStates?.() ?? [];
6835
+ return success(states);
6836
+ } catch (err) {
6837
+ return error(err.message, "STATES_ERROR");
6838
+ }
6839
+ },
6840
+ getState: async (id) => {
6841
+ try {
6842
+ const state = registry.getState?.(id);
6843
+ if (!state) {
6844
+ return error(`State not found: ${id}`, "NOT_FOUND");
6845
+ }
6846
+ return success(state);
6847
+ } catch (err) {
6848
+ return error(err.message, "STATE_ERROR");
6849
+ }
6850
+ },
6851
+ getActiveStates: async () => {
6852
+ try {
6853
+ const states = registry.getActiveStates?.() ?? [];
6854
+ return success(states);
6855
+ } catch (err) {
6856
+ return error(err.message, "ACTIVE_STATES_ERROR");
6857
+ }
6858
+ },
6859
+ activateState: async (id) => {
6860
+ try {
6861
+ if (!registry.activateState) {
6862
+ return error("State management not available", "NOT_IMPLEMENTED");
6863
+ }
6864
+ registry.activateState(id);
6865
+ return success(void 0);
6866
+ } catch (err) {
6867
+ return error(err.message, "ACTIVATE_STATE_ERROR");
6868
+ }
6869
+ },
6870
+ deactivateState: async (id) => {
6871
+ try {
6872
+ if (!registry.deactivateState) {
6873
+ return error("State management not available", "NOT_IMPLEMENTED");
6874
+ }
6875
+ registry.deactivateState(id);
6876
+ return success(void 0);
6877
+ } catch (err) {
6878
+ return error(err.message, "DEACTIVATE_STATE_ERROR");
6879
+ }
6880
+ },
6881
+ getStateGroups: async () => {
6882
+ try {
6883
+ const groups = registry.getStateGroups?.() ?? [];
6884
+ return success(groups);
6885
+ } catch (err) {
6886
+ return error(err.message, "STATE_GROUPS_ERROR");
6887
+ }
6888
+ },
6889
+ activateStateGroup: async (id) => {
6890
+ try {
6891
+ if (!registry.activateStateGroup) {
6892
+ return error("State group management not available", "NOT_IMPLEMENTED");
6893
+ }
6894
+ registry.activateStateGroup(id);
6895
+ return success(void 0);
6896
+ } catch (err) {
6897
+ return error(err.message, "ACTIVATE_STATE_GROUP_ERROR");
6898
+ }
6899
+ },
6900
+ deactivateStateGroup: async (id) => {
6901
+ try {
6902
+ if (!registry.deactivateStateGroup) {
6903
+ return error("State group management not available", "NOT_IMPLEMENTED");
6904
+ }
6905
+ registry.deactivateStateGroup(id);
6906
+ return success(void 0);
6907
+ } catch (err) {
6908
+ return error(err.message, "DEACTIVATE_STATE_GROUP_ERROR");
6909
+ }
6910
+ },
6911
+ getTransitions: async () => {
6912
+ try {
6913
+ const transitions = registry.getTransitions?.() ?? [];
6914
+ return success(transitions);
6915
+ } catch (err) {
6916
+ return error(err.message, "TRANSITIONS_ERROR");
6917
+ }
6918
+ },
6919
+ canExecuteTransition: async (id) => {
6920
+ try {
6921
+ if (!registry.canExecuteTransition) {
6922
+ return error("Transition management not available", "NOT_IMPLEMENTED");
6923
+ }
6924
+ const result = registry.canExecuteTransition(id);
6925
+ return success(result);
6926
+ } catch (err) {
6927
+ return error(err.message, "CAN_EXECUTE_TRANSITION_ERROR");
6928
+ }
6929
+ },
6930
+ executeTransition: async (id) => {
6931
+ try {
6932
+ if (!registry.executeTransition) {
6933
+ return error("Transition execution not available", "NOT_IMPLEMENTED");
6934
+ }
6935
+ const result = await registry.executeTransition(id);
6936
+ return success(result);
6937
+ } catch (err) {
6938
+ return error(err.message, "EXECUTE_TRANSITION_ERROR");
6939
+ }
6940
+ },
6941
+ findPath: async (request) => {
6942
+ try {
6943
+ if (!registry.findPath) {
6944
+ return error("Pathfinding not available", "NOT_IMPLEMENTED");
6945
+ }
6946
+ const result = registry.findPath(request.targetStates);
6947
+ return success(result);
6948
+ } catch (err) {
6949
+ return error(err.message, "FIND_PATH_ERROR");
6950
+ }
6951
+ },
6952
+ navigateTo: async (request) => {
6953
+ try {
6954
+ if (!registry.navigateTo) {
6955
+ return error("Navigation not available", "NOT_IMPLEMENTED");
6956
+ }
6957
+ const result = await registry.navigateTo(request.targetStates);
6958
+ return success(result);
6959
+ } catch (err) {
6960
+ return error(err.message, "NAVIGATE_TO_ERROR");
6961
+ }
6962
+ },
6963
+ getStateSnapshot: async () => {
6964
+ try {
6965
+ if (!registry.getStateSnapshot) {
6966
+ const snapshot = {
6967
+ timestamp: Date.now(),
6968
+ activeStates: (registry.getActiveStates?.() ?? []).map((s) => s.id),
6969
+ states: registry.getStates?.() ?? [],
6970
+ groups: registry.getStateGroups?.() ?? [],
6971
+ transitions: registry.getTransitions?.() ?? []
6972
+ };
6973
+ return success(snapshot);
6974
+ }
6975
+ return success(registry.getStateSnapshot());
6976
+ } catch (err) {
6977
+ return error(err.message, "STATE_SNAPSHOT_ERROR");
6978
+ }
6979
+ },
6980
+ // =========================================================================
6981
+ // Intent Handlers
6982
+ // =========================================================================
6983
+ executeIntent: async (request) => {
6984
+ const startTime = Date.now();
6985
+ try {
6986
+ refreshElements();
6987
+ const intent = intentRegistry.get(request.intentId);
6988
+ if (!intent) {
6989
+ return error(`Intent not found: ${request.intentId}`, "NOT_FOUND");
6990
+ }
6991
+ const nlResponse = await nlExecutor.execute({
6992
+ instruction: intent.description,
6993
+ context: `Executing intent: ${intent.name}`
6994
+ });
6995
+ return success({
6996
+ success: nlResponse.success,
6997
+ intentId: request.intentId,
6998
+ result: nlResponse,
6999
+ error: nlResponse.error,
7000
+ durationMs: Date.now() - startTime
7001
+ });
7002
+ } catch (err) {
7003
+ return error(err.message, "EXECUTE_INTENT_ERROR");
7004
+ }
7005
+ },
7006
+ findIntent: async (request) => {
7007
+ try {
7008
+ const query = request.query.toLowerCase();
7009
+ const results = [];
7010
+ for (const intent of intentRegistry.values()) {
7011
+ let confidence = 0;
7012
+ const nameLower = intent.name.toLowerCase();
7013
+ const descLower = intent.description.toLowerCase();
7014
+ if (nameLower === query) {
7015
+ confidence = 1;
7016
+ } else if (nameLower.includes(query) || query.includes(nameLower)) {
7017
+ confidence = 0.8;
7018
+ } else if (descLower.includes(query)) {
7019
+ confidence = 0.6;
7020
+ } else if (intent.tags?.some((t) => t.toLowerCase().includes(query))) {
7021
+ confidence = 0.5;
7022
+ }
7023
+ if (confidence > 0) {
7024
+ results.push({ intent, confidence });
7025
+ }
7026
+ }
7027
+ results.sort((a, b) => b.confidence - a.confidence);
7028
+ return success({ intents: results });
7029
+ } catch (err) {
7030
+ return error(err.message, "FIND_INTENT_ERROR");
7031
+ }
7032
+ },
7033
+ listIntents: async () => {
7034
+ try {
7035
+ return success(Array.from(intentRegistry.values()));
7036
+ } catch (err) {
7037
+ return error(err.message, "LIST_INTENTS_ERROR");
7038
+ }
7039
+ },
7040
+ registerIntent: async (intent) => {
7041
+ try {
7042
+ intentRegistry.set(intent.id, intent);
7043
+ return success(intent);
7044
+ } catch (err) {
7045
+ return error(err.message, "REGISTER_INTENT_ERROR");
7046
+ }
7047
+ },
7048
+ executeIntentFromQuery: async (request) => {
7049
+ const startTime = Date.now();
7050
+ try {
7051
+ refreshElements();
7052
+ const query = request.query.toLowerCase();
7053
+ let bestIntent = null;
7054
+ let bestConfidence = 0;
7055
+ for (const intent of intentRegistry.values()) {
7056
+ let confidence = 0;
7057
+ const nameLower = intent.name.toLowerCase();
7058
+ const descLower = intent.description.toLowerCase();
7059
+ if (nameLower === query) {
7060
+ confidence = 1;
7061
+ } else if (nameLower.includes(query) || query.includes(nameLower)) {
7062
+ confidence = 0.8;
7063
+ } else if (descLower.includes(query)) {
7064
+ confidence = 0.6;
7065
+ }
7066
+ if (confidence > bestConfidence) {
7067
+ bestConfidence = confidence;
7068
+ bestIntent = intent;
7069
+ }
7070
+ }
7071
+ if (!bestIntent) {
7072
+ return success({
7073
+ success: false,
7074
+ intentId: "",
7075
+ error: `No intent found matching query: ${request.query}`,
7076
+ durationMs: Date.now() - startTime
7077
+ });
7078
+ }
7079
+ const nlResponse = await nlExecutor.execute({
7080
+ instruction: bestIntent.description,
7081
+ context: `Executing intent from query: ${request.query}`
7082
+ });
7083
+ return success({
7084
+ success: nlResponse.success,
7085
+ intentId: bestIntent.id,
7086
+ result: nlResponse,
7087
+ error: nlResponse.error,
7088
+ durationMs: Date.now() - startTime
7089
+ });
7090
+ } catch (err) {
7091
+ return error(err.message, "EXECUTE_INTENT_FROM_QUERY_ERROR");
7092
+ }
7093
+ },
7094
+ // =========================================================================
7095
+ // Recovery Handler
7096
+ // =========================================================================
7097
+ attemptRecovery: async (request) => {
7098
+ const startTime = Date.now();
7099
+ try {
7100
+ refreshElements();
7101
+ const strategiesAttempted = [];
7102
+ let lastResult;
7103
+ const suggestions = request.failure.suggestedActions ?? [];
7104
+ for (let i = 0; i < Math.min(suggestions.length, request.maxRetries); i++) {
7105
+ const suggestion = suggestions[i];
7106
+ strategiesAttempted.push(suggestion.suggestion || `strategy-${i}`);
7107
+ const instruction = suggestion.command || request.instruction;
7108
+ try {
7109
+ const result = await nlExecutor.execute({
7110
+ instruction,
7111
+ context: `Recovery attempt ${i + 1}: ${suggestion.suggestion}`
7112
+ });
7113
+ lastResult = result;
7114
+ if (result.success) {
7115
+ return success({
7116
+ recovered: true,
7117
+ strategiesAttempted,
7118
+ finalResult: result,
7119
+ durationMs: Date.now() - startTime
7120
+ });
7121
+ }
7122
+ } catch {
7123
+ }
7124
+ }
7125
+ if (strategiesAttempted.length === 0 || !lastResult?.success) {
7126
+ strategiesAttempted.push("direct-instruction");
7127
+ try {
7128
+ const result = await nlExecutor.execute({
7129
+ instruction: request.instruction,
7130
+ context: "Recovery: direct instruction attempt"
7131
+ });
7132
+ lastResult = result;
7133
+ if (result.success) {
7134
+ return success({
7135
+ recovered: true,
7136
+ strategiesAttempted,
7137
+ finalResult: result,
7138
+ durationMs: Date.now() - startTime
7139
+ });
7140
+ }
7141
+ } catch {
7142
+ }
7143
+ }
7144
+ return success({
7145
+ recovered: false,
7146
+ strategiesAttempted,
7147
+ finalResult: lastResult,
7148
+ error: "All recovery strategies exhausted",
7149
+ durationMs: Date.now() - startTime
7150
+ });
7151
+ } catch (err) {
7152
+ return error(err.message, "RECOVERY_ERROR");
7153
+ }
7154
+ },
7155
+ // =========================================================================
7156
+ // Cross-App Analysis Handlers
7157
+ // =========================================================================
7158
+ analyzePageData: async () => {
7159
+ try {
7160
+ const controlSnapshot = registry.createSnapshot();
7161
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
7162
+ const result = extractPageData(snapshot.elements);
7163
+ return success(result);
7164
+ } catch (err) {
7165
+ return error(err.message, "ANALYZE_DATA_ERROR");
7166
+ }
7167
+ },
7168
+ analyzePageRegions: async () => {
7169
+ try {
7170
+ const controlSnapshot = registry.createSnapshot();
7171
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
7172
+ const result = segmentPageRegions(snapshot.elements);
7173
+ return success(result);
7174
+ } catch (err) {
7175
+ return error(err.message, "ANALYZE_REGIONS_ERROR");
7176
+ }
7177
+ },
7178
+ analyzeStructuredData: async () => {
7179
+ try {
7180
+ const controlSnapshot = registry.createSnapshot();
7181
+ const snapshot = snapshotManager.createSnapshot(controlSnapshot);
7182
+ const result = extractStructuredData(snapshot.elements);
7183
+ return success(result);
7184
+ } catch (err) {
7185
+ return error(err.message, "ANALYZE_STRUCTURED_DATA_ERROR");
7186
+ }
7187
+ },
7188
+ crossAppCompare: async (request) => {
7189
+ try {
7190
+ const hasComponents = request.sourceComponents && request.targetComponents;
7191
+ const report = generateComparisonReport(
7192
+ request.sourceSnapshot,
7193
+ request.targetSnapshot,
7194
+ hasComponents ? {
7195
+ config: { includeComponents: true },
7196
+ sourceComponents: request.sourceComponents,
7197
+ targetComponents: request.targetComponents
7198
+ } : void 0
7199
+ );
7200
+ return success(report);
7201
+ } catch (err) {
7202
+ return error(err.message, "CROSS_APP_COMPARE_ERROR");
7203
+ }
7204
+ },
7205
+ // =========================================================================
7206
+ // Performance Diagnostics Handlers
7207
+ // =========================================================================
7208
+ getPerformanceEntries: async () => {
7209
+ return {
7210
+ success: true,
7211
+ data: { navigation: null, resources: [], paint: [] },
7212
+ timestamp: Date.now()
7213
+ };
7214
+ },
7215
+ clearPerformanceEntries: async () => {
7216
+ return { success: true, data: { cleared: true }, timestamp: Date.now() };
7217
+ },
7218
+ getBrowserEvents: async (_params) => {
7219
+ return { success: true, data: { events: [], count: 0 }, timestamp: Date.now() };
4248
7220
  }
4249
7221
  };
4250
7222
  }
@@ -4268,7 +7240,12 @@ function createAIHandlers(registry, actionExecutor) {
4268
7240
  const response = searchEngine.search(criteria);
4269
7241
  return { success: true, data: response, timestamp: Date.now() };
4270
7242
  } catch (err) {
4271
- return { success: false, error: err.message, code: "AI_SEARCH_ERROR", timestamp: Date.now() };
7243
+ return {
7244
+ success: false,
7245
+ error: err.message,
7246
+ code: "AI_SEARCH_ERROR",
7247
+ timestamp: Date.now()
7248
+ };
4272
7249
  }
4273
7250
  },
4274
7251
  aiExecute: async (request) => {
@@ -4277,7 +7254,12 @@ function createAIHandlers(registry, actionExecutor) {
4277
7254
  const response = await nlExecutor.execute(request);
4278
7255
  return { success: true, data: response, timestamp: Date.now() };
4279
7256
  } catch (err) {
4280
- return { success: false, error: err.message, code: "AI_EXECUTE_ERROR", timestamp: Date.now() };
7257
+ return {
7258
+ success: false,
7259
+ error: err.message,
7260
+ code: "AI_EXECUTE_ERROR",
7261
+ timestamp: Date.now()
7262
+ };
4281
7263
  }
4282
7264
  },
4283
7265
  aiAssert: async (request) => {
@@ -4286,7 +7268,12 @@ function createAIHandlers(registry, actionExecutor) {
4286
7268
  const result = await assertionExecutor.assert(request);
4287
7269
  return { success: true, data: result, timestamp: Date.now() };
4288
7270
  } catch (err) {
4289
- return { success: false, error: err.message, code: "AI_ASSERT_ERROR", timestamp: Date.now() };
7271
+ return {
7272
+ success: false,
7273
+ error: err.message,
7274
+ code: "AI_ASSERT_ERROR",
7275
+ timestamp: Date.now()
7276
+ };
4290
7277
  }
4291
7278
  },
4292
7279
  aiAssertBatch: async (request) => {
@@ -4295,7 +7282,12 @@ function createAIHandlers(registry, actionExecutor) {
4295
7282
  const result = await assertionExecutor.assertBatch(request);
4296
7283
  return { success: true, data: result, timestamp: Date.now() };
4297
7284
  } catch (err) {
4298
- return { success: false, error: err.message, code: "AI_ASSERT_BATCH_ERROR", timestamp: Date.now() };
7285
+ return {
7286
+ success: false,
7287
+ error: err.message,
7288
+ code: "AI_ASSERT_BATCH_ERROR",
7289
+ timestamp: Date.now()
7290
+ };
4299
7291
  }
4300
7292
  },
4301
7293
  getSemanticSnapshot: async () => {
@@ -4304,7 +7296,12 @@ function createAIHandlers(registry, actionExecutor) {
4304
7296
  const snapshot = snapshotManager.createSnapshot(controlSnapshot);
4305
7297
  return { success: true, data: snapshot, timestamp: Date.now() };
4306
7298
  } catch (err) {
4307
- return { success: false, error: err.message, code: "SEMANTIC_SNAPSHOT_ERROR", timestamp: Date.now() };
7299
+ return {
7300
+ success: false,
7301
+ error: err.message,
7302
+ code: "SEMANTIC_SNAPSHOT_ERROR",
7303
+ timestamp: Date.now()
7304
+ };
4308
7305
  }
4309
7306
  },
4310
7307
  getSemanticDiff: async (_since) => {
@@ -4314,7 +7311,12 @@ function createAIHandlers(registry, actionExecutor) {
4314
7311
  const diff = diffManager.update(currentSnapshot);
4315
7312
  return { success: true, data: diff, timestamp: Date.now() };
4316
7313
  } catch (err) {
4317
- return { success: false, error: err.message, code: "SEMANTIC_DIFF_ERROR", timestamp: Date.now() };
7314
+ return {
7315
+ success: false,
7316
+ error: err.message,
7317
+ code: "SEMANTIC_DIFF_ERROR",
7318
+ timestamp: Date.now()
7319
+ };
4318
7320
  }
4319
7321
  },
4320
7322
  getPageSummary: async () => {
@@ -4332,7 +7334,12 @@ function createAIHandlers(registry, actionExecutor) {
4332
7334
  const summary = generatePageSummary(elements);
4333
7335
  return { success: true, data: summary, timestamp: Date.now() };
4334
7336
  } catch (err) {
4335
- return { success: false, error: err.message, code: "PAGE_SUMMARY_ERROR", timestamp: Date.now() };
7337
+ return {
7338
+ success: false,
7339
+ error: err.message,
7340
+ code: "PAGE_SUMMARY_ERROR",
7341
+ timestamp: Date.now()
7342
+ };
4336
7343
  }
4337
7344
  }
4338
7345
  };
@@ -4507,7 +7514,7 @@ function createNextRouteHandlers(handlers, config = {}) {
4507
7514
  args.push(params[param]);
4508
7515
  }
4509
7516
  }
4510
- if (method === "POST") {
7517
+ if (route.bodyRequired || method === "POST" || method === "PUT" || method === "PATCH") {
4511
7518
  try {
4512
7519
  const body = await request.json();
4513
7520
  args.push(body);
@@ -4533,6 +7540,7 @@ function createNextRouteHandlers(handlers, config = {}) {
4533
7540
  return {
4534
7541
  GET: handleRequest,
4535
7542
  POST: handleRequest,
7543
+ PUT: handleRequest,
4536
7544
  DELETE: handleRequest
4537
7545
  };
4538
7546
  }
@@ -5056,6 +8064,12 @@ var StandaloneServer = class {
5056
8064
  });
5057
8065
  }
5058
8066
  }
8067
+ /**
8068
+ * Get enabled capabilities based on handlers
8069
+ */
8070
+ getCapabilities() {
8071
+ return ["elements", "components", "actions", "search", "workflows"];
8072
+ }
5059
8073
  /**
5060
8074
  * Start the server
5061
8075
  */
@@ -5162,7 +8176,25 @@ var StandaloneServer = class {
5162
8176
  return;
5163
8177
  }
5164
8178
  if (url.pathname === "/health") {
5165
- this.sendJSON(res, { status: "ok", timestamp: Date.now() });
8179
+ const healthResponse = { status: "ok", timestamp: Date.now() };
8180
+ if (this.config.appInfo) {
8181
+ const uiBridge = {
8182
+ version: "0.3.0",
8183
+ ...this.config.appInfo,
8184
+ capabilities: this.getCapabilities()
8185
+ };
8186
+ try {
8187
+ const snapshotResult = await this.handlers.getControlSnapshot?.();
8188
+ if (snapshotResult?.success && snapshotResult.data) {
8189
+ const data = snapshotResult.data;
8190
+ uiBridge.elementCount = data.elements?.length ?? 0;
8191
+ uiBridge.componentCount = data.components?.length ?? 0;
8192
+ }
8193
+ } catch {
8194
+ }
8195
+ healthResponse.uiBridge = uiBridge;
8196
+ }
8197
+ this.sendJSON(res, healthResponse);
5166
8198
  return;
5167
8199
  }
5168
8200
  let path = url.pathname;
@@ -5176,7 +8208,7 @@ var StandaloneServer = class {
5176
8208
  }
5177
8209
  try {
5178
8210
  let body = {};
5179
- if (method === "POST") {
8211
+ if (method === "POST" || method === "PUT" || method === "PATCH" || route.bodyRequired) {
5180
8212
  body = await this.parseBody(req);
5181
8213
  }
5182
8214
  const params = this.extractParams(path, route.path);
@@ -5192,7 +8224,7 @@ var StandaloneServer = class {
5192
8224
  args.push(params[param]);
5193
8225
  }
5194
8226
  }
5195
- if (method === "POST") {
8227
+ if (route.bodyRequired || method === "POST" || method === "PUT" || method === "PATCH") {
5196
8228
  args.push(body);
5197
8229
  }
5198
8230
  if (method === "GET") {