@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.
- package/dist/ai/index.d.mts +312 -155
- package/dist/ai/index.d.ts +312 -155
- package/dist/ai/index.js +2363 -67
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/index.mjs +2328 -68
- package/dist/ai/index.mjs.map +1 -1
- package/dist/annotations/index.d.mts +218 -0
- package/dist/annotations/index.d.ts +218 -0
- package/dist/annotations/index.js +246 -0
- package/dist/annotations/index.js.map +1 -0
- package/dist/annotations/index.mjs +241 -0
- package/dist/annotations/index.mjs.map +1 -0
- package/dist/assertions-BSR3afVr.d.ts +161 -0
- package/dist/assertions-CTw1hfOx.d.mts +161 -0
- package/dist/babel-plugin/index.js +23 -34
- package/dist/babel-plugin/index.js.map +1 -1
- package/dist/babel-plugin/index.mjs +23 -34
- package/dist/babel-plugin/index.mjs.map +1 -1
- package/dist/browser-capture-Bms60T6f.d.mts +47 -0
- package/dist/browser-capture-CsTU29mb.d.ts +47 -0
- package/dist/control/index.d.mts +26 -7
- package/dist/control/index.d.ts +26 -7
- package/dist/control/index.js +276 -48
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +276 -48
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.d.mts +5 -3
- package/dist/debug/index.d.ts +5 -3
- package/dist/debug/index.js +925 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +924 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.mts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +4720 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4656 -174
- package/dist/index.mjs.map +1 -1
- package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
- package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
- package/dist/native/control/index.js +2 -7
- package/dist/native/control/index.js.map +1 -1
- package/dist/native/control/index.mjs +2 -7
- package/dist/native/control/index.mjs.map +1 -1
- package/dist/native/core/index.js.map +1 -1
- package/dist/native/core/index.mjs.map +1 -1
- package/dist/native/debug/index.js +23 -66
- package/dist/native/debug/index.js.map +1 -1
- package/dist/native/debug/index.mjs +23 -66
- package/dist/native/debug/index.mjs.map +1 -1
- package/dist/native/index.js +89 -131
- package/dist/native/index.js.map +1 -1
- package/dist/native/index.mjs +89 -131
- package/dist/native/index.mjs.map +1 -1
- package/dist/native/react/index.js +28 -52
- package/dist/native/react/index.js.map +1 -1
- package/dist/native/react/index.mjs +28 -52
- package/dist/native/react/index.mjs.map +1 -1
- package/dist/native/server/index.js +38 -13
- package/dist/native/server/index.js.map +1 -1
- package/dist/native/server/index.mjs +38 -13
- package/dist/native/server/index.mjs.map +1 -1
- package/dist/react/index.d.mts +107 -8
- package/dist/react/index.d.ts +107 -8
- package/dist/react/index.js +2194 -84
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +2194 -85
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
- package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
- package/dist/render-log/index.d.mts +1 -1
- package/dist/render-log/index.d.ts +1 -1
- package/dist/server/express.d.mts +5 -4
- package/dist/server/express.d.ts +5 -4
- package/dist/server/express.js +104 -2
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.mjs +104 -2
- package/dist/server/express.mjs.map +1 -1
- package/dist/server/handlers.d.mts +36 -5
- package/dist/server/handlers.d.ts +36 -5
- package/dist/server/handlers.js +3129 -224
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/handlers.mjs +3129 -224
- package/dist/server/handlers.mjs.map +1 -1
- package/dist/server/index.d.mts +7 -5
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.js +3215 -183
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +3215 -183
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/nextjs.d.mts +6 -4
- package/dist/server/nextjs.d.ts +6 -4
- package/dist/server/nextjs.js +106 -3
- package/dist/server/nextjs.js.map +1 -1
- package/dist/server/nextjs.mjs +106 -3
- package/dist/server/nextjs.mjs.map +1 -1
- package/dist/server/standalone.d.mts +6 -5
- package/dist/server/standalone.d.ts +6 -5
- package/dist/server/standalone.js +131 -5
- package/dist/server/standalone.js.map +1 -1
- package/dist/server/standalone.mjs +131 -5
- package/dist/server/standalone.mjs.map +1 -1
- package/dist/specs/index.d.mts +365 -0
- package/dist/specs/index.d.ts +365 -0
- package/dist/specs/index.js +2809 -0
- package/dist/specs/index.js.map +1 -0
- package/dist/specs/index.mjs +2786 -0
- package/dist/specs/index.mjs.map +1 -0
- package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
- package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
- package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
- package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
- package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
- package/dist/types-CebMQj76.d.ts +1275 -0
- package/dist/types-D_ypYl3T.d.mts +1275 -0
- package/dist/types-UBtp7R0u.d.mts +132 -0
- package/dist/types-UBtp7R0u.d.ts +132 -0
- package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
- package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
- package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
- package/package.json +18 -2
- package/dist/types-B5Q0GVo0.d.mts +0 -646
- package/dist/types-DfPqwU-i.d.ts +0 -646
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
-
{
|
|
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 || ""] || [
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
900
|
-
|
|
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(
|
|
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)
|
|
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)
|
|
1189
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
3181
|
+
return this.assertVisibility(
|
|
3182
|
+
element,
|
|
3183
|
+
true,
|
|
3184
|
+
elementDescription,
|
|
3185
|
+
request.message,
|
|
3186
|
+
startTime
|
|
3187
|
+
);
|
|
2664
3188
|
case "hidden":
|
|
2665
|
-
return this.assertVisibility(
|
|
3189
|
+
return this.assertVisibility(
|
|
3190
|
+
element,
|
|
3191
|
+
false,
|
|
3192
|
+
elementDescription,
|
|
3193
|
+
request.message,
|
|
3194
|
+
startTime
|
|
3195
|
+
);
|
|
2666
3196
|
case "enabled":
|
|
2667
|
-
return this.assertEnabledState(
|
|
3197
|
+
return this.assertEnabledState(
|
|
3198
|
+
element,
|
|
3199
|
+
true,
|
|
3200
|
+
elementDescription,
|
|
3201
|
+
request.message,
|
|
3202
|
+
startTime
|
|
3203
|
+
);
|
|
2668
3204
|
case "disabled":
|
|
2669
|
-
return this.assertEnabledState(
|
|
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(
|
|
3215
|
+
return this.assertCheckedState(
|
|
3216
|
+
element,
|
|
3217
|
+
true,
|
|
3218
|
+
elementDescription,
|
|
3219
|
+
request.message,
|
|
3220
|
+
startTime
|
|
3221
|
+
);
|
|
2674
3222
|
case "unchecked":
|
|
2675
|
-
return this.assertCheckedState(
|
|
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
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
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
|
-
|
|
3628
|
-
|
|
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
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
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
|
-
{
|
|
3648
|
-
|
|
3649
|
-
|
|
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
|
-
{
|
|
3654
|
-
|
|
3655
|
-
|
|
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
|
-
{
|
|
3661
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
3678
|
-
|
|
3679
|
-
|
|
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
|
-
{
|
|
3684
|
-
|
|
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
|
-
{
|
|
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(
|
|
3785
|
-
|
|
3786
|
-
|
|
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(
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
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(
|
|
3857
|
-
|
|
3858
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
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, "
|
|
6694
|
+
return error(err.message, "ANNOTATION_DELETE_ERROR");
|
|
4101
6695
|
}
|
|
4102
6696
|
},
|
|
4103
|
-
|
|
6697
|
+
importAnnotations: async (config2) => {
|
|
4104
6698
|
try {
|
|
4105
|
-
const
|
|
4106
|
-
|
|
4107
|
-
return success(snapshot);
|
|
6699
|
+
const count = annotationStore.importConfig(config2);
|
|
6700
|
+
return success({ count });
|
|
4108
6701
|
} catch (err) {
|
|
4109
|
-
return error(err.message, "
|
|
6702
|
+
return error(err.message, "ANNOTATION_IMPORT_ERROR");
|
|
4110
6703
|
}
|
|
4111
6704
|
},
|
|
4112
|
-
|
|
6705
|
+
exportAnnotations: async () => {
|
|
4113
6706
|
try {
|
|
4114
|
-
|
|
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, "
|
|
6709
|
+
return error(err.message, "ANNOTATION_EXPORT_ERROR");
|
|
4120
6710
|
}
|
|
4121
6711
|
},
|
|
4122
|
-
|
|
6712
|
+
getAnnotationCoverage: async () => {
|
|
4123
6713
|
try {
|
|
4124
|
-
const
|
|
4125
|
-
const
|
|
4126
|
-
|
|
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, "
|
|
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(
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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") {
|