codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.4

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