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