@descryy/mcp 0.11.5 → 0.11.7

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 (445) hide show
  1. package/dist/action-handshake.d.ts +38 -0
  2. package/dist/action-handshake.d.ts.map +1 -0
  3. package/dist/action-handshake.js +127 -0
  4. package/dist/action-handshake.js.map +1 -0
  5. package/dist/bin/descry-mcp.d.ts +7 -0
  6. package/dist/bin/descry-mcp.d.ts.map +1 -0
  7. package/dist/bin/descry-mcp.js +73 -0
  8. package/dist/bin/descry-mcp.js.map +1 -0
  9. package/dist/browser/auth.d.ts +84 -0
  10. package/dist/browser/auth.d.ts.map +1 -0
  11. package/dist/browser/auth.js +233 -0
  12. package/dist/browser/auth.js.map +1 -0
  13. package/dist/browser/capability-probe.d.ts +109 -0
  14. package/dist/browser/capability-probe.d.ts.map +1 -0
  15. package/dist/browser/capability-probe.js +201 -0
  16. package/dist/browser/capability-probe.js.map +1 -0
  17. package/dist/browser/driver.d.ts +591 -0
  18. package/dist/browser/driver.d.ts.map +1 -0
  19. package/dist/browser/driver.js +116 -0
  20. package/dist/browser/driver.js.map +1 -0
  21. package/dist/browser/evidence.d.ts +28 -0
  22. package/dist/browser/evidence.d.ts.map +1 -0
  23. package/dist/browser/evidence.js +82 -0
  24. package/dist/browser/evidence.js.map +1 -0
  25. package/dist/browser/fake-driver.d.ts +116 -0
  26. package/dist/browser/fake-driver.d.ts.map +1 -0
  27. package/dist/browser/fake-driver.js +416 -0
  28. package/dist/browser/fake-driver.js.map +1 -0
  29. package/dist/browser/fault-attribution.d.ts +178 -0
  30. package/dist/browser/fault-attribution.d.ts.map +1 -0
  31. package/dist/browser/fault-attribution.js +266 -0
  32. package/dist/browser/fault-attribution.js.map +1 -0
  33. package/dist/browser/graph-write.d.ts +45 -0
  34. package/dist/browser/graph-write.d.ts.map +1 -0
  35. package/dist/browser/graph-write.js +103 -0
  36. package/dist/browser/graph-write.js.map +1 -0
  37. package/dist/browser/identity-graph-write.d.ts +55 -0
  38. package/dist/browser/identity-graph-write.d.ts.map +1 -0
  39. package/dist/browser/identity-graph-write.js +45 -0
  40. package/dist/browser/identity-graph-write.js.map +1 -0
  41. package/dist/browser/page-probe.d.ts +81 -0
  42. package/dist/browser/page-probe.d.ts.map +1 -0
  43. package/dist/browser/page-probe.js +202 -0
  44. package/dist/browser/page-probe.js.map +1 -0
  45. package/dist/browser/playwright-driver.d.ts +115 -0
  46. package/dist/browser/playwright-driver.d.ts.map +1 -0
  47. package/dist/browser/playwright-driver.js +1545 -0
  48. package/dist/browser/playwright-driver.js.map +1 -0
  49. package/dist/browser/provider.d.ts +26 -0
  50. package/dist/browser/provider.d.ts.map +1 -0
  51. package/dist/browser/provider.js +14 -0
  52. package/dist/browser/provider.js.map +1 -0
  53. package/dist/browser/reach-recording-session.d.ts +44 -0
  54. package/dist/browser/reach-recording-session.d.ts.map +1 -0
  55. package/dist/browser/reach-recording-session.js +151 -0
  56. package/dist/browser/reach-recording-session.js.map +1 -0
  57. package/dist/browser/reachability.d.ts +168 -0
  58. package/dist/browser/reachability.d.ts.map +1 -0
  59. package/dist/browser/reachability.js +294 -0
  60. package/dist/browser/reachability.js.map +1 -0
  61. package/dist/browser/registry.d.ts +126 -0
  62. package/dist/browser/registry.d.ts.map +1 -0
  63. package/dist/browser/registry.js +177 -0
  64. package/dist/browser/registry.js.map +1 -0
  65. package/dist/browser/scenario-provenance.d.ts +23 -0
  66. package/dist/browser/scenario-provenance.d.ts.map +1 -0
  67. package/dist/browser/scenario-provenance.js +73 -0
  68. package/dist/browser/scenario-provenance.js.map +1 -0
  69. package/dist/browser/scenario-resolve.d.ts +28 -0
  70. package/dist/browser/scenario-resolve.d.ts.map +1 -0
  71. package/dist/browser/scenario-resolve.js +96 -0
  72. package/dist/browser/scenario-resolve.js.map +1 -0
  73. package/dist/browser/scenario-runner.d.ts +72 -0
  74. package/dist/browser/scenario-runner.d.ts.map +1 -0
  75. package/dist/browser/scenario-runner.js +353 -0
  76. package/dist/browser/scenario-runner.js.map +1 -0
  77. package/dist/browser/stack-parser.d.ts +19 -0
  78. package/dist/browser/stack-parser.d.ts.map +1 -0
  79. package/dist/browser/stack-parser.js +85 -0
  80. package/dist/browser/stack-parser.js.map +1 -0
  81. package/dist/browser/tool-support.d.ts +71 -0
  82. package/dist/browser/tool-support.d.ts.map +1 -0
  83. package/dist/browser/tool-support.js +216 -0
  84. package/dist/browser/tool-support.js.map +1 -0
  85. package/dist/browser/url-scheme.d.ts +14 -0
  86. package/dist/browser/url-scheme.d.ts.map +1 -0
  87. package/dist/browser/url-scheme.js +38 -0
  88. package/dist/browser/url-scheme.js.map +1 -0
  89. package/dist/browser/wait-target.d.ts +37 -0
  90. package/dist/browser/wait-target.d.ts.map +1 -0
  91. package/dist/browser/wait-target.js +54 -0
  92. package/dist/browser/wait-target.js.map +1 -0
  93. package/dist/cancellation.d.ts +20 -0
  94. package/dist/cancellation.d.ts.map +1 -0
  95. package/dist/cancellation.js +41 -0
  96. package/dist/cancellation.js.map +1 -0
  97. package/dist/capped.d.ts +39 -0
  98. package/dist/capped.d.ts.map +1 -0
  99. package/dist/capped.js +43 -0
  100. package/dist/capped.js.map +1 -0
  101. package/dist/credential-store.d.ts +22 -0
  102. package/dist/credential-store.d.ts.map +1 -0
  103. package/dist/credential-store.js +33 -0
  104. package/dist/credential-store.js.map +1 -0
  105. package/dist/disclosure-ledger.d.ts +14 -0
  106. package/dist/disclosure-ledger.d.ts.map +1 -0
  107. package/dist/disclosure-ledger.js +18 -0
  108. package/dist/disclosure-ledger.js.map +1 -0
  109. package/dist/evidence/observed-route-evidence.d.ts +25 -0
  110. package/dist/evidence/observed-route-evidence.d.ts.map +1 -0
  111. package/dist/evidence/observed-route-evidence.js +76 -0
  112. package/dist/evidence/observed-route-evidence.js.map +1 -0
  113. package/dist/heap.d.ts +61 -0
  114. package/dist/heap.d.ts.map +1 -0
  115. package/dist/heap.js +82 -0
  116. package/dist/heap.js.map +1 -0
  117. package/dist/heartbeat.d.ts +46 -0
  118. package/dist/heartbeat.d.ts.map +1 -0
  119. package/dist/heartbeat.js +66 -0
  120. package/dist/heartbeat.js.map +1 -0
  121. package/dist/index.d.ts +32 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +19 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/module-trust.d.ts +37 -0
  126. package/dist/module-trust.d.ts.map +1 -0
  127. package/dist/module-trust.js +77 -0
  128. package/dist/module-trust.js.map +1 -0
  129. package/dist/path-confinement.d.ts +31 -0
  130. package/dist/path-confinement.d.ts.map +1 -0
  131. package/dist/path-confinement.js +44 -0
  132. package/dist/path-confinement.js.map +1 -0
  133. package/dist/planner/predict-then-propose.d.ts +134 -0
  134. package/dist/planner/predict-then-propose.d.ts.map +1 -0
  135. package/dist/planner/predict-then-propose.js +138 -0
  136. package/dist/planner/predict-then-propose.js.map +1 -0
  137. package/dist/planner/propose-journey.d.ts +75 -0
  138. package/dist/planner/propose-journey.d.ts.map +1 -0
  139. package/dist/planner/propose-journey.js +96 -0
  140. package/dist/planner/propose-journey.js.map +1 -0
  141. package/dist/protocol.d.ts +57 -0
  142. package/dist/protocol.d.ts.map +1 -0
  143. package/dist/protocol.js +90 -0
  144. package/dist/protocol.js.map +1 -0
  145. package/dist/ready-checks.d.ts +12 -0
  146. package/dist/ready-checks.d.ts.map +1 -0
  147. package/dist/ready-checks.js +43 -0
  148. package/dist/ready-checks.js.map +1 -0
  149. package/dist/registry.d.ts +103 -0
  150. package/dist/registry.d.ts.map +1 -0
  151. package/dist/registry.js +327 -0
  152. package/dist/registry.js.map +1 -0
  153. package/dist/render.d.ts +623 -0
  154. package/dist/render.d.ts.map +1 -0
  155. package/dist/render.js +968 -0
  156. package/dist/render.js.map +1 -0
  157. package/dist/runtime-registry.d.ts +22 -0
  158. package/dist/runtime-registry.d.ts.map +1 -0
  159. package/dist/runtime-registry.js +142 -0
  160. package/dist/runtime-registry.js.map +1 -0
  161. package/dist/sandbox-defaults.d.ts +41 -0
  162. package/dist/sandbox-defaults.d.ts.map +1 -0
  163. package/dist/sandbox-defaults.js +14 -0
  164. package/dist/sandbox-defaults.js.map +1 -0
  165. package/dist/scenarios/browser-run-plan-projection.d.ts +161 -0
  166. package/dist/scenarios/browser-run-plan-projection.d.ts.map +1 -0
  167. package/dist/scenarios/browser-run-plan-projection.js +264 -0
  168. package/dist/scenarios/browser-run-plan-projection.js.map +1 -0
  169. package/dist/scenarios/credential-ref.d.ts +102 -0
  170. package/dist/scenarios/credential-ref.d.ts.map +1 -0
  171. package/dist/scenarios/credential-ref.js +148 -0
  172. package/dist/scenarios/credential-ref.js.map +1 -0
  173. package/dist/scenarios/index.d.ts +7 -0
  174. package/dist/scenarios/index.d.ts.map +1 -0
  175. package/dist/scenarios/index.js +7 -0
  176. package/dist/scenarios/index.js.map +1 -0
  177. package/dist/scenarios/parse.d.ts +18 -0
  178. package/dist/scenarios/parse.d.ts.map +1 -0
  179. package/dist/scenarios/parse.js +212 -0
  180. package/dist/scenarios/parse.js.map +1 -0
  181. package/dist/scenarios/scenario.d.ts +93 -0
  182. package/dist/scenarios/scenario.d.ts.map +1 -0
  183. package/dist/scenarios/scenario.js +27 -0
  184. package/dist/scenarios/scenario.js.map +1 -0
  185. package/dist/scenarios/secret-ref.d.ts +86 -0
  186. package/dist/scenarios/secret-ref.d.ts.map +1 -0
  187. package/dist/scenarios/secret-ref.js +125 -0
  188. package/dist/scenarios/secret-ref.js.map +1 -0
  189. package/dist/scenarios/storage.d.ts +21 -0
  190. package/dist/scenarios/storage.d.ts.map +1 -0
  191. package/dist/scenarios/storage.js +103 -0
  192. package/dist/scenarios/storage.js.map +1 -0
  193. package/dist/server.d.ts +61 -0
  194. package/dist/server.d.ts.map +1 -0
  195. package/dist/server.js +642 -0
  196. package/dist/server.js.map +1 -0
  197. package/dist/session.d.ts +356 -0
  198. package/dist/session.d.ts.map +1 -0
  199. package/dist/session.js +779 -0
  200. package/dist/session.js.map +1 -0
  201. package/dist/tools/alias-kit.d.ts +29 -0
  202. package/dist/tools/alias-kit.d.ts.map +1 -0
  203. package/dist/tools/alias-kit.js +21 -0
  204. package/dist/tools/alias-kit.js.map +1 -0
  205. package/dist/tools/aliases.d.ts +42 -0
  206. package/dist/tools/aliases.d.ts.map +1 -0
  207. package/dist/tools/aliases.js +52 -0
  208. package/dist/tools/aliases.js.map +1 -0
  209. package/dist/tools/analyze-workspace.d.ts +52 -0
  210. package/dist/tools/analyze-workspace.d.ts.map +1 -0
  211. package/dist/tools/analyze-workspace.js +207 -0
  212. package/dist/tools/analyze-workspace.js.map +1 -0
  213. package/dist/tools/analyze.d.ts +163 -0
  214. package/dist/tools/analyze.d.ts.map +1 -0
  215. package/dist/tools/analyze.js +724 -0
  216. package/dist/tools/analyze.js.map +1 -0
  217. package/dist/tools/browser-click.d.ts +18 -0
  218. package/dist/tools/browser-click.d.ts.map +1 -0
  219. package/dist/tools/browser-click.js +110 -0
  220. package/dist/tools/browser-click.js.map +1 -0
  221. package/dist/tools/browser-close-session.d.ts +39 -0
  222. package/dist/tools/browser-close-session.d.ts.map +1 -0
  223. package/dist/tools/browser-close-session.js +120 -0
  224. package/dist/tools/browser-close-session.js.map +1 -0
  225. package/dist/tools/browser-fill.d.ts +18 -0
  226. package/dist/tools/browser-fill.d.ts.map +1 -0
  227. package/dist/tools/browser-fill.js +117 -0
  228. package/dist/tools/browser-fill.js.map +1 -0
  229. package/dist/tools/browser-navigate.d.ts +15 -0
  230. package/dist/tools/browser-navigate.d.ts.map +1 -0
  231. package/dist/tools/browser-navigate.js +106 -0
  232. package/dist/tools/browser-navigate.js.map +1 -0
  233. package/dist/tools/browser-read.d.ts +33 -0
  234. package/dist/tools/browser-read.d.ts.map +1 -0
  235. package/dist/tools/browser-read.js +105 -0
  236. package/dist/tools/browser-read.js.map +1 -0
  237. package/dist/tools/browser-run-scenario.d.ts +28 -0
  238. package/dist/tools/browser-run-scenario.d.ts.map +1 -0
  239. package/dist/tools/browser-run-scenario.js +238 -0
  240. package/dist/tools/browser-run-scenario.js.map +1 -0
  241. package/dist/tools/browser-save-scenario.d.ts +19 -0
  242. package/dist/tools/browser-save-scenario.d.ts.map +1 -0
  243. package/dist/tools/browser-save-scenario.js +447 -0
  244. package/dist/tools/browser-save-scenario.js.map +1 -0
  245. package/dist/tools/browser-select.d.ts +20 -0
  246. package/dist/tools/browser-select.d.ts.map +1 -0
  247. package/dist/tools/browser-select.js +120 -0
  248. package/dist/tools/browser-select.js.map +1 -0
  249. package/dist/tools/browser-snapshot.d.ts +46 -0
  250. package/dist/tools/browser-snapshot.d.ts.map +1 -0
  251. package/dist/tools/browser-snapshot.js +127 -0
  252. package/dist/tools/browser-snapshot.js.map +1 -0
  253. package/dist/tools/browser-start-session.d.ts +24 -0
  254. package/dist/tools/browser-start-session.d.ts.map +1 -0
  255. package/dist/tools/browser-start-session.js +653 -0
  256. package/dist/tools/browser-start-session.js.map +1 -0
  257. package/dist/tools/browser-submit.d.ts +18 -0
  258. package/dist/tools/browser-submit.d.ts.map +1 -0
  259. package/dist/tools/browser-submit.js +106 -0
  260. package/dist/tools/browser-submit.js.map +1 -0
  261. package/dist/tools/browser-type.d.ts +18 -0
  262. package/dist/tools/browser-type.d.ts.map +1 -0
  263. package/dist/tools/browser-type.js +115 -0
  264. package/dist/tools/browser-type.js.map +1 -0
  265. package/dist/tools/browser-wait-for.d.ts +45 -0
  266. package/dist/tools/browser-wait-for.d.ts.map +1 -0
  267. package/dist/tools/browser-wait-for.js +164 -0
  268. package/dist/tools/browser-wait-for.js.map +1 -0
  269. package/dist/tools/browser.d.ts +69 -0
  270. package/dist/tools/browser.d.ts.map +1 -0
  271. package/dist/tools/browser.js +326 -0
  272. package/dist/tools/browser.js.map +1 -0
  273. package/dist/tools/chain.d.ts +41 -0
  274. package/dist/tools/chain.d.ts.map +1 -0
  275. package/dist/tools/chain.js +101 -0
  276. package/dist/tools/chain.js.map +1 -0
  277. package/dist/tools/change-scope.d.ts +25 -0
  278. package/dist/tools/change-scope.d.ts.map +1 -0
  279. package/dist/tools/change-scope.js +103 -0
  280. package/dist/tools/change-scope.js.map +1 -0
  281. package/dist/tools/code-context.d.ts +83 -0
  282. package/dist/tools/code-context.d.ts.map +1 -0
  283. package/dist/tools/code-context.js +432 -0
  284. package/dist/tools/code-context.js.map +1 -0
  285. package/dist/tools/contracts.d.ts +74 -0
  286. package/dist/tools/contracts.d.ts.map +1 -0
  287. package/dist/tools/contracts.js +214 -0
  288. package/dist/tools/contracts.js.map +1 -0
  289. package/dist/tools/cross-pr.d.ts +178 -0
  290. package/dist/tools/cross-pr.d.ts.map +1 -0
  291. package/dist/tools/cross-pr.js +362 -0
  292. package/dist/tools/cross-pr.js.map +1 -0
  293. package/dist/tools/disclosure.d.ts +75 -0
  294. package/dist/tools/disclosure.d.ts.map +1 -0
  295. package/dist/tools/disclosure.js +325 -0
  296. package/dist/tools/disclosure.js.map +1 -0
  297. package/dist/tools/draw-conclusion.d.ts +55 -0
  298. package/dist/tools/draw-conclusion.d.ts.map +1 -0
  299. package/dist/tools/draw-conclusion.js +243 -0
  300. package/dist/tools/draw-conclusion.js.map +1 -0
  301. package/dist/tools/git-context.d.ts +39 -0
  302. package/dist/tools/git-context.d.ts.map +1 -0
  303. package/dist/tools/git-context.js +374 -0
  304. package/dist/tools/git-context.js.map +1 -0
  305. package/dist/tools/git-diff.d.ts +45 -0
  306. package/dist/tools/git-diff.d.ts.map +1 -0
  307. package/dist/tools/git-diff.js +338 -0
  308. package/dist/tools/git-diff.js.map +1 -0
  309. package/dist/tools/git-history.d.ts +16 -0
  310. package/dist/tools/git-history.d.ts.map +1 -0
  311. package/dist/tools/git-history.js +106 -0
  312. package/dist/tools/git-history.js.map +1 -0
  313. package/dist/tools/history.d.ts +34 -0
  314. package/dist/tools/history.d.ts.map +1 -0
  315. package/dist/tools/history.js +121 -0
  316. package/dist/tools/history.js.map +1 -0
  317. package/dist/tools/impact.d.ts +46 -0
  318. package/dist/tools/impact.d.ts.map +1 -0
  319. package/dist/tools/impact.js +225 -0
  320. package/dist/tools/impact.js.map +1 -0
  321. package/dist/tools/index.d.ts +59 -0
  322. package/dist/tools/index.d.ts.map +1 -0
  323. package/dist/tools/index.js +70 -0
  324. package/dist/tools/index.js.map +1 -0
  325. package/dist/tools/kit.d.ts +165 -0
  326. package/dist/tools/kit.d.ts.map +1 -0
  327. package/dist/tools/kit.js +153 -0
  328. package/dist/tools/kit.js.map +1 -0
  329. package/dist/tools/link-workspace.d.ts +24 -0
  330. package/dist/tools/link-workspace.d.ts.map +1 -0
  331. package/dist/tools/link-workspace.js +131 -0
  332. package/dist/tools/link-workspace.js.map +1 -0
  333. package/dist/tools/lookup.d.ts +19 -0
  334. package/dist/tools/lookup.d.ts.map +1 -0
  335. package/dist/tools/lookup.js +63 -0
  336. package/dist/tools/lookup.js.map +1 -0
  337. package/dist/tools/mark-incident.d.ts +28 -0
  338. package/dist/tools/mark-incident.d.ts.map +1 -0
  339. package/dist/tools/mark-incident.js +171 -0
  340. package/dist/tools/mark-incident.js.map +1 -0
  341. package/dist/tools/merged-browser.d.ts +11 -0
  342. package/dist/tools/merged-browser.d.ts.map +1 -0
  343. package/dist/tools/merged-browser.js +20 -0
  344. package/dist/tools/merged-browser.js.map +1 -0
  345. package/dist/tools/merged-context.d.ts +11 -0
  346. package/dist/tools/merged-context.d.ts.map +1 -0
  347. package/dist/tools/merged-context.js +21 -0
  348. package/dist/tools/merged-context.js.map +1 -0
  349. package/dist/tools/merged-observe.d.ts +13 -0
  350. package/dist/tools/merged-observe.d.ts.map +1 -0
  351. package/dist/tools/merged-observe.js +21 -0
  352. package/dist/tools/merged-observe.js.map +1 -0
  353. package/dist/tools/merged-workspace.d.ts +14 -0
  354. package/dist/tools/merged-workspace.d.ts.map +1 -0
  355. package/dist/tools/merged-workspace.js +21 -0
  356. package/dist/tools/merged-workspace.js.map +1 -0
  357. package/dist/tools/observe-runtime.d.ts +262 -0
  358. package/dist/tools/observe-runtime.d.ts.map +1 -0
  359. package/dist/tools/observe-runtime.js +1966 -0
  360. package/dist/tools/observe-runtime.js.map +1 -0
  361. package/dist/tools/observe-tests.d.ts +57 -0
  362. package/dist/tools/observe-tests.d.ts.map +1 -0
  363. package/dist/tools/observe-tests.js +391 -0
  364. package/dist/tools/observe-tests.js.map +1 -0
  365. package/dist/tools/observe.d.ts +28 -0
  366. package/dist/tools/observe.d.ts.map +1 -0
  367. package/dist/tools/observe.js +291 -0
  368. package/dist/tools/observe.js.map +1 -0
  369. package/dist/tools/pr-analysis.d.ts +189 -0
  370. package/dist/tools/pr-analysis.d.ts.map +1 -0
  371. package/dist/tools/pr-analysis.js +365 -0
  372. package/dist/tools/pr-analysis.js.map +1 -0
  373. package/dist/tools/pre-push.d.ts +168 -0
  374. package/dist/tools/pre-push.d.ts.map +1 -0
  375. package/dist/tools/pre-push.js +416 -0
  376. package/dist/tools/pre-push.js.map +1 -0
  377. package/dist/tools/predict-reach.d.ts +57 -0
  378. package/dist/tools/predict-reach.d.ts.map +1 -0
  379. package/dist/tools/predict-reach.js +183 -0
  380. package/dist/tools/predict-reach.js.map +1 -0
  381. package/dist/tools/propagation.d.ts +37 -0
  382. package/dist/tools/propagation.d.ts.map +1 -0
  383. package/dist/tools/propagation.js +162 -0
  384. package/dist/tools/propagation.js.map +1 -0
  385. package/dist/tools/propose-journey.d.ts +16 -0
  386. package/dist/tools/propose-journey.d.ts.map +1 -0
  387. package/dist/tools/propose-journey.js +132 -0
  388. package/dist/tools/propose-journey.js.map +1 -0
  389. package/dist/tools/prove-reach.d.ts +74 -0
  390. package/dist/tools/prove-reach.d.ts.map +1 -0
  391. package/dist/tools/prove-reach.js +274 -0
  392. package/dist/tools/prove-reach.js.map +1 -0
  393. package/dist/tools/questions.d.ts +30 -0
  394. package/dist/tools/questions.d.ts.map +1 -0
  395. package/dist/tools/questions.js +230 -0
  396. package/dist/tools/questions.js.map +1 -0
  397. package/dist/tools/refusal-fetch.d.ts +35 -0
  398. package/dist/tools/refusal-fetch.d.ts.map +1 -0
  399. package/dist/tools/refusal-fetch.js +123 -0
  400. package/dist/tools/refusal-fetch.js.map +1 -0
  401. package/dist/tools/runtime-incident.d.ts +33 -0
  402. package/dist/tools/runtime-incident.d.ts.map +1 -0
  403. package/dist/tools/runtime-incident.js +67 -0
  404. package/dist/tools/runtime-incident.js.map +1 -0
  405. package/dist/tools/runtime-journey-drive.d.ts +102 -0
  406. package/dist/tools/runtime-journey-drive.d.ts.map +1 -0
  407. package/dist/tools/runtime-journey-drive.js +247 -0
  408. package/dist/tools/runtime-journey-drive.js.map +1 -0
  409. package/dist/tools/scope.d.ts +45 -0
  410. package/dist/tools/scope.d.ts.map +1 -0
  411. package/dist/tools/scope.js +217 -0
  412. package/dist/tools/scope.js.map +1 -0
  413. package/dist/tools/similar-incidents.d.ts +43 -0
  414. package/dist/tools/similar-incidents.d.ts.map +1 -0
  415. package/dist/tools/similar-incidents.js +190 -0
  416. package/dist/tools/similar-incidents.js.map +1 -0
  417. package/dist/tools/validate.d.ts +133 -0
  418. package/dist/tools/validate.d.ts.map +1 -0
  419. package/dist/tools/validate.js +488 -0
  420. package/dist/tools/validate.js.map +1 -0
  421. package/dist/tools/verb.d.ts +46 -0
  422. package/dist/tools/verb.d.ts.map +1 -0
  423. package/dist/tools/verb.js +78 -0
  424. package/dist/tools/verb.js.map +1 -0
  425. package/dist/tools/verification-status.d.ts +38 -0
  426. package/dist/tools/verification-status.d.ts.map +1 -0
  427. package/dist/tools/verification-status.js +203 -0
  428. package/dist/tools/verification-status.js.map +1 -0
  429. package/dist/tools/verify-claim.d.ts +29 -0
  430. package/dist/tools/verify-claim.d.ts.map +1 -0
  431. package/dist/tools/verify-claim.js +218 -0
  432. package/dist/tools/verify-claim.js.map +1 -0
  433. package/dist/tools/workspace.d.ts +28 -0
  434. package/dist/tools/workspace.d.ts.map +1 -0
  435. package/dist/tools/workspace.js +94 -0
  436. package/dist/tools/workspace.js.map +1 -0
  437. package/dist/transport.d.ts +32 -0
  438. package/dist/transport.d.ts.map +1 -0
  439. package/dist/transport.js +85 -0
  440. package/dist/transport.js.map +1 -0
  441. package/dist/workspace-index.d.ts +164 -0
  442. package/dist/workspace-index.d.ts.map +1 -0
  443. package/dist/workspace-index.js +381 -0
  444. package/dist/workspace-index.js.map +1 -0
  445. package/package.json +5 -5
