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.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. 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
+ }