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