@@ -0,0 +1,1545 @@
1
+ /**
2
+ * The real `BrowserDriver`, over `@descryy/runtime-browser`. Import stays
3
+ * dynamic — `loadRuntimeModule` is a test injection seam (fake module, no
4
+ * browser needed). Adds only element refs over the runtime; stack-trace parsing is caller-supplied (rule 1: no language named here).
5
+ */
6
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
7
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
8
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
9
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
10
+ });
11
+ }
12
+ return path;
13
+ };
14
+ // RT-263: static, unlike the dynamic `@descryy/runtime-browser` load below -- this
15
+ // package is an ordinary dependency of @descryy/mcp regardless of whether the browser
16
+ // backend is installed. Currently blocked on a runtime release: @descryy/mcp pins
17
+ // @descryy/runtime-controller at 0.3.5, which predates preflight.ts.
18
+ import { runPreflight, portAnswersCheck } from "@descryy/runtime-controller";
19
+ import { BrowserSessionCrashedError, NoSuchOptionError, NotOperableError, NotSelectableError, NotSubmittableError, UnknownElementRefError, } from "./driver.js";
20
+ import { clampWaitTimeout, describeWaitTarget, isEmptyWaitTarget, matchesWaitTarget, WAIT_POLL_INTERVAL_MS, } from "./wait-target.js";
21
+ import { PROBE_CONSTANTS, probeInPage } from "./page-probe.js";
22
+ /** The module the driver composes. Named once, imported at call time. */
23
+ const RUNTIME_BROWSER_MODULE = "@descryy/runtime-browser";
24
+ const defaultRuntimeModuleLoader = () => import(__rewriteRelativeImportExtension(RUNTIME_BROWSER_MODULE));
25
+ /** Same seam, for the optional identity-grounding module — imported dynamically and only
26
+ * when `groundIdentity` is set, so a build or a session that never asks for it pays nothing. */
27
+ const IDENTITY_GROUNDING_MODULE = "@descryy/runtime-identity-grounding";
28
+ const defaultIdentityGroundingModuleLoader = () => import(__rewriteRelativeImportExtension(IDENTITY_GROUNDING_MODULE));
29
+ /** Ceiling on waiting for network activity to *start* — no signal proves "never will". Kept at the old fixed 300ms so a no-op action observes the same as before. */
30
+ const ACTIVITY_GRACE_MS = 300;
31
+ /** How long after the last request event counts as settled; fires only once activity is seen, so it measures a gap, not a guess about more coming. */
32
+ const NETWORK_QUIET_MS = 120;
33
+ /** Ceiling on the whole wait; reaching it is recorded on the observation and disclosed, never swallowed. */
34
+ const SETTLE_TIMEOUT_MS = 5_000;
35
+ /** How often the settle loop re-checks. Small enough not to add latency of its own. */
36
+ const POLL_INTERVAL_MS = 10;
37
+ /** Elements an agent can act on — deliberately narrow; a full-page snapshot would blow the reply budget. `[role]` included for custom controls. */
38
+ const INTERACTIVE = "a, button, input, select, textarea, [role], [onclick], summary";
39
+ /** How many scroll-and-re-check rounds a reveal pass runs before it stops and says it stopped. */
40
+ const REVEAL_MAX_PASSES = 12;
41
+ /** How long after a scroll before re-checking. A lazily-rendered row needs a frame or two to exist. */
42
+ const REVEAL_SETTLE_MS = 150;
43
+ /** `RawElementIdentity` -> the port's own `ElementComponentIdentity` — the one place the two
44
+ * vocabularies are joined, so a change to either shows up as one diff here. */
45
+ function mapIdentity(raw) {
46
+ switch (raw.outcome) {
47
+ case "grounded":
48
+ return {
49
+ outcome: "grounded",
50
+ component: raw.component.name,
51
+ file: raw.sourceLocation.file,
52
+ line: raw.sourceLocation.line,
53
+ authoredSource: raw.authoredSource,
54
+ reason: null,
55
+ };
56
+ case "component-only":
57
+ return {
58
+ outcome: "component-only",
59
+ component: raw.component.name,
60
+ file: null,
61
+ line: null,
62
+ authoredSource: false,
63
+ reason: raw.reason,
64
+ };
65
+ case "refused":
66
+ return {
67
+ outcome: "refused",
68
+ component: null,
69
+ file: null,
70
+ line: null,
71
+ authoredSource: false,
72
+ reason: raw.reason,
73
+ };
74
+ }
75
+ }
76
+ function withSessionWarnings(observation, warnings) {
77
+ if (warnings.length === 0)
78
+ return observation;
79
+ const drained = [...warnings];
80
+ warnings.length = 0;
81
+ return { ...observation, evidenceWarnings: [...observation.evidenceWarnings, ...drained] };
82
+ }
83
+ /** Grounds one selector, never throwing — a probe failure is reported as a named refusal
84
+ * (rule 2: no guess), the same as every other outcome the grounder itself can name. */
85
+ async function groundOne(grounder, page, selector) {
86
+ try {
87
+ const raw = await grounder.ground(page, selector);
88
+ return mapIdentity(raw);
89
+ }
90
+ catch (error) {
91
+ return {
92
+ outcome: "refused",
93
+ component: null,
94
+ file: null,
95
+ line: null,
96
+ authoredSource: false,
97
+ reason: `the identity probe failed: ${error instanceof Error ? error.message : String(error)}`,
98
+ };
99
+ }
100
+ }
101
+ /** What one element looks like from inside the page. Serialized and run there, so it closes over
102
+ * nothing in this module — every helper it needs is declared inside it.
103
+ *
104
+ * Exported for `browser-element-describe.test.ts`: role, name and renderedness are pure
105
+ * functions of one element, and pinning their decision table should not need a browser. */
106
+ export function describeElement(el) {
107
+ const tag = el.tagName.toLowerCase();
108
+ const explicit = el.getAttribute("role");
109
+ const type = el.getAttribute("type")?.toLowerCase() ?? null;
110
+ /** The HTML→ARIA role of this element. The old chain ended `: tag`, which reported
111
+ * `input[type="number"]` as role `"input"` — a role no agent filters on and no verb
112
+ * validates against, so a real control went silently missing from every role-filtered
113
+ * read. The mapping below is HTML's own, and it names no framework (rule 1).
114
+ *
115
+ * It still ends at the tag for a tag this does not know, deliberately: `[role]` and
116
+ * `[onclick]` put arbitrary elements into the query, and inventing a role for one of
117
+ * them would be this same defect pointed the other way. */
118
+ function derivedRole() {
119
+ if (tag === "a")
120
+ return "link";
121
+ if (tag === "button")
122
+ return "button";
123
+ if (tag === "textarea")
124
+ return "textbox";
125
+ // `summary` is the control that opens a `<details>`; a click is what it takes.
126
+ if (tag === "summary")
127
+ return "button";
128
+ if (tag === "select") {
129
+ const multiple = el.getAttribute("multiple") !== null;
130
+ const size = Number(el.getAttribute("size") ?? "1");
131
+ return multiple || size > 1 ? "listbox" : "combobox";
132
+ }
133
+ if (tag !== "input")
134
+ return tag;
135
+ switch (type) {
136
+ case "button":
137
+ case "submit":
138
+ case "reset":
139
+ case "image":
140
+ return "button";
141
+ case "checkbox":
142
+ return "checkbox";
143
+ case "radio":
144
+ return "radio";
145
+ case "range":
146
+ return "slider";
147
+ case "number":
148
+ return "spinbutton";
149
+ case "search":
150
+ return "searchbox";
151
+ // `password` has no ARIA role of its own; `textbox` is both the useful answer and the
152
+ // true one about how it is operated. The rest are HTML's textbox-mapped types.
153
+ case null:
154
+ case "text":
155
+ case "email":
156
+ case "tel":
157
+ case "url":
158
+ case "password":
159
+ return "textbox";
160
+ default:
161
+ // `file`, `color`, `date`, `hidden`, and anything a later HTML adds. Reporting the
162
+ // tag here is a disclosed gap; reporting `textbox` would be a wrong fact.
163
+ return tag;
164
+ }
165
+ }
166
+ const role = explicit ?? derivedRole();
167
+ /** A pragmatic accessible-name approximation, not a full AccName implementation.
168
+ *
169
+ * One case is carved out rather than approximated: a **chooser's own contents are its
170
+ * options**, so `innerText` on a `<select>` returns the list of things it can be set to,
171
+ * and a period picker came back named `"This month\nLast month"`. That is not a rough
172
+ * name, it is a false one — no person calls the control that, and the `select` verb
173
+ * matches on name. A chooser therefore skips the text tiers entirely and is left unnamed
174
+ * when nothing else names it. Unnamed is a gap; named after its options is a lie. */
175
+ function derivedName() {
176
+ const aria = el.getAttribute("aria-label");
177
+ if (aria !== null && aria.trim() !== "")
178
+ return aria;
179
+ if (tag !== "select") {
180
+ const inner = el.innerText?.trim();
181
+ if (inner !== undefined && inner !== "")
182
+ return inner;
183
+ const text = el.textContent?.trim();
184
+ if (text !== undefined && text !== null && text !== "")
185
+ return text;
186
+ }
187
+ // A push-button input carries its label in `value`, where no text tier would find it.
188
+ if (tag === "input" && (type === "button" || type === "submit" || type === "reset")) {
189
+ const value = el.getAttribute("value");
190
+ if (value !== null && value.trim() !== "")
191
+ return value.trim();
192
+ }
193
+ return el.getAttribute("placeholder") ?? el.getAttribute("title") ?? el.getAttribute("name") ?? "";
194
+ }
195
+ const name = derivedName();
196
+ /** Whether the page is laying this element out and painting it — true, false, or `null`
197
+ * when nothing available could answer.
198
+ *
199
+ * **Deliberately not a viewport test.** An element below the fold is on the page and the
200
+ * reveal pass depends on finding it; treating off-screen as hidden would trade this
201
+ * precision defect for a recall one. What it does reject is an element the page renders
202
+ * nowhere at all: `display:none`, the `hidden` attribute, a closed `<dialog>`,
203
+ * `content-visibility: hidden`, `visibility: hidden`, and `<canvas>` fallback content.
204
+ *
205
+ * Opacity is **not** rejected: a zero-opacity element still occupies space and still
206
+ * takes a click, and a fade-in caught mid-animation would vanish from the snapshot. */
207
+ function renderedState() {
208
+ if (typeof el.checkVisibility === "function") {
209
+ try {
210
+ return el.checkVisibility({ visibilityProperty: true });
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ if (typeof el.getClientRects !== "function")
217
+ return null;
218
+ try {
219
+ if (el.getClientRects().length === 0)
220
+ return false;
221
+ }
222
+ catch {
223
+ return null;
224
+ }
225
+ const view = globalThis.getComputedStyle;
226
+ if (typeof view !== "function")
227
+ return null;
228
+ try {
229
+ const visibility = view.call(globalThis, el)?.visibility;
230
+ if (visibility === undefined)
231
+ return null;
232
+ return visibility !== "hidden" && visibility !== "collapse";
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ // The node's own root: the document for a light-DOM element, the shadow root for one inside
239
+ // an open shadow root. Only a shadow root has a `host`, which is what tells them apart, and
240
+ // it is also the scope a uniqueness check has to run in — `ownerDocument.querySelectorAll`
241
+ // matches nothing at all for a shadow child, so every such element used to come back with a
242
+ // null selector for a reason that read like "no stable hook".
243
+ const root = typeof el.getRootNode === "function" ? el.getRootNode() : el.ownerDocument;
244
+ const inShadowRoot = root !== el.ownerDocument && root.host !== undefined && root.host !== null;
245
+ // Computed in this same round trip so role/name/selector read off one live DOM state.
246
+ // Rejects ids that look generated (hex run, React's `:r0:` useId
247
+ // style, or blank) — "stable across reload" is what a generated id lacks.
248
+ const GENERATED_ID = /[0-9a-f]{8}|:r[0-9a-z]+:|^\s*$/i;
249
+ const FORM_CONTROLS = ["input", "select", "textarea", "button"];
250
+ function escapeAttrValue(value) {
251
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
252
+ }
253
+ function isUnique(candidate) {
254
+ try {
255
+ return root.querySelectorAll(candidate).length === 1;
256
+ }
257
+ catch {
258
+ // Invalid selector (rare) is treated as "no candidate", not thrown.
259
+ return false;
260
+ }
261
+ }
262
+ /** The element's own stable-hook selector, or null. Used both on the target itself and while climbing for a structural path's anchor. */
263
+ function stableHookSelector(node) {
264
+ const testId = node.getAttribute("data-testid");
265
+ if (testId !== null && testId.trim() !== "")
266
+ return `[data-testid="${escapeAttrValue(testId)}"]`;
267
+ const testAttr = node.getAttribute("data-test");
268
+ if (testAttr !== null && testAttr.trim() !== "")
269
+ return `[data-test="${escapeAttrValue(testAttr)}"]`;
270
+ const id = node.getAttribute("id");
271
+ if (id !== null && !GENERATED_ID.test(id))
272
+ return `[id="${escapeAttrValue(id)}"]`;
273
+ return null;
274
+ }
275
+ function nthOfTypeSegment(node, parent) {
276
+ const nodeTag = node.tagName.toLowerCase();
277
+ const siblings = parent.querySelectorAll(`:scope > ${nodeTag}`);
278
+ if (siblings.length <= 1)
279
+ return nodeTag;
280
+ for (let i = 0; i < siblings.length; i += 1) {
281
+ if (siblings.item(i) === node)
282
+ return `${nodeTag}:nth-of-type(${i + 1})`;
283
+ }
284
+ return nodeTag;
285
+ }
286
+ /** A bounded path from the nearest ancestor carrying a stable hook, capped at 6 segments. Null
287
+ * when none is found in range — including when the climb reaches a shadow boundary, where
288
+ * `parentElement` is null and there is genuinely no further ancestor inside this root. */
289
+ function structuralPath(target) {
290
+ const segments = [];
291
+ let node = target;
292
+ for (let depth = 0; depth < 6; depth += 1) {
293
+ const parent = node.parentElement;
294
+ if (parent === null)
295
+ return null;
296
+ segments.unshift(nthOfTypeSegment(node, parent));
297
+ const hook = stableHookSelector(parent);
298
+ if (hook !== null) {
299
+ const candidate = `${hook} > ${segments.join(" > ")}`;
300
+ return isUnique(candidate) ? candidate : null;
301
+ }
302
+ node = parent;
303
+ }
304
+ return null;
305
+ }
306
+ function computeSelector(target) {
307
+ // Priority order: first unique candidate wins. Each tier is checked
308
+ // independently — a shared data-testid isn't addressable either,
309
+ // so the next tier gets a turn instead of stopping there.
310
+ const testId = target.getAttribute("data-testid");
311
+ if (testId !== null && testId.trim() !== "") {
312
+ const candidate = `[data-testid="${escapeAttrValue(testId)}"]`;
313
+ if (isUnique(candidate))
314
+ return candidate;
315
+ }
316
+ const testAttr = target.getAttribute("data-test");
317
+ if (testAttr !== null && testAttr.trim() !== "") {
318
+ const candidate = `[data-test="${escapeAttrValue(testAttr)}"]`;
319
+ if (isUnique(candidate))
320
+ return candidate;
321
+ }
322
+ const id = target.getAttribute("id");
323
+ if (id !== null && !GENERATED_ID.test(id)) {
324
+ const candidate = `[id="${escapeAttrValue(id)}"]`;
325
+ if (isUnique(candidate))
326
+ return candidate;
327
+ }
328
+ const controlTag = target.tagName.toLowerCase();
329
+ const controlName = target.getAttribute("name");
330
+ if (controlName !== null && controlName.trim() !== "" && FORM_CONTROLS.includes(controlTag)) {
331
+ const candidate = `${controlTag}[name="${escapeAttrValue(controlName)}"]`;
332
+ if (isUnique(candidate))
333
+ return candidate;
334
+ }
335
+ const explicitRole = target.getAttribute("role");
336
+ const ariaLabel = target.getAttribute("aria-label");
337
+ if (explicitRole !== null && ariaLabel !== null && ariaLabel.trim() !== "") {
338
+ const candidate = `[role="${escapeAttrValue(explicitRole)}"][aria-label="${escapeAttrValue(ariaLabel)}"]`;
339
+ if (isUnique(candidate))
340
+ return candidate;
341
+ }
342
+ return structuralPath(target);
343
+ }
344
+ return { role, name: name.slice(0, 200), selector: computeSelector(el), inShadowRoot, rendered: renderedState() };
345
+ }
346
+ /** Said once, so the cap and the sentence describing it cannot drift apart. */
347
+ const CONTAINER_SCAN_NOTE = "this page holds more elements than the reveal pass scans for scrollable containers, so a list " +
348
+ "inside one past that point was not scrolled and anything only it holds is not above";
349
+ /**
350
+ * One reveal step, run inside the page: scrolls the document **and every scrollable container
351
+ * in it** by just under their own height.
352
+ *
353
+ * The container half is the point. `window.scrollBy` does nothing to a list inside
354
+ * `overflow: auto` — a shape common enough that a window-only reveal reports "nothing below
355
+ * the fold" about a page whose entire content is below one. The step stays under a full
356
+ * viewport so a row sitting on the seam is never scrolled past between two enumerations.
357
+ *
358
+ * Serialized and run in the page, so it closes over nothing in this module — the scan cap is
359
+ * declared inside it for that reason and mirrored in `CONTAINER_SCAN_NOTE` above.
360
+ */
361
+ function revealStep() {
362
+ const win = globalThis;
363
+ const doc = win.document;
364
+ const SCAN_CAP = 5000;
365
+ let scrolled = false;
366
+ if (win.scrollY + win.innerHeight < doc.documentElement.scrollHeight - 1) {
367
+ win.scrollBy(0, Math.round(win.innerHeight * 0.9));
368
+ scrolled = true;
369
+ }
370
+ const all = doc.querySelectorAll("*");
371
+ const limit = all.length > SCAN_CAP ? SCAN_CAP : all.length;
372
+ let containers = 0;
373
+ for (let i = 0; i < limit; i += 1) {
374
+ const node = all.item(i);
375
+ if (node === null)
376
+ continue;
377
+ // Geometry first, style second: the overflow read is the expensive one, and an element
378
+ // whose content fits inside it can never be a scroller whatever its overflow says.
379
+ if (typeof node.scrollHeight !== "number" || typeof node.clientHeight !== "number")
380
+ continue;
381
+ if (node.clientHeight === 0 || node.scrollHeight <= node.clientHeight + 1)
382
+ continue;
383
+ const overflow = typeof win.getComputedStyle === "function" ? win.getComputedStyle(node)?.overflowY : undefined;
384
+ if (overflow !== "auto" && overflow !== "scroll" && overflow !== "overlay")
385
+ continue;
386
+ containers += 1;
387
+ if (node.scrollTop + node.clientHeight < node.scrollHeight - 1) {
388
+ node.scrollBy(0, Math.round(node.clientHeight * 0.9));
389
+ scrolled = true;
390
+ }
391
+ }
392
+ return { y: win.scrollY, scrolled, containers, scanCapped: all.length > SCAN_CAP };
393
+ }
394
+ /** A frame's URL, or "" — a frame detaching mid-snapshot throws from `url()`. */
395
+ function safeUrl(frame) {
396
+ try {
397
+ return typeof frame.url === "function" ? frame.url() : "";
398
+ }
399
+ catch {
400
+ return "";
401
+ }
402
+ }
403
+ /** Something a person can read back: the frame's name, else the iframe element's id or title.
404
+ * Null when it carries none — the reply then falls back to the frame's URL. */
405
+ async function frameLabel(frame) {
406
+ try {
407
+ const declared = typeof frame.name === "function" ? frame.name() : "";
408
+ if (declared !== "")
409
+ return declared;
410
+ if (typeof frame.frameElement !== "function")
411
+ return null;
412
+ const element = await frame.frameElement();
413
+ try {
414
+ if (typeof element.getAttribute !== "function")
415
+ return null;
416
+ const id = await element.getAttribute("id");
417
+ if (id !== null && id.trim() !== "")
418
+ return id;
419
+ const title = await element.getAttribute("title");
420
+ if (title !== null && title.trim() !== "")
421
+ return title;
422
+ return null;
423
+ }
424
+ finally {
425
+ await element.dispose().catch(() => { });
426
+ }
427
+ }
428
+ catch {
429
+ return null;
430
+ }
431
+ }
432
+ /** Identity for reveal accounting. Elements are compared as a multiset, not by ref: a ref is
433
+ * issued per snapshot and two enumerations of the same row never share one. */
434
+ function countKeys(elements) {
435
+ const counts = new Map();
436
+ for (const element of elements) {
437
+ const key = `${element.frame?.id ?? ""}\u0000${element.role}\u0000${element.name}\u0000${element.selector ?? ""}`;
438
+ counts.set(key, (counts.get(key) ?? 0) + 1);
439
+ }
440
+ return counts;
441
+ }
442
+ /** How many of `a` have no counterpart in `b`. */
443
+ function surplus(a, b) {
444
+ let total = 0;
445
+ for (const [key, count] of a)
446
+ total += Math.max(0, count - (b.get(key) ?? 0));
447
+ return total;
448
+ }
449
+ export class BrowserBackendMissingError extends Error {
450
+ constructor(cause) {
451
+ super(`Could not load ${RUNTIME_BROWSER_MODULE}: ${cause instanceof Error ? cause.message : String(cause)}. ` +
452
+ "This build of @descryy/mcp does not have the browser backend installed.");
453
+ this.name = "BrowserBackendMissingError";
454
+ }
455
+ }
456
+ /**
457
+ * RT-263: the real implementation behind `BrowserDriver.probeTargetReachable` /
458
+ * `BrowserPageSession.probeTargetReachable` — single TCP connect, seconds, before
459
+ * either a real Chromium launch (`browser_start_session`) or a real replay step
460
+ * (`browser_run_scenario`) spends the slow path learning the same thing. Lives here,
461
+ * not in either tool, so the fake driver used across `packages/mcp/test`'s
462
+ * `createFakeBrowserDriver`-based suites never runs a real network probe against a
463
+ * URL nothing is actually listening on — the port method is optional precisely so
464
+ * those tests are unaffected by this existing.
465
+ */
466
+ async function probeTargetReachable(targetUrl) {
467
+ let host;
468
+ let port;
469
+ try {
470
+ const parsed = new URL(targetUrl);
471
+ // Only a network target has a port to answer. `about:blank`, `data:` and
472
+ // `file:` pages load without one, so there is nothing to probe.
473
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
474
+ return null;
475
+ host = parsed.hostname;
476
+ port = parsed.port !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
477
+ }
478
+ catch {
479
+ // Not this probe's problem to diagnose -- the real navigate/launch attempt
480
+ // reports its own, more specific error for an unparseable target.
481
+ return null;
482
+ }
483
+ const result = await runPreflight([portAnswersCheck(host, port, `${targetUrl} answers`)]);
484
+ if (result.status !== "broken")
485
+ return { reachable: true, message: "ok" };
486
+ return { reachable: false, message: result.checks.find((check) => !check.ok)?.message ?? `Nothing answered at ${targetUrl}.` };
487
+ }
488
+ const sharedBrowserManagers = new Map();
489
+ function sharedBrowserManagerFor(runtime) {
490
+ if (typeof runtime.createSharedBrowserManager !== "function")
491
+ return null;
492
+ const cached = sharedBrowserManagers.get(runtime);
493
+ if (cached !== undefined)
494
+ return cached;
495
+ const created = runtime.createSharedBrowserManager();
496
+ sharedBrowserManagers.set(runtime, created);
497
+ return created;
498
+ }
499
+ export async function disposeSharedPlaywrightBrowsers() {
500
+ const managers = [...sharedBrowserManagers.values()];
501
+ sharedBrowserManagers.clear();
502
+ await Promise.all(managers.map((manager) => manager.dispose().catch(() => { })));
503
+ }
504
+ export async function createPlaywrightBrowserDriver(options = {}) {
505
+ let runtime;
506
+ try {
507
+ runtime = (await (options.loadRuntimeModule ?? defaultRuntimeModuleLoader)());
508
+ }
509
+ catch (error) {
510
+ throw new BrowserBackendMissingError(error);
511
+ }
512
+ return {
513
+ probeTargetReachable,
514
+ async launch(launchOptions) {
515
+ const auth = runtimeAuthFor(launchOptions);
516
+ const sessionOptions = {
517
+ headless: launchOptions.headless ?? true,
518
+ // B1. This used to be `{ headless }` and nothing else, while the port
519
+ // above already carried `storageState` and a reply note already said
520
+ // it had been seeded. The option reached the fake driver and stopped;
521
+ // against a real browser nothing was ever applied and the note was
522
+ // false. Building the seed here, from the target URL, is also what
523
+ // makes the origin a fact the runtime is *told* rather than one it
524
+ // has to work out.
525
+ ...(auth === null ? {} : { auth }),
526
+ };
527
+ const manager = sharedBrowserManagerFor(runtime);
528
+ const launchNotes = [];
529
+ let launch;
530
+ if (manager === null) {
531
+ launch = await runtime.launchBrowserSessionOrRefuse(sessionOptions);
532
+ launchNotes.push("Started a new browser for this check (this runtime-browser build can't reuse one).");
533
+ }
534
+ else {
535
+ const acquired = await manager.acquireSession(sessionOptions);
536
+ if (acquired.launched && acquired.relaunchedBrowser) {
537
+ launchNotes.push("The shared browser had stopped; Descry started a new one.");
538
+ }
539
+ launch = acquired;
540
+ }
541
+ if (!launch.launched) {
542
+ const { launched: _launched, ...refusal } = launch;
543
+ return { launched: false, ...refusal };
544
+ }
545
+ const session = await openSession(runtime, launch.session, launchOptions, options, launchNotes);
546
+ return { launched: true, session };
547
+ },
548
+ };
549
+ }
550
+ async function openSession(runtime, session, launchOptions, options, launchNotes) {
551
+ const emitted = [];
552
+ const actions = [];
553
+ const unsettled = [];
554
+ // Never cleared, unlike `actions` — the session's whole-lifetime provenance record for
555
+ // `browser_save_scenario`, not per-drain evidence. Goes out of reach the moment this closure
556
+ // does (session closed/reaped) — there is no other path back to it.
557
+ const performed = [];
558
+ // inFlight/lastEventAt track request activity: counted, not slept through.
559
+ let inFlight = 0;
560
+ let lastEventAt = 0;
561
+ session.page.on("request", () => {
562
+ inFlight += 1;
563
+ lastEventAt = Date.now();
564
+ });
565
+ const settleOne = () => {
566
+ inFlight = Math.max(0, inFlight - 1);
567
+ lastEventAt = Date.now();
568
+ };
569
+ session.page.on("requestfinished", settleOne);
570
+ session.page.on("requestfailed", settleOne);
571
+ let refs = new Map();
572
+ let generation = 0;
573
+ let closed = false;
574
+ let finalObservation = null;
575
+ /** Set when a `saveStorageStateTo` write failed. Read by `storageStateSaveFailure()`. */
576
+ let storageStateSaveError = null;
577
+ /** Set when `groundIdentity` was asked for and installed successfully. Every snapshot
578
+ * element with a selector is grounded through it; one with none is never attempted
579
+ * (§B1's `identity: null` distinction — see `driver.ts`). */
580
+ let grounder = null;
581
+ const sessionWarnings = [...launchNotes];
582
+ const context = {
583
+ executionId: `mcp-browser-${session.sessionId}`,
584
+ configuration: { environmentTier: "tier-2-container", fidelityLevel: 2, timeoutMs: 30_000, services: {} },
585
+ emit: (evidence) => {
586
+ emitted.push(evidence);
587
+ },
588
+ ...(options.resolveSourceRoot === undefined ? {} : { resolveSourceRoot: options.resolveSourceRoot }),
589
+ };
590
+ const actionCollector = runtime.createBrowserActionCollector({
591
+ session,
592
+ ...(options.service === undefined ? {} : { service: options.service }),
593
+ });
594
+ const consoleCollector = runtime.createBrowserConsoleCollector({
595
+ session,
596
+ ...(options.service === undefined ? {} : { service: options.service }),
597
+ ...(options.stackParser === undefined ? {} : { stackTraceParser: options.stackParser }),
598
+ });
599
+ const networkCollector = runtime.createBrowserNetworkCollector({
600
+ session,
601
+ ...(options.service === undefined ? {} : { service: options.service }),
602
+ ...(options.stackParser === undefined ? {} : { stackTraceParser: options.stackParser }),
603
+ });
604
+ // Started before first navigation: Playwright's init-script contract means
605
+ // a collector started later misses instrumentation, reporting an
606
+ // honest-looking but empty stack.
607
+ await actionCollector.start(context);
608
+ await consoleCollector.start(context);
609
+ await networkCollector.start(context);
610
+ // Same contract as the collectors above, and for the same reason: the identity probe is
611
+ // an init script too, so it must install before the first navigation or it reports
612
+ // `probe-not-installed` for the document's whole lifetime. Off by default (B1) — a session
613
+ // that never asks for it pays no page.evaluate and loads no module.
614
+ if (options.groundIdentity === true) {
615
+ try {
616
+ const identityModule = (await (options.loadIdentityGroundingModule ?? defaultIdentityGroundingModuleLoader)());
617
+ grounder = identityModule.createFrameworkAwareGrounder(options.resolveSourceRoot === undefined ? {} : { resolveSourceRoot: options.resolveSourceRoot });
618
+ await grounder.install(session.page);
619
+ }
620
+ catch (error) {
621
+ // Honest degradation (rule 7): identity grounding could not be installed, so every
622
+ // element on this session reports `identity: null` — never a guess — and the reason
623
+ // travels on the observation rather than being swallowed.
624
+ grounder = null;
625
+ sessionWarnings.push(`identity grounding was requested (groundIdentity) but could not be installed: ` +
626
+ `${error instanceof Error ? error.message : String(error)}. Every element on this session ` +
627
+ "reports identity: null rather than a guess.");
628
+ }
629
+ }
630
+ await actionCollector.navigate(launchOptions.targetUrl);
631
+ function assertUsable() {
632
+ if (closed || !session.isUsable())
633
+ throw new BrowserSessionCrashedError(session.sessionId);
634
+ }
635
+ function resolve(ref) {
636
+ const found = refs.get(ref);
637
+ if (found === undefined)
638
+ throw new UnknownElementRefError(ref);
639
+ return found;
640
+ }
641
+ function recordAction(action, ref, mode) {
642
+ actions.push({ action, ref, mode, url: session.page.url(), at: new Date().toISOString() });
643
+ }
644
+ function recordPerformed(action, target, url, text) {
645
+ performed.push({ action, target, url, text, at: new Date().toISOString() });
646
+ }
647
+ /** Waits for the action's network effect (started within `ACTIVITY_GRACE_MS`, then quiet for `NETWORK_QUIET_MS`), capped at `SETTLE_TIMEOUT_MS` — reaching the cap is recorded, never swallowed. */
648
+ async function settleAfterAction(action, ref) {
649
+ const startedAt = Date.now();
650
+ lastEventAt = startedAt;
651
+ let sawActivity = inFlight > 0;
652
+ for (;;) {
653
+ const now = Date.now();
654
+ const waitedMs = now - startedAt;
655
+ if (inFlight > 0)
656
+ sawActivity = true;
657
+ if (waitedMs >= SETTLE_TIMEOUT_MS) {
658
+ unsettled.push({ action, ref, waitedMs });
659
+ return;
660
+ }
661
+ if (sawActivity) {
662
+ if (inFlight === 0 && now - lastEventAt >= NETWORK_QUIET_MS)
663
+ return;
664
+ }
665
+ else if (waitedMs >= ACTIVITY_GRACE_MS) {
666
+ return;
667
+ }
668
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
669
+ }
670
+ }
671
+ async function dropRefs() {
672
+ const handles = [...refs.values()].map((r) => r.handle);
673
+ refs = new Map();
674
+ await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
675
+ }
676
+ /** Every document to read, main first. When the backend exposes no frame list the page
677
+ * itself is the only one, and that limitation is reported as an unread frame rather than
678
+ * passed off as "this page has no iframes". */
679
+ function documentsToRead() {
680
+ if (typeof session.page.frames !== "function")
681
+ return { frames: [{ frame: session.page, ref: null }], frameListUnavailable: true };
682
+ const all = session.page.frames();
683
+ const main = typeof session.page.mainFrame === "function" ? session.page.mainFrame() : all[0];
684
+ const out = [];
685
+ let index = 0;
686
+ for (const frame of all) {
687
+ if (frame === main) {
688
+ out.unshift({ frame, ref: null });
689
+ continue;
690
+ }
691
+ index += 1;
692
+ out.push({ frame, ref: { id: `frame-${index}`, url: safeUrl(frame), name: null } });
693
+ }
694
+ if (out.length === 0 || out[0]?.ref !== null)
695
+ out.unshift({ frame: session.page, ref: null });
696
+ return { frames: out, frameListUnavailable: false };
697
+ }
698
+ /** Reads the elements of one document, describing each in a single round trip. */
699
+ async function enumerateFrame(frame, frameRef) {
700
+ const handles = await frame.$$(INTERACTIVE);
701
+ const elements = [];
702
+ let ambiguousInDocument = 0;
703
+ let hidden = 0;
704
+ let untested = 0;
705
+ for (const handle of handles) {
706
+ const described = await handle.evaluate(describeElement).catch(() => null);
707
+ if (described === null) {
708
+ await handle.dispose().catch(() => { });
709
+ continue;
710
+ }
711
+ // The CSS query matches what is in the document; this is what is on the page. A
712
+ // `display:none` menu, a closed modal and an inactive tab panel all match the query,
713
+ // and reporting them as usable controls is Descry stating something false about the
714
+ // user's screen. Withheld and counted, never dropped in silence (rule 7).
715
+ if (described.rendered === false) {
716
+ hidden += 1;
717
+ await handle.dispose().catch(() => { });
718
+ continue;
719
+ }
720
+ // `null` is "nothing could answer". The element is kept — omitting on an unanswered
721
+ // question would hide real controls behind a test that did not run — and the fact that
722
+ // the question went unanswered travels with the snapshot.
723
+ if (described.rendered === null)
724
+ untested += 1;
725
+ let selector = described.selector;
726
+ // A hook unique inside its own shadow root is not necessarily unique in the document:
727
+ // two instances of the same component each carry `[data-testid="submit"]`. Playwright's
728
+ // own CSS resolution pierces, so asking it is the same question a replay would ask.
729
+ if (selector !== null && described.inShadowRoot === true) {
730
+ const matches = await frame.$$(selector).catch(() => null);
731
+ if (matches !== null)
732
+ await Promise.all(matches.map((m) => m.dispose().catch(() => { })));
733
+ if (matches === null || matches.length !== 1) {
734
+ selector = null;
735
+ ambiguousInDocument += 1;
736
+ }
737
+ }
738
+ elements.push({
739
+ handle,
740
+ role: described.role,
741
+ name: described.name,
742
+ // An element inside a frame has no saveable selector: the scenario format carries no
743
+ // field to name the frame, so a stored selector would replay against the main document.
744
+ selector: frameRef === null ? selector : null,
745
+ inShadowRoot: described.inShadowRoot === true,
746
+ frame: frameRef,
747
+ });
748
+ }
749
+ return { elements, ambiguousInDocument, hidden, untested };
750
+ }
751
+ /** Counts custom elements that look like closed shadow hosts. Returns null when the probe could not run. */
752
+ async function probeClosedHosts(frame) {
753
+ if (typeof frame.evaluate !== "function")
754
+ return null;
755
+ return frame
756
+ .evaluate(() => {
757
+ const page = globalThis;
758
+ const all = page.document.querySelectorAll("*");
759
+ let suspected = 0;
760
+ for (let i = 0; i < all.length; i += 1) {
761
+ const element = all.item(i);
762
+ if (element === null)
763
+ continue;
764
+ // A dash in the tag name is what the custom-element spec requires of every one of
765
+ // them; it names no language or framework (rule 1), only the HTML naming rule.
766
+ if (!element.tagName.toLowerCase().includes("-"))
767
+ continue;
768
+ if (element.shadowRoot !== null && element.shadowRoot !== undefined)
769
+ continue;
770
+ if ((element.childElementCount ?? 0) > 0)
771
+ continue;
772
+ suspected += 1;
773
+ }
774
+ return suspected;
775
+ })
776
+ .catch(() => null);
777
+ }
778
+ /** Reads every document, and reports each one it could not read with the reason. */
779
+ async function enumerateAll() {
780
+ const { frames: documents, frameListUnavailable } = documentsToRead();
781
+ const elements = [];
782
+ const reach = [];
783
+ let ambiguousInDocument = 0;
784
+ let suspectedClosedHosts = 0;
785
+ let probed = false;
786
+ let hidden = 0;
787
+ let untested = 0;
788
+ for (const { frame, ref } of documents) {
789
+ let frameRef = ref;
790
+ if (frameRef !== null)
791
+ frameRef = { ...frameRef, name: await frameLabel(frame) };
792
+ try {
793
+ const read = await enumerateFrame(frame, frameRef);
794
+ elements.push(...read.elements);
795
+ ambiguousInDocument += read.ambiguousInDocument;
796
+ hidden += read.hidden;
797
+ untested += read.untested;
798
+ reach.push({ frame: frameRef, enumerated: true, elements: read.elements.length, reason: null });
799
+ }
800
+ catch (error) {
801
+ reach.push({
802
+ frame: frameRef,
803
+ enumerated: false,
804
+ elements: 0,
805
+ reason: `this frame could not be read: ${error instanceof Error ? error.message : String(error)}`,
806
+ });
807
+ continue;
808
+ }
809
+ const closed = await probeClosedHosts(frame);
810
+ if (closed !== null) {
811
+ probed = true;
812
+ suspectedClosedHosts += closed;
813
+ }
814
+ }
815
+ if (frameListUnavailable) {
816
+ reach.push({
817
+ frame: { id: "frames-unavailable", url: "", name: null },
818
+ enumerated: false,
819
+ elements: 0,
820
+ reason: "this browser backend exposes no frame list, so any iframe on this page was not read. " +
821
+ "Nothing here claims the page has none.",
822
+ });
823
+ }
824
+ return {
825
+ elements,
826
+ frames: reach,
827
+ shadow: {
828
+ piercedElements: elements.filter((e) => e.inShadowRoot).length,
829
+ ambiguousInDocument,
830
+ suspectedClosedHosts,
831
+ probed,
832
+ },
833
+ hidden: { withheld: hidden, untested },
834
+ };
835
+ }
836
+ /** Scrolls the document **and every scrollable container in it** a viewport at a time until
837
+ * nothing can move further, or the pass cap stops it. Containers are the half a window-only
838
+ * scroll never reached: a list inside `overflow: auto` does not move when the page does. */
839
+ async function scrollToReveal() {
840
+ const page = session.page;
841
+ if (typeof page.evaluate !== "function") {
842
+ return {
843
+ passes: 0,
844
+ stoppedAtCap: false,
845
+ finalScrollY: 0,
846
+ containersScrolled: 0,
847
+ reason: "this browser backend exposes no page evaluation, so nothing below the fold could be revealed",
848
+ };
849
+ }
850
+ const evaluate = page.evaluate.bind(page);
851
+ let passes = 0;
852
+ let containers = 0;
853
+ let lastY = 0;
854
+ let scanCapped = false;
855
+ for (;;) {
856
+ const step = await evaluate(revealStep).catch(() => null);
857
+ if (step === null) {
858
+ return {
859
+ passes,
860
+ stoppedAtCap: false,
861
+ finalScrollY: lastY,
862
+ containersScrolled: containers,
863
+ reason: "the page stopped answering mid-reveal",
864
+ };
865
+ }
866
+ lastY = step.y;
867
+ containers = Math.max(containers, step.containers);
868
+ scanCapped = scanCapped || step.scanCapped;
869
+ // Nothing left to move: the page and every container it holds are at their end, or
870
+ // none of them can scroll at all. Either way there is nothing further to reveal.
871
+ if (!step.scrolled) {
872
+ return {
873
+ passes,
874
+ stoppedAtCap: false,
875
+ finalScrollY: step.y,
876
+ containersScrolled: containers,
877
+ reason: scanCapped ? CONTAINER_SCAN_NOTE : null,
878
+ };
879
+ }
880
+ passes += 1;
881
+ if (passes >= REVEAL_MAX_PASSES) {
882
+ return {
883
+ passes,
884
+ stoppedAtCap: true,
885
+ finalScrollY: step.y,
886
+ containersScrolled: containers,
887
+ reason: scanCapped ? CONTAINER_SCAN_NOTE : null,
888
+ };
889
+ }
890
+ await new Promise((done) => setTimeout(done, REVEAL_SETTLE_MS));
891
+ }
892
+ }
893
+ /**
894
+ * Refuses a control the page has put out of reach.
895
+ *
896
+ * `click` never calls this: Playwright's own actionability checks already
897
+ * cover it, and duplicating them here would mean two rules that can
898
+ * disagree. `select` and `submit` do, because neither of their underlying
899
+ * operations hit-tests — see `NotOperableError`.
900
+ *
901
+ * A browser that cannot answer is treated as operable rather than refused.
902
+ * Refusing on an unanswered question would turn a missing capability into a
903
+ * false negative on every element, which is the same trade rule 2 rejects
904
+ * pointed the other way.
905
+ */
906
+ async function assertOperable(ref, handle) {
907
+ const state = await handle
908
+ .evaluate((el) => ({
909
+ // `inert` applies to a whole subtree and `HTMLElement.inert` reflects only the
910
+ // attribute on the node it is read from, so this has to walk ancestors.
911
+ inert: el.closest === undefined ? null : el.closest("[inert]") !== null,
912
+ disabled: el.disabled === true,
913
+ }))
914
+ .catch(() => null);
915
+ if (state === null)
916
+ return;
917
+ if (state.inert === true)
918
+ throw new NotOperableError(ref, "inert");
919
+ if (state.disabled)
920
+ throw new NotOperableError(ref, "disabled");
921
+ }
922
+ // Named rather than returned inline: `waitFor` re-enumerates the page each poll and does it
923
+ // by calling this same `snapshot()`, so the refs it hands back come from the one enumeration
924
+ // path instead of a second copy that could drift from it.
925
+ const api = {
926
+ sessionId: session.sessionId,
927
+ startedAt: session.startedAt,
928
+ isUsable: () => !closed && session.isUsable(),
929
+ currentUrl: () => session.page.url(),
930
+ probeTargetReachable,
931
+ async navigate(url) {
932
+ assertUsable();
933
+ await dropRefs();
934
+ await actionCollector.navigate(url);
935
+ recordAction("navigate", null, null);
936
+ recordPerformed("navigate", null, url, null);
937
+ },
938
+ async snapshot(options) {
939
+ assertUsable();
940
+ await dropRefs();
941
+ generation += 1;
942
+ let read = await enumerateAll();
943
+ let reveal = null;
944
+ if (options?.reveal === true) {
945
+ const before = countKeys(read.elements);
946
+ const scrolled = await scrollToReveal();
947
+ if (scrolled.passes > 0) {
948
+ // Handles taken before the scroll may point at rows the list has since destroyed;
949
+ // the page is re-read rather than merged, so every ref handed back is one that
950
+ // exists now. What the scroll cost is reported, not hidden.
951
+ await Promise.all(read.elements.map((e) => e.handle.dispose().catch(() => { })));
952
+ read = await enumerateAll();
953
+ }
954
+ const after = countKeys(read.elements);
955
+ reveal = {
956
+ passes: scrolled.passes,
957
+ containersScrolled: scrolled.containersScrolled,
958
+ revealedElements: surplus(after, before),
959
+ recycledElements: surplus(before, after),
960
+ stoppedAtCap: scrolled.stoppedAtCap,
961
+ finalScrollY: scrolled.finalScrollY,
962
+ framesNotScrolled: read.frames
963
+ .filter((f) => f.frame !== null)
964
+ .map((f) => f.frame?.name ?? f.frame?.url ?? f.frame?.id ?? ""),
965
+ reason: scrolled.reason,
966
+ };
967
+ }
968
+ const elements = [];
969
+ for (const [index, found] of read.elements.entries()) {
970
+ const ref = `ref-${generation}-${index}`;
971
+ const element = {
972
+ ref,
973
+ role: found.role,
974
+ name: found.name,
975
+ selector: found.selector,
976
+ frame: found.frame,
977
+ inShadowRoot: found.inShadowRoot,
978
+ // Grounding resolves a *document* selector, so a framed or hookless element
979
+ // (selector === null) is never attempted — `identity` stays null rather than
980
+ // guessed at, same reasoning `driver.ts` documents on the field itself.
981
+ identity: grounder === null || found.selector === null
982
+ ? null
983
+ : await groundOne(grounder, session.page, found.selector),
984
+ };
985
+ refs.set(ref, { element, handle: found.handle });
986
+ elements.push(element);
987
+ }
988
+ return {
989
+ url: session.page.url(),
990
+ title: await session.page.title().catch(() => ""),
991
+ elements,
992
+ totalElements: elements.length,
993
+ frames: read.frames,
994
+ shadow: read.shadow,
995
+ hidden: read.hidden,
996
+ reveal,
997
+ };
998
+ },
999
+ async click(ref) {
1000
+ assertUsable();
1001
+ const { element, handle } = resolve(ref);
1002
+ await handle.click();
1003
+ recordAction("click", ref, null);
1004
+ recordPerformed("click", { role: element.role, name: element.name, selector: element.selector }, null, null);
1005
+ // requestfinished fires after the page's handler resolves; draining immediately would miss the very request being watched for.
1006
+ await settleAfterAction("click", ref);
1007
+ },
1008
+ async type(ref, text) {
1009
+ assertUsable();
1010
+ const { element, handle } = resolve(ref);
1011
+ if (handle.pressSequentially !== undefined)
1012
+ await handle.pressSequentially(text);
1013
+ else if (handle.type !== undefined)
1014
+ await handle.type(text);
1015
+ else
1016
+ throw new Error("this Playwright build exposes no keystroke-level typing method");
1017
+ recordAction("type", ref, "append");
1018
+ recordPerformed("type", { role: element.role, name: element.name, selector: element.selector }, null, text);
1019
+ await settleAfterAction("type", ref);
1020
+ },
1021
+ async fill(ref, text) {
1022
+ assertUsable();
1023
+ const { element, handle } = resolve(ref);
1024
+ await handle.fill(text);
1025
+ recordAction("fill", ref, "replace");
1026
+ recordPerformed("fill", { role: element.role, name: element.name, selector: element.selector }, null, text);
1027
+ await settleAfterAction("fill", ref);
1028
+ },
1029
+ async select(ref, value) {
1030
+ assertUsable();
1031
+ const { element, handle } = resolve(ref);
1032
+ await assertOperable(ref, handle);
1033
+ // Read what the element actually offers before touching it. Playwright's own
1034
+ // selectOption would time out on a non-match and say nothing useful about why; the
1035
+ // refusal a caller can act on has to carry the real option list, and that means one
1036
+ // round trip to read it.
1037
+ const offered = await handle.evaluate((el) => {
1038
+ if (el.options === undefined)
1039
+ return null;
1040
+ const out = [];
1041
+ for (let i = 0; i < el.options.length; i += 1) {
1042
+ const option = el.options[i];
1043
+ out.push({ value: option.value, label: option.label === "" ? option.text : option.label });
1044
+ }
1045
+ return out;
1046
+ });
1047
+ if (offered === null)
1048
+ throw new NotSelectableError(ref, element.role);
1049
+ // By stored value first, then by the label a person reads — both exact. A caller
1050
+ // reading a snapshot sees labels and a caller reading the page's source sees values;
1051
+ // neither should have to know which one this element uses.
1052
+ const matched = offered.find((o) => o.value === value) ?? offered.find((o) => o.label.trim() === value.trim());
1053
+ if (matched === undefined) {
1054
+ throw new NoSuchOptionError(ref, value, offered.map((o) => (o.label.trim() === "" ? o.value : o.label)));
1055
+ }
1056
+ if (handle.selectOption === undefined)
1057
+ throw new Error("this Playwright build exposes no option-selecting method");
1058
+ await handle.selectOption(matched.value);
1059
+ recordAction("select", ref, "replace");
1060
+ recordPerformed("select", { role: element.role, name: element.name, selector: element.selector }, null, matched.value);
1061
+ await settleAfterAction("select", ref);
1062
+ },
1063
+ async submit(ref) {
1064
+ assertUsable();
1065
+ const { element, handle } = resolve(ref);
1066
+ await assertOperable(ref, handle);
1067
+ // Submitted through the form's own submit control rather than by synthesising a bare
1068
+ // submit event: that is what a person's click does, it names the submitter the page's
1069
+ // handler reads, and it runs the form's validation. A form with no such control is
1070
+ // submitted by script on some other control — refused, not guessed at.
1071
+ const outcome = await handle.evaluate((el) => {
1072
+ const form = el.closest === undefined ? null : el.closest("form");
1073
+ if (form === null)
1074
+ return "noForm";
1075
+ const control = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
1076
+ if (control === null)
1077
+ return "noSubmitControl";
1078
+ form.requestSubmit(control);
1079
+ return "submitted";
1080
+ });
1081
+ if (outcome !== "submitted")
1082
+ throw new NotSubmittableError(ref, outcome);
1083
+ recordAction("submit", ref, null);
1084
+ recordPerformed("submit", { role: element.role, name: element.name, selector: element.selector }, null, null);
1085
+ await settleAfterAction("submit", ref);
1086
+ },
1087
+ async waitFor(target, waitOptions = {}) {
1088
+ assertUsable();
1089
+ if (isEmptyWaitTarget(target)) {
1090
+ throw new Error("browser_wait_for needs a target: give a role, a name, or both. Waiting for nothing in particular " +
1091
+ `would return the first element on the page, which is not what any caller means. Got: ${describeWaitTarget(target)}.`);
1092
+ }
1093
+ const timeoutMs = clampWaitTimeout(waitOptions.timeoutMs);
1094
+ const startedAt = Date.now();
1095
+ for (;;) {
1096
+ assertUsable();
1097
+ // Re-enumerates each poll rather than watching one selector: the port has no
1098
+ // selector to watch, and a fresh snapshot is what makes the matched element
1099
+ // actionable at all. That cost is real, and is why the interval is not tighter.
1100
+ const snapshot = await api.snapshot();
1101
+ const matches = snapshot.elements.filter((e) => matchesWaitTarget(e, target));
1102
+ const waitedMs = Date.now() - startedAt;
1103
+ if (matches.length > 0) {
1104
+ return { appeared: true, waitedMs, timeoutMs, matched: matches[0], matchCount: matches.length, snapshot };
1105
+ }
1106
+ if (waitedMs >= timeoutMs) {
1107
+ return { appeared: false, waitedMs, timeoutMs, matched: null, matchCount: 0, snapshot };
1108
+ }
1109
+ await new Promise((done) => setTimeout(done, WAIT_POLL_INTERVAL_MS));
1110
+ }
1111
+ },
1112
+ async injectStorageState(origin, state) {
1113
+ assertUsable();
1114
+ if (typeof session.injectStorageState !== "function") {
1115
+ // Honest degradation (rule 7), same shape the groundIdentity install
1116
+ // failure above uses: an older runtime-browser build predates this
1117
+ // primitive, so nothing was injected and the reason travels on the
1118
+ // result rather than a silent no-op or a thrown TypeError.
1119
+ return {
1120
+ cookiesInjected: 0,
1121
+ localStorageInjected: 0,
1122
+ localStorageSkipped: "this build of @descryy/runtime-browser does not export injectStorageState -- nothing was injected. " +
1123
+ "Republish runtime-browser at a version that includes BrowserSession.injectStorageState.",
1124
+ };
1125
+ }
1126
+ return session.injectStorageState({
1127
+ origin,
1128
+ ...(state.cookies === undefined ? {} : { cookies: state.cookies }),
1129
+ ...(state.localStorage === undefined ? {} : { localStorage: state.localStorage }),
1130
+ });
1131
+ },
1132
+ async drainEvidence() {
1133
+ const observation = collect(emitted, actions, unsettled);
1134
+ emitted.length = 0;
1135
+ actions.length = 0;
1136
+ unsettled.length = 0;
1137
+ return withSessionWarnings(observation, sessionWarnings);
1138
+ },
1139
+ performedSteps() {
1140
+ return [...performed];
1141
+ },
1142
+ storageStateSaveFailure: () => storageStateSaveError,
1143
+ /**
1144
+ * §4.2's probe. Reads the page and counts; decides nothing — `estimateReach`
1145
+ * is the only thing that turns these counts into a claim, so a change of
1146
+ * policy never touches a driver.
1147
+ *
1148
+ * The body lives in `page-probe.ts` rather than here: this file belongs to
1149
+ * another lane, and a six-line delegation is mergeable where two hundred
1150
+ * lines of DOM walking would not be.
1151
+ */
1152
+ async fingerprint() {
1153
+ assertUsable();
1154
+ const [root] = await session.page.$$("html");
1155
+ if (root === undefined) {
1156
+ // No document element at all. Refusing is right: a fingerprint of
1157
+ // zeroes would read as a page with nothing on it.
1158
+ throw new Error("the page exposed no document element to probe");
1159
+ }
1160
+ try {
1161
+ return await root.evaluate(probeInPage, PROBE_CONSTANTS);
1162
+ }
1163
+ finally {
1164
+ await root.dispose();
1165
+ }
1166
+ },
1167
+ async close() {
1168
+ if (finalObservation !== null)
1169
+ return finalObservation;
1170
+ closed = true;
1171
+ // Stopped before observing: a stop flushes what each collector was
1172
+ // still holding, which observing first would drop.
1173
+ await actionCollector.stop().catch(() => { });
1174
+ await consoleCollector.stop().catch(() => { });
1175
+ await networkCollector.stop().catch(() => { });
1176
+ const observation = withSessionWarnings(collect(emitted, actions, unsettled), sessionWarnings);
1177
+ emitted.length = 0;
1178
+ actions.length = 0;
1179
+ unsettled.length = 0;
1180
+ await dropRefs();
1181
+ // Before `session.close()`, not after: a closed context holds no
1182
+ // cookies to read, and a save that ran afterwards would write a file
1183
+ // that looks like a session with nothing in it — which is worse than no
1184
+ // file, because the next run would seed from it and silently drive the
1185
+ // logged-out surface. A failure here is reported as a note by the
1186
+ // caller rather than swallowed, but it never fails the close: evidence
1187
+ // already collected must not be lost to a write that did not work.
1188
+ if (launchOptions.saveStorageStateTo !== undefined && session.saveStorageState !== undefined) {
1189
+ try {
1190
+ await session.saveStorageState(launchOptions.saveStorageStateTo);
1191
+ }
1192
+ catch (error) {
1193
+ storageStateSaveError = error instanceof Error ? error.message : String(error);
1194
+ }
1195
+ }
1196
+ await session.close().catch(() => { });
1197
+ finalObservation = observation;
1198
+ return observation;
1199
+ },
1200
+ };
1201
+ return api;
1202
+ }
1203
+ /** Turns collector evidence into the port's vocabulary. Joins request/response on `requestId`, keeping `status: null` (not guessed) when unanswered. Exported for `browser-request-outcome-mapping.test.ts`. */
1204
+ export function collect(emitted, actions, unsettled) {
1205
+ // `requestId` is a correlation header the app must itself propagate, not a
1206
+ // transport id — an id-only join silently reported `status: null` for
1207
+ // every request without one, i.e. most of them, hence the URL fallback
1208
+ // below. `status` and `outcome` are read from the *same* terminal event and
1209
+ // stored together: two independent last-write-wins maps let a later
1210
+ // terminal event for a *different* request overwrite one field but not the
1211
+ // other (e.g. an id-less "unanswered" HTTP_ERROR with no numeric status
1212
+ // updates only `outcome`), so a third, genuinely-still-pending id-less
1213
+ // request sharing that URL could read back `status: 200` from one sibling
1214
+ // and `outcome: "unanswered"` from another — a self-contradictory pair
1215
+ // that is a real, reported defect (P6-D5), not a hypothetical one.
1216
+ //
1217
+ // **B4: that pairing was necessary and was not sufficient.** Three routes to
1218
+ // a false fact survived it, each verified here rather than reasoned about,
1219
+ // and each fixed at the line it occurs on below: the pair itself was never
1220
+ // checked for self-consistency across a boundary that versions separately;
1221
+ // "paired" was a set-membership test on the URL rather than a record of what
1222
+ // this request consumed, so an in-flight sibling read as failed; and the
1223
+ // HTTP_ERROR `descry-runtime` emits *alongside* a >= 400 NETWORK_RESPONSE
1224
+ // took a second queue slot, handing one exchange's 500 to two requests.
1225
+ const byRequestId = new Map();
1226
+ // C3. A **transport-level** identity, when the collector carries one: an id
1227
+ // the browser's own request object owns, not a header an application chose
1228
+ // to propagate. It outranks `requestId` because it cannot be absent for
1229
+ // application reasons, and where it is present the whole out-of-order
1230
+ // question below simply does not arise. `descry-runtime`'s network
1231
+ // collector already holds this identity — its pending map is keyed by the
1232
+ // Playwright `Request` object itself, "not a URL or an id, which could
1233
+ // collide across concurrent requests to the same endpoint" — and does not
1234
+ // yet emit it. Read here the same way `outcome` was read before its
1235
+ // producer shipped: validated, optional, and absent degrades to the join
1236
+ // below rather than to a guess. See
1237
+ // `DEC-NEXT-transport-level-exchange-id-for-browser-requests`.
1238
+ const byExchangeId = new Map();
1239
+ // FIFO per URL, fed only by unidentified terminal events. A URL is not
1240
+ // unique the way an id is — a page can issue the same URL more than once —
1241
+ // so a shared cache must hand each terminal event's (status, outcome) pair
1242
+ // to at most one request, in arrival order, rather than broadcasting the
1243
+ // most recent one to every request sharing that URL. An id-bearing terminal
1244
+ // event is deliberately excluded from this pool: it is already claimed
1245
+ // evidence for one specific request, and folding it into the shared pool
1246
+ // would just relocate the same contamination.
1247
+ //
1248
+ // **C3: arrival order is not an answer, so it is no longer used as one.**
1249
+ // Two unidentified requests to one URL whose responses arrive out of order
1250
+ // swap records, and nothing at this layer can tell an in-order pair from a
1251
+ // swapped one. A FIFO is therefore right about half the time and silent
1252
+ // when it is wrong — the exact shape rule 2 forbids, because the failure
1253
+ // leaves no trace in the reply. Where the assignment is genuinely
1254
+ // undetermined the queue is not consulted at all and every request in that
1255
+ // bucket is reported `unattributed` with the terminal events disclosed by
1256
+ // URL instead. Where it is determined — one unidentified request, or
1257
+ // several whose records are indistinguishable from one another, so that
1258
+ // every possible assignment yields the same answer — it still pairs, since
1259
+ // refusing there would trade a silent wrong answer for a loud missing one
1260
+ // that nothing required.
1261
+ const queueByUrl = new Map();
1262
+ const warnings = new Set();
1263
+ // URLs that produced an id-less NETWORK_RESPONSE, so the duplicate-HTTP_ERROR
1264
+ // exclusion below can require its sibling rather than assume it. A build that
1265
+ // ever emitted a numeric-status HTTP_ERROR *alone* would otherwise have it
1266
+ // silently dropped, turning a real 4xx into "pending".
1267
+ const respondedUrls = new Set();
1268
+ for (const evidence of emitted) {
1269
+ if (evidence.eventType !== "NETWORK_RESPONSE" || evidence.requestId !== null)
1270
+ continue;
1271
+ if (exchangeIdOf(evidence.payload) !== null)
1272
+ continue;
1273
+ const url = evidence.payload["url"];
1274
+ if (typeof url === "string")
1275
+ respondedUrls.add(url);
1276
+ }
1277
+ for (const evidence of emitted) {
1278
+ if (evidence.eventType !== "NETWORK_RESPONSE" && evidence.eventType !== "HTTP_ERROR")
1279
+ continue;
1280
+ const url = evidence.payload["url"];
1281
+ const rawStatus = evidence.payload["status"];
1282
+ const status = typeof rawStatus === "number" ? rawStatus : null;
1283
+ // B4. The pair is validated, not just each field: this payload crosses a
1284
+ // package boundary that versions separately, which is the same reason
1285
+ // `requestOutcomeOf` validates instead of casting. A numeric status means
1286
+ // a response arrived; "unanswered" means none ever did. Both cannot be
1287
+ // true of one request, and forwarding the pair verbatim is what produced
1288
+ // the `status: 200` + `outcome: "unanswered"` record the cal.com pass
1289
+ // reported. Neither field can be trusted to correct the other, so the
1290
+ // directly-observed status is kept and the outcome degrades to the same
1291
+ // "not reported" state an older collector build produces — and it is
1292
+ // named in the reply rather than dropped quietly (rule 7).
1293
+ const outcome = reconcile(status, requestOutcomeOf(evidence.payload), url, warnings);
1294
+ const record = { status, outcome };
1295
+ const exchangeId = exchangeIdOf(evidence.payload);
1296
+ if (exchangeId !== null) {
1297
+ byExchangeId.set(exchangeId, record);
1298
+ continue;
1299
+ }
1300
+ if (evidence.requestId !== null) {
1301
+ byRequestId.set(evidence.requestId, record);
1302
+ continue;
1303
+ }
1304
+ if (typeof url !== "string")
1305
+ continue;
1306
+ // B4. One exchange, one slot. `descry-runtime`'s collector emits
1307
+ // NETWORK_RESPONSE *and* a payload-sharing HTTP_ERROR for a single
1308
+ // response with status >= 400, and a numeric status is the only shape in
1309
+ // which it ever emits HTTP_ERROR — every other HTTP_ERROR it produces
1310
+ // (requestfailed, its own timeout, a websocket socketerror, a session
1311
+ // closing mid-flight) carries `status: null` and has no NETWORK_RESPONSE
1312
+ // beside it. Keyed by requestId that duplicate is harmless (same content,
1313
+ // one map entry); in this queue it took a second slot, so one 500
1314
+ // exchange handed a 500 to a second request that never got a response at
1315
+ // all. Excluded here only — an id-keyed one still records, in case a
1316
+ // build ever emits it without its sibling.
1317
+ if (evidence.eventType === "HTTP_ERROR" && status !== null && respondedUrls.has(url))
1318
+ continue;
1319
+ const queue = queueByUrl.get(url);
1320
+ if (queue === undefined)
1321
+ queueByUrl.set(url, [record]);
1322
+ else
1323
+ queue.push(record);
1324
+ }
1325
+ /** The record this evidence owns outright, or undefined when it must draw from the shared
1326
+ * URL queue. Exchange id first: it identifies the transport exchange, where `requestId`
1327
+ * identifies whatever the application decided to label. */
1328
+ const keyedRecordFor = (evidence) => {
1329
+ const exchangeId = exchangeIdOf(evidence.payload);
1330
+ if (exchangeId !== null) {
1331
+ const byExchange = byExchangeId.get(exchangeId);
1332
+ if (byExchange !== undefined)
1333
+ return byExchange;
1334
+ }
1335
+ return evidence.requestId === null ? undefined : byRequestId.get(evidence.requestId);
1336
+ };
1337
+ // C3. How many requests to each URL have no record of their own — exactly the set that
1338
+ // draws from the shared queue, and therefore exactly the set the queue can be wrong about.
1339
+ // Counted after the maps are built, not from the presence of an id: a request carrying an
1340
+ // id that never came back on a terminal event falls through to the queue like any other,
1341
+ // and counting it as identified would leave the queue one short of the requests using it.
1342
+ const unkeyedByUrl = new Map();
1343
+ for (const evidence of emitted) {
1344
+ if (evidence.eventType !== "NETWORK_REQUEST")
1345
+ continue;
1346
+ const url = evidence.payload["url"];
1347
+ if (typeof url !== "string")
1348
+ continue;
1349
+ if (keyedRecordFor(evidence) !== undefined)
1350
+ continue;
1351
+ unkeyedByUrl.set(url, (unkeyedByUrl.get(url) ?? 0) + 1);
1352
+ }
1353
+ // C3. The URLs whose assignment is undetermined. Not "has duplicates" — having two requests
1354
+ // to one URL is ordinary and usually harmless. It is undetermined only when two different
1355
+ // assignments would produce two different replies.
1356
+ const undeterminedUrls = new Set();
1357
+ for (const [url, records] of queueByUrl) {
1358
+ const unkeyed = unkeyedByUrl.get(url) ?? 0;
1359
+ if (unkeyed <= 1)
1360
+ continue;
1361
+ const first = records[0];
1362
+ if (first === undefined)
1363
+ continue;
1364
+ const alike = records.every((record) => record.status === first.status && record.outcome === first.outcome);
1365
+ // Enough alike records to go round means every request gets the same one, so which is
1366
+ // which never comes up. One short means at least one request is still in flight and
1367
+ // nothing can say which — and "pending" on the wrong one is a false fact about a healthy
1368
+ // request, which is the defect B4 fixed in its first costume.
1369
+ if (alike && records.length >= unkeyed)
1370
+ continue;
1371
+ undeterminedUrls.add(url);
1372
+ warnings.add(undeterminedJoinWarning(url, unkeyed, records));
1373
+ }
1374
+ const requests = [];
1375
+ const consoleErrors = [];
1376
+ for (const evidence of emitted) {
1377
+ if (evidence.eventType === "NETWORK_REQUEST") {
1378
+ const method = evidence.payload["method"];
1379
+ const url = evidence.payload["url"];
1380
+ if (typeof method !== "string" || typeof url !== "string")
1381
+ continue;
1382
+ // The keyed match wins when it exists; only a request with no record of its own draws
1383
+ // from the shared URL queue, and it draws once. C3: on an undetermined URL the queue is
1384
+ // not consulted at all — consuming from it is what produced the swap.
1385
+ const keyed = keyedRecordFor(evidence);
1386
+ const undetermined = keyed === undefined && undeterminedUrls.has(url);
1387
+ const matched = keyed ?? (undetermined ? undefined : queueByUrl.get(url)?.shift());
1388
+ // B4. Paired is what this request *consumed*, not what its URL was seen
1389
+ // in. It used to be a set-membership test on the URL, so a second,
1390
+ // genuinely in-flight request to an already-answered URL came out with
1391
+ // no outcome at all — which `isFailedRequest`'s older-build fallback
1392
+ // reads as a failure, because its status is null. A healthy in-flight
1393
+ // request reported as a failed one. No record consumed means no terminal
1394
+ // event was ever attributable to this request, and "pending" is exactly
1395
+ // that fact. An unrecognised outcome on a record that *was* consumed
1396
+ // still leaves the field undefined, which is the older-build case the
1397
+ // fallback exists for.
1398
+ // C3. `unattributed` before `pending`: a terminal event for this URL exists, so claiming
1399
+ // the request is still in flight would be as wrong as handing it a sibling's status —
1400
+ // and wrong in the direction that reads as healthy.
1401
+ const outcome = undetermined
1402
+ ? "unattributed"
1403
+ : matched === undefined
1404
+ ? "pending"
1405
+ : matched.outcome;
1406
+ requests.push({
1407
+ method,
1408
+ url,
1409
+ status: undetermined ? null : (matched?.status ?? null),
1410
+ ...(outcome === undefined ? {} : { outcome }),
1411
+ stackText: renderStack(evidence.stackTrace),
1412
+ stackTrace: evidence.stackTrace,
1413
+ scriptUrlMapping: scriptUrlOutcomesOf(evidence.payload),
1414
+ at: evidence.timestamp,
1415
+ });
1416
+ continue;
1417
+ }
1418
+ // Uncaught page error and console error both mean "something went wrong"; callers don't need to know which listener fired.
1419
+ if (evidence.eventType === "EXCEPTION" || evidence.eventType === "CONSOLE_MESSAGE") {
1420
+ const level = evidence.payload["level"] ?? evidence.payload["type"];
1421
+ if (evidence.eventType === "CONSOLE_MESSAGE" && level !== "error")
1422
+ continue;
1423
+ const text = evidence.payload["text"] ?? evidence.payload["message"];
1424
+ consoleErrors.push({
1425
+ text: typeof text === "string" ? text : JSON.stringify(evidence.payload),
1426
+ stackText: renderStack(evidence.stackTrace),
1427
+ scriptUrlMapping: scriptUrlOutcomesOf(evidence.payload),
1428
+ at: evidence.timestamp,
1429
+ });
1430
+ }
1431
+ }
1432
+ return { actions: [...actions], consoleErrors, requests, unsettled: [...unsettled], evidenceWarnings: [...warnings] };
1433
+ }
1434
+ /** B4's pair check. Returns the outcome that may be published alongside `status`, dropping it —
1435
+ * and naming the drop in `warnings` — when the two cannot both be true. Deliberately one-sided:
1436
+ * a status is a number the collector read off a real response object, an outcome is a label it
1437
+ * derived, so when they disagree the derived half is the one that goes. `url` only names the
1438
+ * request in the warning; a missing one degrades the text, never the check. */
1439
+ function reconcile(status, outcome, url, warnings) {
1440
+ const impossible = (outcome === "unanswered" && status !== null) || (outcome === "answered" && status === null);
1441
+ if (!impossible)
1442
+ return outcome;
1443
+ const where = typeof url === "string" ? ` for ${url}` : "";
1444
+ warnings.add(`Descry's browser collector reported a request${where} as both "${outcome}" and ` +
1445
+ `status ${status === null ? "null" : String(status)}, which cannot both be true. The status is ` +
1446
+ "what was read off the response, so it is kept and the outcome is dropped — this request is " +
1447
+ "reported without one rather than with a contradiction. Usually a version mismatch between " +
1448
+ "@descryy/mcp and @descryy/runtime-browser.");
1449
+ return undefined;
1450
+ }
1451
+ /** Reads `outcome` off an untyped cross-package payload; validated not cast, since this build doesn't depend on `@descryy/runtime-browser`'s types — unrecognised values degrade to undefined, never a guessed literal. `"unattributed"` is deliberately not accepted here: it is this package's verdict about a join it could not make, and a collector is in no position to assert it. */
1452
+ function requestOutcomeOf(payload) {
1453
+ const raw = payload["outcome"];
1454
+ if (raw === "answered" || raw === "unanswered" || raw === "pending")
1455
+ return raw;
1456
+ return undefined;
1457
+ }
1458
+ /** Reads the collector's transport-level exchange id, when it carries one. Validated not cast,
1459
+ * and an empty string is treated as absent — an id that identifies nothing would put every
1460
+ * request into one bucket, which is worse than having none. */
1461
+ function exchangeIdOf(payload) {
1462
+ const raw = payload["exchangeId"];
1463
+ return typeof raw === "string" && raw !== "" ? raw : null;
1464
+ }
1465
+ /** C3's disclosure. Says what was observed and refuses to say whose it was — the terminal
1466
+ * events are named in full so a real 500 is never lost, only left unassigned. */
1467
+ function undeterminedJoinWarning(url, unkeyed, records) {
1468
+ const describe = (record) => `${record.status === null ? "no status" : `status ${record.status}`}${record.outcome === undefined ? "" : ` (${record.outcome})`}`;
1469
+ const shortfall = records.length < unkeyed
1470
+ ? ` Fewer terminal events arrived than requests were issued, so at least one of them was still in flight, and which one is the same unanswerable question.`
1471
+ : "";
1472
+ return (`Descry saw ${unkeyed} requests to ${url} carrying no id of their own, and ${records.length} terminal ` +
1473
+ `event(s) for that URL that do not all say the same thing: ${records.map(describe).join("; ")}.${shortfall} ` +
1474
+ "Pairing them by arrival order would be right roughly half the time and silent when it was wrong, so none " +
1475
+ "of these requests is reported with a status or an outcome. The events just listed did happen — Descry " +
1476
+ "cannot say which request each one belongs to. A transport-level exchange id from the browser collector " +
1477
+ "would remove this; the collector in this build does not emit one.");
1478
+ }
1479
+ /** Reads `scriptUrlMapping` off an untyped cross-package payload; validated not cast, so a malformed/absent field degrades to "no mapping reported" rather than a half-populated result. */
1480
+ function scriptUrlOutcomesOf(payload) {
1481
+ const raw = payload["scriptUrlMapping"];
1482
+ if (!Array.isArray(raw))
1483
+ return [];
1484
+ const outcomes = [];
1485
+ for (const entry of raw) {
1486
+ if (typeof entry !== "object" || entry === null)
1487
+ continue;
1488
+ const record = entry;
1489
+ const url = record["url"];
1490
+ if (typeof url !== "string")
1491
+ continue;
1492
+ const mapped = record["mapped"];
1493
+ const reason = record["reason"];
1494
+ outcomes.push({
1495
+ url,
1496
+ mapped: typeof mapped === "string" ? mapped : null,
1497
+ reason: typeof reason === "string" ? reason : null,
1498
+ });
1499
+ }
1500
+ return outcomes;
1501
+ }
1502
+ /** Human-readable rendering of parsed stack frames, for the reply; the R4 write uses `stackTrace` itself, never this. Primary frame marked since it isn't always the innermost. */
1503
+ function renderStack(stack) {
1504
+ if (stack === null || stack.frames.length === 0)
1505
+ return null;
1506
+ return stack.frames
1507
+ .slice(0, 5)
1508
+ .map((frame, index) => {
1509
+ const where = `${frame.location.file}:${frame.location.line}`;
1510
+ const name = frame.location.functionName ?? "(anonymous)";
1511
+ return `${index === stack.primaryFrameIndex ? "> " : " "}at ${name} (${where})`;
1512
+ })
1513
+ .join("\n");
1514
+ }
1515
+ /**
1516
+ * The runtime's auth seed for this launch, or null when the caller named none.
1517
+ *
1518
+ * The origin comes from `targetUrl` and from nowhere else — it is the same
1519
+ * value the registry records as the session's consented origin, so "the seed
1520
+ * is scoped to what was consented" is a property of one derivation rather
1521
+ * than of two that have to agree.
1522
+ */
1523
+ function runtimeAuthFor(launchOptions) {
1524
+ const state = launchOptions.storageState;
1525
+ const headers = launchOptions.extraHttpHeaders;
1526
+ if (state === undefined && headers === undefined)
1527
+ return null;
1528
+ let origin;
1529
+ try {
1530
+ origin = new URL(launchOptions.targetUrl).origin;
1531
+ }
1532
+ catch {
1533
+ // Unparseable targets are refused far above this — the tool cannot mint
1534
+ // consent for a URL it could not read an origin from. Carrying the raw
1535
+ // string keeps this total rather than throwing from inside a launch.
1536
+ origin = launchOptions.targetUrl;
1537
+ }
1538
+ return {
1539
+ origin,
1540
+ ...(state?.cookies === undefined ? {} : { cookies: state.cookies }),
1541
+ ...(state?.localStorage === undefined ? {} : { localStorage: state.localStorage }),
1542
+ ...(headers === undefined ? {} : { extraHttpHeaders: headers }),
1543
+ };
1544
+ }
1545
+ //# sourceMappingURL=playwright-driver.js.map