codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3
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/CHANGELOG.md +15 -0
- package/README.md +27 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +155 -1
- package/contracts/v1/parity-suite.json +96 -1
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
- package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- package/dist/src/browser/downloads.d.ts +2 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.js +1222 -59
- package/dist/src/commands/configuration.js +13 -7
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +26 -3
- package/dist/src/commands/experience.js +35 -0
- package/dist/src/commands/files.js +4 -27
- package/dist/src/commands/modes.js +49 -17
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +627 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +7 -2
- package/dist/src/commands/work.js +31 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +161 -0
- package/dist/src/operations/browser-adapter.js +2023 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1208 -0
- package/dist/src/operations/browser-target.d.ts +103 -0
- package/dist/src/operations/browser-target.js +489 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1150 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1143 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +22 -0
- package/dist/src/operations/index.js +22 -0
- package/dist/src/operations/journal.d.ts +142 -0
- package/dist/src/operations/journal.js +1931 -0
- package/dist/src/operations/production-attachments.d.ts +116 -0
- package/dist/src/operations/production-attachments.js +1018 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1363 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +1073 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +118 -0
- package/dist/src/operations/runtime-adapter.js +786 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +281 -0
- package/dist/src/operations/service.js +2810 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +39 -0
- package/dist/src/runtime/coordinated-page.js +634 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
- package/dist/src/scripts/live-smoke/scenarios.js +110 -18
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +1 -1
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +73 -0
- package/references/python-parity.md +72 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { normalizeForLabelMatch } from "../dom/label-match.js";
|
|
2
|
+
/**
|
|
3
|
+
* The upper bound is deliberately conservative. A Power selector is expected
|
|
4
|
+
* to expose a small, discrete set of reasoning levels; accepting an enormous
|
|
5
|
+
* range would turn an unverified selector into a long mutation loop and would
|
|
6
|
+
* make an aria typo surprisingly expensive.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_POWER_LEVELS = 32;
|
|
9
|
+
export const MAX_POWER_SLIDERS = 32;
|
|
10
|
+
/** Maximum browser-realm nodes inspected by one Power discovery probe. */
|
|
11
|
+
export const MAX_POWER_DOM_NODES = 4096;
|
|
12
|
+
const MAX_POWER_LABELS = 64;
|
|
13
|
+
const MAX_POWER_LABEL_LENGTH = 240;
|
|
14
|
+
const MAX_POWER_TEXT_CHARS = 32 * 1024;
|
|
15
|
+
// There is deliberately no English fallback. Callers must provide the locale
|
|
16
|
+
// pack observed for the current surface; otherwise an accidental English
|
|
17
|
+
// match could select a control in a localized session.
|
|
18
|
+
const DEFAULT_POWER_LABELS = [];
|
|
19
|
+
/**
|
|
20
|
+
* Inspect the visible DOM once. This function is intentionally read-only: it
|
|
21
|
+
* never focuses, presses, clicks, hovers, waits, or writes to the page.
|
|
22
|
+
*/
|
|
23
|
+
export async function discoverPowerSlider(page, options = {}) {
|
|
24
|
+
if (typeof page.evaluate !== "function") {
|
|
25
|
+
return classifyPowerSliderObservation({ sliders: [] }, options);
|
|
26
|
+
}
|
|
27
|
+
const observation = await page.evaluate((config) => {
|
|
28
|
+
// This callback is serialized into the browser realm. Keep the probe
|
|
29
|
+
// independent from module scope and use one SHOW_ALL traversal: text and
|
|
30
|
+
// comment nodes consume the same global budget as elements. In
|
|
31
|
+
// particular, do not replace this with a selector query or a DOM text
|
|
32
|
+
// property; either would materialize arbitrary page-sized data before the
|
|
33
|
+
// Power-specific caps can take effect.
|
|
34
|
+
const maxLabelLength = 240;
|
|
35
|
+
const maxLabelInput = maxLabelLength * 4;
|
|
36
|
+
const elements = [];
|
|
37
|
+
const sliderElements = [];
|
|
38
|
+
const textByElement = new Map();
|
|
39
|
+
let visitedNodes = 0;
|
|
40
|
+
let textChars = 0;
|
|
41
|
+
let textTruncated = false;
|
|
42
|
+
const normalize = (value) => {
|
|
43
|
+
const limit = Math.min(value.length, maxLabelInput);
|
|
44
|
+
let normalized = "";
|
|
45
|
+
let pendingSpace = false;
|
|
46
|
+
for (let index = 0; index < limit && normalized.length < maxLabelLength; index += 1) {
|
|
47
|
+
const character = value[index];
|
|
48
|
+
if (/\s/u.test(character)) {
|
|
49
|
+
if (normalized.length > 0)
|
|
50
|
+
pendingSpace = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (pendingSpace && normalized.length < maxLabelLength)
|
|
54
|
+
normalized += " ";
|
|
55
|
+
pendingSpace = false;
|
|
56
|
+
normalized += character;
|
|
57
|
+
}
|
|
58
|
+
if (value.length > limit)
|
|
59
|
+
textTruncated = true;
|
|
60
|
+
return normalized.trim().slice(0, maxLabelLength);
|
|
61
|
+
};
|
|
62
|
+
const appendText = (node) => {
|
|
63
|
+
if (node.nodeType !== 3)
|
|
64
|
+
return;
|
|
65
|
+
const raw = typeof node.nodeValue === "string" ? node.nodeValue : "";
|
|
66
|
+
if (raw.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
const remaining = config.maxTextChars - textChars;
|
|
69
|
+
if (remaining <= 0) {
|
|
70
|
+
textTruncated = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const piece = raw.slice(0, Math.min(raw.length, remaining));
|
|
74
|
+
textChars += piece.length;
|
|
75
|
+
if (piece.length < raw.length)
|
|
76
|
+
textTruncated = true;
|
|
77
|
+
let parent = node.parentNode;
|
|
78
|
+
let depth = 0;
|
|
79
|
+
while (parent !== null && depth < 64) {
|
|
80
|
+
if (parent.nodeType === 1) {
|
|
81
|
+
const element = parent;
|
|
82
|
+
const previous = textByElement.get(element) ?? "";
|
|
83
|
+
if (previous.length < maxLabelLength) {
|
|
84
|
+
textByElement.set(element, `${previous}${piece.slice(0, maxLabelLength - previous.length)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
parent = parent.parentNode;
|
|
88
|
+
depth += 1;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const visit = (node) => {
|
|
92
|
+
visitedNodes += 1;
|
|
93
|
+
if (visitedNodes > config.maxNodes)
|
|
94
|
+
throw new Error("node limit exceeded");
|
|
95
|
+
appendText(node);
|
|
96
|
+
if (node.nodeType === 1) {
|
|
97
|
+
const element = node;
|
|
98
|
+
elements.push(element);
|
|
99
|
+
if (element.getAttribute("role") === "slider") {
|
|
100
|
+
if (sliderElements.length >= config.maxSliders)
|
|
101
|
+
throw new Error("slider limit exceeded");
|
|
102
|
+
sliderElements.push(element);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const ownerDocument = document;
|
|
107
|
+
try {
|
|
108
|
+
if (typeof ownerDocument.createTreeWalker === "function") {
|
|
109
|
+
const walker = ownerDocument.createTreeWalker(ownerDocument, 0xffffffff);
|
|
110
|
+
let current = walker.nextNode();
|
|
111
|
+
while (current !== null) {
|
|
112
|
+
visit(current);
|
|
113
|
+
current = walker.nextNode();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// The manual path keeps deterministic behavior in minimal browser
|
|
118
|
+
// adapters and tests which expose the DOM node links but no TreeWalker.
|
|
119
|
+
let current = ownerDocument.firstChild;
|
|
120
|
+
while (current !== null) {
|
|
121
|
+
visit(current);
|
|
122
|
+
if (current.firstChild !== null) {
|
|
123
|
+
current = current.firstChild;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
while (current !== null && current !== ownerDocument && current.nextSibling === null) {
|
|
127
|
+
current = current.parentNode;
|
|
128
|
+
}
|
|
129
|
+
if (current === ownerDocument || current === null)
|
|
130
|
+
break;
|
|
131
|
+
current = current.nextSibling;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof Error
|
|
137
|
+
&& (error.message === "node limit exceeded" || error.message === "slider limit exceeded")) {
|
|
138
|
+
return { sliders: [], slidersTruncated: true };
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const idIndex = new Map();
|
|
143
|
+
for (const element of elements) {
|
|
144
|
+
const id = element.getAttribute("id");
|
|
145
|
+
if (id !== null && id.length > 0 && !idIndex.has(id))
|
|
146
|
+
idIndex.set(id, element);
|
|
147
|
+
}
|
|
148
|
+
const textOf = (element) => {
|
|
149
|
+
if (element === null || element === undefined)
|
|
150
|
+
return "";
|
|
151
|
+
const label = element.getAttribute("aria-label");
|
|
152
|
+
if (label !== null)
|
|
153
|
+
return normalize(label);
|
|
154
|
+
return normalize(textByElement.get(element) ?? "");
|
|
155
|
+
};
|
|
156
|
+
const visibleTextOf = (element) => {
|
|
157
|
+
if (element === null || element === undefined)
|
|
158
|
+
return "";
|
|
159
|
+
return normalize(textByElement.get(element) ?? "");
|
|
160
|
+
};
|
|
161
|
+
const matchesPowerLabel = (value) => {
|
|
162
|
+
const normalized = normalize(value).toLocaleLowerCase();
|
|
163
|
+
return config.powerLabels.some(label => {
|
|
164
|
+
const wanted = normalize(label).toLocaleLowerCase();
|
|
165
|
+
if (wanted.length === 0)
|
|
166
|
+
return false;
|
|
167
|
+
return normalized === wanted
|
|
168
|
+
|| normalized.startsWith(`${wanted} `)
|
|
169
|
+
|| normalized.endsWith(` ${wanted}`)
|
|
170
|
+
|| normalized.includes(` ${wanted} `);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const isVisible = (element) => {
|
|
174
|
+
let current = element;
|
|
175
|
+
let depth = 0;
|
|
176
|
+
while (current !== null && depth < 16) {
|
|
177
|
+
if (current.nodeType !== 1)
|
|
178
|
+
break;
|
|
179
|
+
const currentElement = current;
|
|
180
|
+
const html = currentElement;
|
|
181
|
+
if (html.hidden
|
|
182
|
+
|| currentElement.getAttribute("aria-hidden") === "true"
|
|
183
|
+
|| currentElement.hasAttribute("inert"))
|
|
184
|
+
return false;
|
|
185
|
+
const style = typeof window !== "undefined"
|
|
186
|
+
? window.getComputedStyle?.(html)
|
|
187
|
+
: undefined;
|
|
188
|
+
if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
current = current.parentNode;
|
|
192
|
+
depth += 1;
|
|
193
|
+
}
|
|
194
|
+
const rect = element.getBoundingClientRect?.();
|
|
195
|
+
return rect === undefined || (rect.width > 0 && rect.height > 0);
|
|
196
|
+
};
|
|
197
|
+
const labelledByText = (element) => {
|
|
198
|
+
const ids = (element.getAttribute("aria-labelledby") ?? "")
|
|
199
|
+
.slice(0, 512)
|
|
200
|
+
.split(/\s+/)
|
|
201
|
+
.map(id => id.trim())
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
return normalize(ids
|
|
204
|
+
.map(id => textOf(idIndex.get(id)))
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.join(" "))
|
|
207
|
+
.slice(0, 240);
|
|
208
|
+
};
|
|
209
|
+
const directSurfaceHint = (element) => {
|
|
210
|
+
const values = [];
|
|
211
|
+
let current = element;
|
|
212
|
+
let depth = 0;
|
|
213
|
+
while (current !== null && depth < 8) {
|
|
214
|
+
if (current.nodeType !== 1)
|
|
215
|
+
break;
|
|
216
|
+
const currentElement = current;
|
|
217
|
+
for (const attribute of ["data-surface", "data-experience", "data-testid", "id", "class"]) {
|
|
218
|
+
const value = currentElement.getAttribute(attribute);
|
|
219
|
+
if (value !== null)
|
|
220
|
+
values.push(value.slice(0, 240));
|
|
221
|
+
}
|
|
222
|
+
current = current.parentNode;
|
|
223
|
+
depth += 1;
|
|
224
|
+
}
|
|
225
|
+
const joined = values.join(" ").toLocaleLowerCase();
|
|
226
|
+
const hasWork = /(?:^|[-_\s])work(?:$|[-_\s])/.test(joined);
|
|
227
|
+
const hasChat = /(?:^|[-_\s])chat(?:$|[-_\s])/.test(joined);
|
|
228
|
+
const experience = hasWork === hasChat
|
|
229
|
+
? undefined
|
|
230
|
+
: hasWork ? "work" : "chat";
|
|
231
|
+
const selectorProfile = /advanced/.test(joined)
|
|
232
|
+
? experience === "work" ? "work_advanced_v1" : "unknown"
|
|
233
|
+
: experience === "work" ? "work_basic_v1"
|
|
234
|
+
: experience === "chat" ? "chat_simplified_v1" : undefined;
|
|
235
|
+
return experience === undefined && selectorProfile === undefined
|
|
236
|
+
? {}
|
|
237
|
+
: {
|
|
238
|
+
...(experience === undefined ? {} : { experience }),
|
|
239
|
+
...(selectorProfile === undefined ? {} : { selectorProfile })
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const isWithin = (element, root) => {
|
|
243
|
+
let current = element;
|
|
244
|
+
let depth = 0;
|
|
245
|
+
while (current !== null && depth < 64) {
|
|
246
|
+
if (current === root)
|
|
247
|
+
return true;
|
|
248
|
+
current = current.parentNode;
|
|
249
|
+
depth += 1;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
};
|
|
253
|
+
const hasDatalistAncestor = (element) => {
|
|
254
|
+
let current = element;
|
|
255
|
+
let depth = 0;
|
|
256
|
+
while (current !== null && depth < 64) {
|
|
257
|
+
if (current.nodeType === 1 && current.tagName?.toLocaleLowerCase() === "datalist")
|
|
258
|
+
return true;
|
|
259
|
+
current = current.parentNode;
|
|
260
|
+
depth += 1;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
};
|
|
264
|
+
const readOptions = (root) => {
|
|
265
|
+
if (root === null)
|
|
266
|
+
return { options: [], truncated: false };
|
|
267
|
+
const mapped = [];
|
|
268
|
+
let matched = 0;
|
|
269
|
+
let truncated = false;
|
|
270
|
+
const rootIsDatalist = root.tagName?.toLocaleLowerCase() === "datalist";
|
|
271
|
+
for (const node of elements) {
|
|
272
|
+
if (node === root || !isWithin(node, root))
|
|
273
|
+
continue;
|
|
274
|
+
const role = node.getAttribute("role");
|
|
275
|
+
const tagName = node.tagName?.toLocaleLowerCase();
|
|
276
|
+
const isOption = role === "option"
|
|
277
|
+
|| role === "menuitemradio"
|
|
278
|
+
|| role === "radio"
|
|
279
|
+
|| node.getAttribute("data-power-value") !== null
|
|
280
|
+
|| (tagName === "option" && (rootIsDatalist || hasDatalistAncestor(node)));
|
|
281
|
+
if (!isOption)
|
|
282
|
+
continue;
|
|
283
|
+
matched += 1;
|
|
284
|
+
if (matched > config.maxOptions) {
|
|
285
|
+
truncated = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
const value = node.getAttribute("aria-valuenow")
|
|
289
|
+
?? node.getAttribute("data-power-value")
|
|
290
|
+
?? node.getAttribute("data-value")
|
|
291
|
+
?? node.getAttribute("value")
|
|
292
|
+
?? undefined;
|
|
293
|
+
const logicalDatalistOption = rootIsDatalist || hasDatalistAncestor(node);
|
|
294
|
+
mapped.push({
|
|
295
|
+
label: textOf(node),
|
|
296
|
+
...(value === undefined ? {} : { value }),
|
|
297
|
+
// Datalist options are intentionally not rendered, but they are the
|
|
298
|
+
// semantic value map for the associated slider. Treating their
|
|
299
|
+
// hidden presentation as a missing map would force an unnecessary
|
|
300
|
+
// probe while still keeping ordinary hidden controls fail-closed.
|
|
301
|
+
visible: logicalDatalistOption || isVisible(node)
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
options: mapped.filter(option => option.label.length > 0),
|
|
306
|
+
truncated
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const nearestOwner = (slider) => {
|
|
310
|
+
let current = slider.parentNode;
|
|
311
|
+
let depth = 0;
|
|
312
|
+
while (current !== null && depth < 8) {
|
|
313
|
+
if (current.nodeType !== 1)
|
|
314
|
+
break;
|
|
315
|
+
const element = current;
|
|
316
|
+
const role = element.getAttribute("role");
|
|
317
|
+
if (role === "menuitem"
|
|
318
|
+
|| role === "menuitemradio"
|
|
319
|
+
|| role === "option"
|
|
320
|
+
|| role === "radio"
|
|
321
|
+
|| role === "group"
|
|
322
|
+
|| role === "row")
|
|
323
|
+
return element;
|
|
324
|
+
current = current.parentNode;
|
|
325
|
+
depth += 1;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
};
|
|
329
|
+
const nearestMenu = (slider) => {
|
|
330
|
+
let current = slider.parentNode;
|
|
331
|
+
let depth = 0;
|
|
332
|
+
while (current !== null && depth < 16) {
|
|
333
|
+
if (current.nodeType !== 1)
|
|
334
|
+
break;
|
|
335
|
+
const element = current;
|
|
336
|
+
const role = element.getAttribute("role");
|
|
337
|
+
if (role === "menu"
|
|
338
|
+
|| role === "listbox"
|
|
339
|
+
|| element.getAttribute("data-radix-popper-content-wrapper") !== null
|
|
340
|
+
|| element.getAttribute("data-radix-menu-content") !== null)
|
|
341
|
+
return element;
|
|
342
|
+
current = current.parentNode;
|
|
343
|
+
depth += 1;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
};
|
|
347
|
+
const sliders = sliderElements.map((slider, index) => {
|
|
348
|
+
const owner = nearestOwner(slider);
|
|
349
|
+
const menu = nearestMenu(slider);
|
|
350
|
+
const listId = slider.getAttribute("list");
|
|
351
|
+
const datalist = listId === null ? null : idIndex.get(listId) ?? null;
|
|
352
|
+
// A whole menu is not an option map: it can contain model, speed, and
|
|
353
|
+
// effort rows alongside Power. Only use it when the menu itself is
|
|
354
|
+
// explicitly labelled as the Power axis; otherwise require an owning
|
|
355
|
+
// semantic group (or an associated datalist).
|
|
356
|
+
const menuLabel = menu?.getAttribute("aria-label") ?? "";
|
|
357
|
+
const powerMenu = menu !== null && matchesPowerLabel(menuLabel) ? menu : null;
|
|
358
|
+
const optionRoot = datalist ?? owner ?? powerMenu;
|
|
359
|
+
const optionSource = datalist !== null
|
|
360
|
+
? "datalist"
|
|
361
|
+
: owner !== null
|
|
362
|
+
? "owner"
|
|
363
|
+
: powerMenu !== null ? "power_menu" : undefined;
|
|
364
|
+
const { options, truncated: optionsTruncated } = readOptions(optionRoot);
|
|
365
|
+
return {
|
|
366
|
+
index,
|
|
367
|
+
visible: isVisible(slider),
|
|
368
|
+
...(textOf(slider).length === 0 ? {} : { ariaLabel: textOf(slider) }),
|
|
369
|
+
...(labelledByText(slider).length === 0 ? {} : { labelledByText: labelledByText(slider) }),
|
|
370
|
+
...(slider.getAttribute("aria-valuetext") === null
|
|
371
|
+
? {}
|
|
372
|
+
: { valueText: normalize(slider.getAttribute("aria-valuetext") ?? "") }),
|
|
373
|
+
...(slider.getAttribute("aria-valuemin") === null
|
|
374
|
+
? {}
|
|
375
|
+
: { minimum: slider.getAttribute("aria-valuemin") }),
|
|
376
|
+
...(slider.getAttribute("aria-valuemax") === null
|
|
377
|
+
? {}
|
|
378
|
+
: { maximum: slider.getAttribute("aria-valuemax") }),
|
|
379
|
+
...(slider.getAttribute("aria-valuenow") === null
|
|
380
|
+
? {}
|
|
381
|
+
: { current: slider.getAttribute("aria-valuenow") }),
|
|
382
|
+
...(slider.getAttribute("aria-valuestep") === null
|
|
383
|
+
? {}
|
|
384
|
+
: { step: slider.getAttribute("aria-valuestep") }),
|
|
385
|
+
...(owner === null ? {} : {
|
|
386
|
+
owner: {
|
|
387
|
+
...(owner.getAttribute("role") === null ? {} : { role: owner.getAttribute("role") }),
|
|
388
|
+
...(owner.getAttribute("aria-label") === null ? {} : { label: owner.getAttribute("aria-label") }),
|
|
389
|
+
text: visibleTextOf(owner),
|
|
390
|
+
visible: isVisible(owner)
|
|
391
|
+
}
|
|
392
|
+
}),
|
|
393
|
+
...(menu === null ? {} : {
|
|
394
|
+
menu: {
|
|
395
|
+
role: menu.getAttribute("role") ?? "overlay",
|
|
396
|
+
...(menu.getAttribute("aria-label") === null ? {} : { label: menu.getAttribute("aria-label") }),
|
|
397
|
+
text: visibleTextOf(menu),
|
|
398
|
+
visible: isVisible(menu)
|
|
399
|
+
}
|
|
400
|
+
}),
|
|
401
|
+
surface: directSurfaceHint(slider),
|
|
402
|
+
...(options.length === 0 ? {} : { options }),
|
|
403
|
+
...(optionSource === undefined ? {} : { optionSource }),
|
|
404
|
+
...(optionsTruncated ? { optionsTruncated: true } : {})
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
if (textTruncated) {
|
|
408
|
+
return { sliders: [], slidersTruncated: true };
|
|
409
|
+
}
|
|
410
|
+
return { sliders };
|
|
411
|
+
}, {
|
|
412
|
+
powerLabels: [...(options.powerLabels ?? DEFAULT_POWER_LABELS)].slice(0, MAX_POWER_LABELS + 1),
|
|
413
|
+
maxSliders: MAX_POWER_SLIDERS,
|
|
414
|
+
maxOptions: MAX_POWER_LEVELS,
|
|
415
|
+
maxNodes: MAX_POWER_DOM_NODES,
|
|
416
|
+
maxTextChars: MAX_POWER_TEXT_CHARS
|
|
417
|
+
}).catch(() => ({ sliders: [] }));
|
|
418
|
+
return classifyPowerSliderObservation(observation, options);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Classify one bounded, read-only DOM observation. No state is retained; each
|
|
422
|
+
* call starts from the supplied snapshot and therefore cannot leak a previous
|
|
423
|
+
* operation's discovered range or value mapping.
|
|
424
|
+
*/
|
|
425
|
+
export function classifyPowerSliderObservation(observation, options = {}) {
|
|
426
|
+
const suppliedPowerLabels = [...(options.powerLabels ?? DEFAULT_POWER_LABELS)];
|
|
427
|
+
const limitExceeded = observation.slidersTruncated === true
|
|
428
|
+
|| observation.sliders.length > MAX_POWER_SLIDERS
|
|
429
|
+
|| suppliedPowerLabels.length > MAX_POWER_LABELS
|
|
430
|
+
|| observation.sliders.some(slider => slider.optionsTruncated === true
|
|
431
|
+
|| (slider.options?.length ?? 0) > MAX_POWER_LEVELS);
|
|
432
|
+
if (limitExceeded) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: "observation_limit_exceeded",
|
|
436
|
+
evidence: {
|
|
437
|
+
visibleSliderCount: 0,
|
|
438
|
+
semanticSliderCount: 0,
|
|
439
|
+
invalidSemanticSliderCount: 0,
|
|
440
|
+
hiddenSliderCount: 0,
|
|
441
|
+
observedProfiles: [],
|
|
442
|
+
observationTruncated: true
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const powerLabels = suppliedPowerLabels.filter(label => label.trim().length > 0
|
|
447
|
+
&& label.length <= MAX_POWER_LABEL_LENGTH);
|
|
448
|
+
const visible = observation.sliders.filter(slider => slider.visible);
|
|
449
|
+
const semantic = visible.filter(slider => semanticRelationship(slider, powerLabels) !== undefined);
|
|
450
|
+
const hiddenCount = observation.sliders.length - visible.length;
|
|
451
|
+
const observedProfiles = [...new Set(visible
|
|
452
|
+
.map(slider => slider.surface?.selectorProfile ?? slider.surface?.experience)
|
|
453
|
+
.filter((value) => value !== undefined))];
|
|
454
|
+
const failureEvidence = (invalidSemanticSliderCount = 0) => ({
|
|
455
|
+
visibleSliderCount: visible.length,
|
|
456
|
+
semanticSliderCount: semantic.length,
|
|
457
|
+
invalidSemanticSliderCount,
|
|
458
|
+
hiddenSliderCount: hiddenCount,
|
|
459
|
+
observedProfiles,
|
|
460
|
+
observationTruncated: false
|
|
461
|
+
});
|
|
462
|
+
if (visible.length === 0) {
|
|
463
|
+
return { ok: false, reason: "no_visible_slider", evidence: failureEvidence() };
|
|
464
|
+
}
|
|
465
|
+
if (semantic.length === 0) {
|
|
466
|
+
return { ok: false, reason: "no_semantic_power_slider", evidence: failureEvidence() };
|
|
467
|
+
}
|
|
468
|
+
if (semantic.length > 1) {
|
|
469
|
+
return { ok: false, reason: "ambiguous_power_slider", evidence: failureEvidence() };
|
|
470
|
+
}
|
|
471
|
+
const candidate = semantic[0];
|
|
472
|
+
const relationship = semanticRelationship(candidate, powerLabels);
|
|
473
|
+
if (relationship === undefined || candidate.menu?.visible !== true) {
|
|
474
|
+
return { ok: false, reason: "missing_menu_relationship", evidence: failureEvidence() };
|
|
475
|
+
}
|
|
476
|
+
if (options.expectedSurface !== undefined
|
|
477
|
+
&& candidate.surface?.experience !== options.expectedSurface) {
|
|
478
|
+
return { ok: false, reason: "surface_mismatch", evidence: failureEvidence() };
|
|
479
|
+
}
|
|
480
|
+
const minimum = parseInteger(candidate.minimum);
|
|
481
|
+
const maximum = parseInteger(candidate.maximum);
|
|
482
|
+
const current = parseInteger(candidate.current);
|
|
483
|
+
const step = candidate.step === undefined ? 1 : parseInteger(candidate.step);
|
|
484
|
+
if (minimum === undefined || maximum === undefined || current === undefined
|
|
485
|
+
|| step === undefined || step !== 1 || minimum >= maximum || current < minimum || current > maximum) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
reason: "invalid_aria_range",
|
|
489
|
+
evidence: failureEvidence(1)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const count = maximum - minimum + 1;
|
|
493
|
+
if (count > MAX_POWER_LEVELS || count < 2) {
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
reason: "unsupported_range",
|
|
497
|
+
evidence: failureEvidence()
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const optionsEvidence = optionEvidence(candidate.optionSource === undefined ? [] : candidate.options ?? [], minimum, maximum, count);
|
|
501
|
+
const range = { minimum, maximum, current, count };
|
|
502
|
+
const evidence = {
|
|
503
|
+
role: "slider",
|
|
504
|
+
sliderIndex: candidate.index,
|
|
505
|
+
relationship,
|
|
506
|
+
matchedPowerLabels: matchedPowerLabels(candidate, powerLabels),
|
|
507
|
+
surface: candidate.surface?.experience ?? "unknown",
|
|
508
|
+
selectorProfile: candidate.surface?.selectorProfile ?? "unknown",
|
|
509
|
+
menuRole: candidate.menu.role,
|
|
510
|
+
...(candidate.owner?.role === undefined ? {} : { ownerRole: candidate.owner.role }),
|
|
511
|
+
...(candidate.valueText === undefined ? {} : { valueText: candidate.valueText }),
|
|
512
|
+
options: optionsEvidence,
|
|
513
|
+
range
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
evidence,
|
|
518
|
+
sliderIndex: candidate.index,
|
|
519
|
+
range,
|
|
520
|
+
options: optionsEvidence,
|
|
521
|
+
...(candidate.valueText === undefined ? {} : { valueText: candidate.valueText })
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Resolve a visible requested label to a numeric value only when the DOM gave
|
|
526
|
+
* us a complete semantic mapping. A current aria-valuetext is enough to
|
|
527
|
+
* recognize that no mutation is required; it is not enough to guess another
|
|
528
|
+
* level. This is the important boundary that keeps ordinary inspection and
|
|
529
|
+
* unprobed slider ranges fail-closed.
|
|
530
|
+
*/
|
|
531
|
+
export function resolvePowerTarget(discovery, requestedLabels) {
|
|
532
|
+
if (requestedLabels.length > MAX_POWER_LABELS
|
|
533
|
+
|| requestedLabels.some(label => label.length > MAX_POWER_LABEL_LENGTH))
|
|
534
|
+
return undefined;
|
|
535
|
+
const matches = discovery.options.filter(option => requestedLabels.some(label => labelsMatch(option.label, label)));
|
|
536
|
+
const values = [...new Set(matches.map(option => option.value).filter((value) => value !== undefined))];
|
|
537
|
+
if (matches.length > 0 && matches.length === values.length && values.length === 1) {
|
|
538
|
+
return values[0];
|
|
539
|
+
}
|
|
540
|
+
if (discovery.valueText !== undefined && requestedLabels.some(label => labelsMatch(discovery.valueText, label))) {
|
|
541
|
+
return discovery.range.current;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
/** Return a locator for the observed slider without broadening the selector. */
|
|
546
|
+
export function observedPowerSlider(page, discovery) {
|
|
547
|
+
const all = page.locator?.("[role='slider']");
|
|
548
|
+
if (all === undefined)
|
|
549
|
+
return undefined;
|
|
550
|
+
if (all.nth !== undefined)
|
|
551
|
+
return all.nth(discovery.sliderIndex);
|
|
552
|
+
return discovery.sliderIndex === 0 ? all : undefined;
|
|
553
|
+
}
|
|
554
|
+
function semanticRelationship(slider, powerLabels) {
|
|
555
|
+
if (powerLabels.some(label => labelsMatch(slider.ariaLabel, label)))
|
|
556
|
+
return "aria-label";
|
|
557
|
+
if (powerLabels.some(label => labelsMatch(slider.labelledByText, label)))
|
|
558
|
+
return "aria-labelledby";
|
|
559
|
+
if (slider.owner?.visible === true
|
|
560
|
+
&& (powerLabels.some(label => labelsMatch(slider.owner?.label, label))
|
|
561
|
+
|| powerLabels.some(label => labelsMatch(slider.owner?.text, label)))) {
|
|
562
|
+
return "owner";
|
|
563
|
+
}
|
|
564
|
+
if (slider.menu?.visible === true
|
|
565
|
+
&& powerLabels.some(label => labelsMatch(slider.menu?.label, label))) {
|
|
566
|
+
return "menu-label";
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
function matchedPowerLabels(slider, powerLabels) {
|
|
571
|
+
const texts = [slider.ariaLabel, slider.labelledByText, slider.owner?.label, slider.owner?.text, slider.menu?.label]
|
|
572
|
+
.filter((text) => text !== undefined);
|
|
573
|
+
return powerLabels.filter(label => texts.some(text => labelsMatch(text, label)));
|
|
574
|
+
}
|
|
575
|
+
function optionEvidence(options, minimum, maximum, count) {
|
|
576
|
+
const visible = (options ?? []).filter(option => option.visible
|
|
577
|
+
&& option.label.trim().length > 0
|
|
578
|
+
&& option.label.length <= MAX_POWER_LABEL_LENGTH);
|
|
579
|
+
if (visible.length === 0)
|
|
580
|
+
return [];
|
|
581
|
+
const explicit = visible.map(option => {
|
|
582
|
+
const value = parseInteger(option.value);
|
|
583
|
+
return value === undefined
|
|
584
|
+
? { label: option.label }
|
|
585
|
+
: { label: option.label, value };
|
|
586
|
+
});
|
|
587
|
+
const explicitValues = explicit.map(option => option.value);
|
|
588
|
+
if (explicit.length === count
|
|
589
|
+
&& explicitValues.every((value) => value !== undefined && value >= minimum && value <= maximum)
|
|
590
|
+
&& new Set(explicitValues).size === explicitValues.length) {
|
|
591
|
+
return explicit;
|
|
592
|
+
}
|
|
593
|
+
// An ordered list without explicit values is safe only when it is complete
|
|
594
|
+
// and belongs to the already identified Power owner. Its DOM order is the
|
|
595
|
+
// semantic order exposed by the control, not a menu-position heuristic.
|
|
596
|
+
if (visible.length === count && explicitValues.every(value => value === undefined)) {
|
|
597
|
+
return visible.map((option, index) => ({ label: option.label, value: minimum + index }));
|
|
598
|
+
}
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
function parseInteger(value) {
|
|
602
|
+
if (value === undefined)
|
|
603
|
+
return undefined;
|
|
604
|
+
const trimmed = value.trim();
|
|
605
|
+
if (trimmed.length === 0 || trimmed.length > 32 || !/^-?\d+$/.test(trimmed))
|
|
606
|
+
return undefined;
|
|
607
|
+
const parsed = Number(trimmed);
|
|
608
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
609
|
+
}
|
|
610
|
+
function labelsMatch(value, wanted) {
|
|
611
|
+
if (value === undefined
|
|
612
|
+
|| value.length > MAX_POWER_LABEL_LENGTH
|
|
613
|
+
|| wanted.length > MAX_POWER_LABEL_LENGTH
|
|
614
|
+
|| value.trim().length === 0
|
|
615
|
+
|| wanted.trim().length === 0)
|
|
616
|
+
return false;
|
|
617
|
+
const normalizedValue = normalizeForLabelMatch(value);
|
|
618
|
+
const normalizedWanted = normalizeForLabelMatch(wanted);
|
|
619
|
+
if (normalizedValue === normalizedWanted)
|
|
620
|
+
return true;
|
|
621
|
+
const separators = `[\\s:•·\\-–—/]`;
|
|
622
|
+
return new RegExp(`(?:^|${separators})${escapeRegExp(normalizedWanted)}(?:$|${separators})`, "iu")
|
|
623
|
+
.test(normalizedValue);
|
|
624
|
+
}
|
|
625
|
+
function escapeRegExp(value) {
|
|
626
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
627
|
+
}
|