@descryy/mcp 0.2.0 → 0.4.0

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 (160) hide show
  1. package/dist/bin/descry-mcp.js +6 -1
  2. package/dist/bin/descry-mcp.js.map +1 -1
  3. package/dist/browser/driver.d.ts +236 -0
  4. package/dist/browser/driver.d.ts.map +1 -0
  5. package/dist/browser/driver.js +70 -0
  6. package/dist/browser/driver.js.map +1 -0
  7. package/dist/browser/evidence.d.ts +47 -0
  8. package/dist/browser/evidence.d.ts.map +1 -0
  9. package/dist/browser/evidence.js +55 -0
  10. package/dist/browser/evidence.js.map +1 -0
  11. package/dist/browser/fake-driver.d.ts +58 -0
  12. package/dist/browser/fake-driver.d.ts.map +1 -0
  13. package/dist/browser/fake-driver.js +161 -0
  14. package/dist/browser/fake-driver.js.map +1 -0
  15. package/dist/browser/graph-write.d.ts +86 -0
  16. package/dist/browser/graph-write.d.ts.map +1 -0
  17. package/dist/browser/graph-write.js +135 -0
  18. package/dist/browser/graph-write.js.map +1 -0
  19. package/dist/browser/playwright-driver.d.ts +65 -0
  20. package/dist/browser/playwright-driver.d.ts.map +1 -0
  21. package/dist/browser/playwright-driver.js +359 -0
  22. package/dist/browser/playwright-driver.js.map +1 -0
  23. package/dist/browser/provider.d.ts +49 -0
  24. package/dist/browser/provider.d.ts.map +1 -0
  25. package/dist/browser/provider.js +28 -0
  26. package/dist/browser/provider.js.map +1 -0
  27. package/dist/browser/registry.d.ts +182 -0
  28. package/dist/browser/registry.d.ts.map +1 -0
  29. package/dist/browser/registry.js +215 -0
  30. package/dist/browser/registry.js.map +1 -0
  31. package/dist/browser/scenario-resolve.d.ts +62 -0
  32. package/dist/browser/scenario-resolve.d.ts.map +1 -0
  33. package/dist/browser/scenario-resolve.js +127 -0
  34. package/dist/browser/scenario-resolve.js.map +1 -0
  35. package/dist/browser/scenario-runner.d.ts +113 -0
  36. package/dist/browser/scenario-runner.d.ts.map +1 -0
  37. package/dist/browser/scenario-runner.js +315 -0
  38. package/dist/browser/scenario-runner.js.map +1 -0
  39. package/dist/browser/stack-parser.d.ts +43 -0
  40. package/dist/browser/stack-parser.d.ts.map +1 -0
  41. package/dist/browser/stack-parser.js +112 -0
  42. package/dist/browser/stack-parser.js.map +1 -0
  43. package/dist/browser/tool-support.d.ts +113 -0
  44. package/dist/browser/tool-support.d.ts.map +1 -0
  45. package/dist/browser/tool-support.js +181 -0
  46. package/dist/browser/tool-support.js.map +1 -0
  47. package/dist/index.d.ts +11 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +10 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/render.d.ts +112 -10
  52. package/dist/render.d.ts.map +1 -1
  53. package/dist/render.js +79 -13
  54. package/dist/render.js.map +1 -1
  55. package/dist/scenarios/index.d.ts +4 -0
  56. package/dist/scenarios/index.d.ts.map +1 -0
  57. package/dist/scenarios/index.js +4 -0
  58. package/dist/scenarios/index.js.map +1 -0
  59. package/dist/scenarios/parse.d.ts +32 -0
  60. package/dist/scenarios/parse.d.ts.map +1 -0
  61. package/dist/scenarios/parse.js +199 -0
  62. package/dist/scenarios/parse.js.map +1 -0
  63. package/dist/scenarios/scenario.d.ts +115 -0
  64. package/dist/scenarios/scenario.d.ts.map +1 -0
  65. package/dist/scenarios/scenario.js +41 -0
  66. package/dist/scenarios/scenario.js.map +1 -0
  67. package/dist/scenarios/storage.d.ts +51 -0
  68. package/dist/scenarios/storage.d.ts.map +1 -0
  69. package/dist/scenarios/storage.js +93 -0
  70. package/dist/scenarios/storage.js.map +1 -0
  71. package/dist/server.d.ts.map +1 -1
  72. package/dist/server.js +6 -3
  73. package/dist/server.js.map +1 -1
  74. package/dist/session.d.ts +68 -0
  75. package/dist/session.d.ts.map +1 -1
  76. package/dist/session.js +89 -0
  77. package/dist/session.js.map +1 -1
  78. package/dist/tools/analyze.d.ts +41 -1
  79. package/dist/tools/analyze.d.ts.map +1 -1
  80. package/dist/tools/analyze.js +81 -8
  81. package/dist/tools/analyze.js.map +1 -1
  82. package/dist/tools/browser-click.d.ts +36 -0
  83. package/dist/tools/browser-click.d.ts.map +1 -0
  84. package/dist/tools/browser-click.js +117 -0
  85. package/dist/tools/browser-click.js.map +1 -0
  86. package/dist/tools/browser-close-session.d.ts +24 -0
  87. package/dist/tools/browser-close-session.d.ts.map +1 -0
  88. package/dist/tools/browser-close-session.js +63 -0
  89. package/dist/tools/browser-close-session.js.map +1 -0
  90. package/dist/tools/browser-fill.d.ts +49 -0
  91. package/dist/tools/browser-fill.d.ts.map +1 -0
  92. package/dist/tools/browser-fill.js +138 -0
  93. package/dist/tools/browser-fill.js.map +1 -0
  94. package/dist/tools/browser-navigate.d.ts +32 -0
  95. package/dist/tools/browser-navigate.d.ts.map +1 -0
  96. package/dist/tools/browser-navigate.js +103 -0
  97. package/dist/tools/browser-navigate.js.map +1 -0
  98. package/dist/tools/browser-run-scenario.d.ts +54 -0
  99. package/dist/tools/browser-run-scenario.d.ts.map +1 -0
  100. package/dist/tools/browser-run-scenario.js +228 -0
  101. package/dist/tools/browser-run-scenario.js.map +1 -0
  102. package/dist/tools/browser-save-scenario.d.ts +44 -0
  103. package/dist/tools/browser-save-scenario.d.ts.map +1 -0
  104. package/dist/tools/browser-save-scenario.js +298 -0
  105. package/dist/tools/browser-save-scenario.js.map +1 -0
  106. package/dist/tools/browser-snapshot.d.ts +60 -0
  107. package/dist/tools/browser-snapshot.d.ts.map +1 -0
  108. package/dist/tools/browser-snapshot.js +139 -0
  109. package/dist/tools/browser-snapshot.js.map +1 -0
  110. package/dist/tools/browser-start-session.d.ts +43 -0
  111. package/dist/tools/browser-start-session.d.ts.map +1 -0
  112. package/dist/tools/browser-start-session.js +272 -0
  113. package/dist/tools/browser-start-session.js.map +1 -0
  114. package/dist/tools/browser-type.d.ts +48 -0
  115. package/dist/tools/browser-type.d.ts.map +1 -0
  116. package/dist/tools/browser-type.js +136 -0
  117. package/dist/tools/browser-type.js.map +1 -0
  118. package/dist/tools/contracts.d.ts +15 -0
  119. package/dist/tools/contracts.d.ts.map +1 -1
  120. package/dist/tools/contracts.js +14 -3
  121. package/dist/tools/contracts.js.map +1 -1
  122. package/dist/tools/git-diff.d.ts.map +1 -1
  123. package/dist/tools/git-diff.js +85 -6
  124. package/dist/tools/git-diff.js.map +1 -1
  125. package/dist/tools/impact.d.ts +9 -0
  126. package/dist/tools/impact.d.ts.map +1 -1
  127. package/dist/tools/impact.js +3 -3
  128. package/dist/tools/impact.js.map +1 -1
  129. package/dist/tools/index.d.ts +9 -0
  130. package/dist/tools/index.d.ts.map +1 -1
  131. package/dist/tools/index.js +27 -0
  132. package/dist/tools/index.js.map +1 -1
  133. package/dist/tools/kit.d.ts +10 -1
  134. package/dist/tools/kit.d.ts.map +1 -1
  135. package/dist/tools/kit.js +2 -0
  136. package/dist/tools/kit.js.map +1 -1
  137. package/dist/tools/observe-runtime.d.ts +113 -19
  138. package/dist/tools/observe-runtime.d.ts.map +1 -1
  139. package/dist/tools/observe-runtime.js +324 -53
  140. package/dist/tools/observe-runtime.js.map +1 -1
  141. package/dist/tools/observe-tests.d.ts +125 -0
  142. package/dist/tools/observe-tests.d.ts.map +1 -0
  143. package/dist/tools/observe-tests.js +313 -0
  144. package/dist/tools/observe-tests.js.map +1 -0
  145. package/dist/tools/pr-analysis.d.ts +11 -0
  146. package/dist/tools/pr-analysis.d.ts.map +1 -1
  147. package/dist/tools/pr-analysis.js +3 -3
  148. package/dist/tools/pr-analysis.js.map +1 -1
  149. package/dist/tools/pre-push.d.ts +77 -0
  150. package/dist/tools/pre-push.d.ts.map +1 -0
  151. package/dist/tools/pre-push.js +250 -0
  152. package/dist/tools/pre-push.js.map +1 -0
  153. package/dist/tools/questions.d.ts.map +1 -1
  154. package/dist/tools/questions.js +41 -16
  155. package/dist/tools/questions.js.map +1 -1
  156. package/dist/tools/verification-status.d.ts +22 -0
  157. package/dist/tools/verification-status.d.ts.map +1 -1
  158. package/dist/tools/verification-status.js +51 -5
  159. package/dist/tools/verification-status.js.map +1 -1
  160. package/package.json +14 -13
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Live browser sessions belonging to one connection — `mcp-browser-tools.md`
3
+ * §4.
4
+ *
5
+ * ## Why this is the hard part
6
+ *
7
+ * Every other tool on this surface is one-shot. It opens what it needs,
8
+ * answers, and holds nothing; a crash in the middle costs the caller one
9
+ * reply. A browser process that outlives a call is a different shape
10
+ * entirely, and the failure it introduces is not "a call fails" but "a
11
+ * process is still running on the developer's machine an hour after the
12
+ * agent stopped caring about it".
13
+ *
14
+ * So the design rule here is that **there is no path on which a session stops
15
+ * being tracked without its browser being closed and its evidence being
16
+ * handed back**. Four paths reach that state — idle timeout, client
17
+ * disconnect, crash, explicit close — and they converge on one private
18
+ * function, `reap`, rather than each doing their own teardown. They were four
19
+ * separate implementations in the first draft and the evidence flush was
20
+ * correct on three of them.
21
+ *
22
+ * ## Evidence is held, not dropped, and not silently discarded either
23
+ *
24
+ * A timeout or a disconnect has no reply to attach evidence to. Discarding it
25
+ * would be the failure rule 7 names — silently doing less — so a reaped
26
+ * session leaves a `ReapedSession` record behind carrying the reason and the
27
+ * final flush, and the next caller who names that id receives it instead of a
28
+ * bare "unknown session". That record is what turns "your session vanished"
29
+ * into "your session was idle for five minutes; here is what it saw before it
30
+ * went".
31
+ *
32
+ * ## Per connection, never global
33
+ *
34
+ * A registry is created per `Session`, which is created per connection. A
35
+ * global one would let a second client `use` a first client's browser by
36
+ * guessing a uuid, and would make the disconnect reap ambiguous about whose
37
+ * sessions it was reaping.
38
+ */
39
+ import { EMPTY_OBSERVATION } from "./driver.js";
40
+ /**
41
+ * How long a session may go untouched before it is torn down.
42
+ *
43
+ * A starting value, not a measurement, in this repo's usual sense: no agent
44
+ * has yet been observed pausing mid-exploration, so there is nothing to fit
45
+ * it to. Five minutes is long enough to survive a model thinking, short
46
+ * enough that a forgotten browser is a five-minute leak rather than an
47
+ * overnight one. It is deliberately the same figure as the action handshake's
48
+ * token TTL — an agent that cannot decide within one is unlikely to be coming
49
+ * back for the other.
50
+ */
51
+ export const BROWSER_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
52
+ /**
53
+ * How many sessions one connection may hold open at once.
54
+ *
55
+ * Small on purpose. A calling agent driving more than a couple of browsers
56
+ * simultaneously is not doing something this surface was built for, and each
57
+ * one is a real Chromium. Three leaves room for the honest case — a
58
+ * comparison between two states, plus one being opened — without letting a
59
+ * loop with a missing `browser_close_session` fill the machine.
60
+ */
61
+ export const MAX_BROWSER_SESSIONS = 3;
62
+ const SWEEP_INTERVAL_MS = 30_000;
63
+ function originOf(url) {
64
+ try {
65
+ return new URL(url).origin;
66
+ }
67
+ catch {
68
+ // An unparseable target is the tool's problem, not the registry's — it
69
+ // refuses at the schema. Recording the raw string keeps the consent
70
+ // comparison total rather than throwing from a bookkeeping call.
71
+ return url;
72
+ }
73
+ }
74
+ export function createBrowserSessionRegistry(options = {}) {
75
+ const now = options.now ?? Date.now;
76
+ const maxSessions = options.maxSessions ?? MAX_BROWSER_SESSIONS;
77
+ const idleTimeoutMs = options.idleTimeoutMs ?? BROWSER_IDLE_TIMEOUT_MS;
78
+ const live = new Map();
79
+ const reapedSessions = new Map();
80
+ /**
81
+ * The one teardown. Every path goes through here so that "closed the
82
+ * browser" and "kept what it saw" cannot come apart.
83
+ *
84
+ * `close()` on the port returns the final flush and is idempotent, so a
85
+ * crashed session still yields whatever it had buffered — which is the case
86
+ * that matters most, the page's last words before it died.
87
+ */
88
+ async function reap(sessionId, session, reason) {
89
+ live.delete(sessionId);
90
+ let observation = EMPTY_OBSERVATION;
91
+ try {
92
+ observation = await session.page.close();
93
+ }
94
+ catch {
95
+ // A teardown that throws must not leave the session tracked, and must
96
+ // not lose the record either. The browser may well be gone already;
97
+ // what matters is that this id is now unusable and says why.
98
+ }
99
+ const record = { sessionId, reason, observation, at: now() };
100
+ reapedSessions.set(sessionId, record);
101
+ return record;
102
+ }
103
+ const sweeper = options.startSweeper === false ? null : setInterval(() => void sweepIdle(), SWEEP_INTERVAL_MS);
104
+ sweeper?.unref();
105
+ async function sweepIdle() {
106
+ const deadline = now() - idleTimeoutMs;
107
+ const expired = [...live.entries()].filter(([, session]) => session.lastUsedAt < deadline);
108
+ return Promise.all(expired.map(([sessionId, session]) => reap(sessionId, session, "idleTimeout")));
109
+ }
110
+ return {
111
+ idleTimeoutMs,
112
+ maxSessions,
113
+ async open(driver, launchOptions) {
114
+ if (live.size >= maxSessions) {
115
+ return {
116
+ opened: false,
117
+ kind: "atCapacity",
118
+ // Both numbers, because "too many browser sessions" is not
119
+ // something a caller can act on and "3 of 3" is — it tells them
120
+ // whether to close one or to stop opening them in a loop.
121
+ reason: `This connection already has ${live.size} of ${maxSessions} browser sessions open. Close one with ` +
122
+ "browser_close_session before starting another. The cap is per connection and exists because each " +
123
+ "session is a real browser process.",
124
+ };
125
+ }
126
+ const launch = await driver.launch(launchOptions);
127
+ if (!launch.launched) {
128
+ const { launched: _launched, ...refusal } = launch;
129
+ return { opened: false, kind: "chromiumNotInstalled", ...refusal };
130
+ }
131
+ const at = now();
132
+ const session = {
133
+ runId: `browser-${launch.session.sessionId}`,
134
+ page: launch.session,
135
+ profile: launchOptions.profile ?? null,
136
+ consentedOrigin: originOf(launchOptions.targetUrl),
137
+ openedAt: at,
138
+ lastUsedAt: at,
139
+ };
140
+ live.set(launch.session.sessionId, session);
141
+ return { opened: true, entry: entryOf(launch.session.sessionId, session) };
142
+ },
143
+ async use(sessionId) {
144
+ const session = live.get(sessionId);
145
+ if (session === undefined) {
146
+ const record = reapedSessions.get(sessionId);
147
+ if (record !== undefined) {
148
+ return { ok: false, kind: "reaped", reason: reasonFor(record), reaped: record };
149
+ }
150
+ return {
151
+ ok: false,
152
+ kind: "unknown",
153
+ reason: `No browser session "${sessionId}" on this connection. Sessions are per connection and do not ` +
154
+ "survive a reconnect; start one with browser_start_session.",
155
+ };
156
+ }
157
+ if (!session.page.isUsable()) {
158
+ // Down the same teardown every other path uses, so the crash case
159
+ // cannot be the one where the flush is skipped.
160
+ const record = await reap(sessionId, session, "crashed");
161
+ return { ok: false, kind: "crashed", reason: crashReason(sessionId), reaped: record };
162
+ }
163
+ session.lastUsedAt = now();
164
+ return { ok: true, entry: entryOf(sessionId, session) };
165
+ },
166
+ async close(sessionId) {
167
+ const session = live.get(sessionId);
168
+ if (session === undefined)
169
+ return reapedSessions.get(sessionId)?.observation ?? EMPTY_OBSERVATION;
170
+ const record = await reap(sessionId, session, "closed");
171
+ return record.observation;
172
+ },
173
+ sweepIdle,
174
+ async reapAll(reason) {
175
+ const all = [...live.entries()];
176
+ return Promise.all(all.map(([sessionId, session]) => reap(sessionId, session, reason)));
177
+ },
178
+ reaped: (sessionId) => reapedSessions.get(sessionId),
179
+ openCount: () => live.size,
180
+ stop() {
181
+ if (sweeper !== null)
182
+ clearInterval(sweeper);
183
+ },
184
+ };
185
+ }
186
+ function entryOf(sessionId, session) {
187
+ return {
188
+ sessionId,
189
+ runId: session.runId,
190
+ page: session.page,
191
+ profile: session.profile,
192
+ consentedOrigin: session.consentedOrigin,
193
+ openedAt: session.openedAt,
194
+ lastUsedAt: session.lastUsedAt,
195
+ };
196
+ }
197
+ function crashReason(sessionId) {
198
+ return (`Browser session ${sessionId} crashed or its page closed. The session id is invalidated rather than ` +
199
+ "reopened against a different browser, which would silently change what later evidence refers to. " +
200
+ "Anything the page reported before it died is included here.");
201
+ }
202
+ function reasonFor(record) {
203
+ switch (record.reason) {
204
+ case "idleTimeout":
205
+ return (`Browser session ${record.sessionId} was torn down after being idle. Its evidence was kept and is ` +
206
+ "included here. Start a new session to continue.");
207
+ case "clientDisconnect":
208
+ return `Browser session ${record.sessionId} was reaped when the client disconnected.`;
209
+ case "crashed":
210
+ return crashReason(record.sessionId);
211
+ case "closed":
212
+ return `Browser session ${record.sessionId} was closed by browser_close_session.`;
213
+ }
214
+ }
215
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/browser/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AASH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAErD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAqHjC,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,oEAAoE;QACpE,iEAAiE;QACjE,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,UAAyC,EAAE;IACtF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAChE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,uBAAuB,CAAC;IAEvE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC5C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC;IAExD;;;;;;;OAOG;IACH,KAAK,UAAU,IAAI,CAAC,SAAiB,EAAE,OAAoB,EAAE,MAAkB;QAC7E,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvB,IAAI,WAAW,GAAuB,iBAAiB,CAAC;QACxD,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;YACtE,oEAAoE;YACpE,6DAA6D;QAC/D,CAAC;QACD,MAAM,MAAM,GAAkB,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;QAC5E,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,EAAE,EAAE,iBAAiB,CAAC,CAAC;IAC/G,OAAO,EAAE,KAAK,EAAE,CAAC;IAEjB,KAAK,UAAU,SAAS;QACtB,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,aAAa,CAAC;QACvC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC;QAC3F,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IACrG,CAAC;IAED,OAAO;QACL,aAAa;QACb,WAAW;QAEX,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa;YAC9B,IAAI,IAAI,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC;gBAC7B,OAAO;oBACL,MAAM,EAAE,KAAK;oBACb,IAAI,EAAE,YAAY;oBAClB,2DAA2D;oBAC3D,gEAAgE;oBAChE,0DAA0D;oBAC1D,MAAM,EACJ,+BAA+B,IAAI,CAAC,IAAI,OAAO,WAAW,yCAAyC;wBACnG,mGAAmG;wBACnG,oCAAoC;iBACvC,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACrB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,GAAG,MAAM,CAAC;gBACnD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,sBAAsB,EAAE,GAAG,OAAO,EAAE,CAAC;YACrE,CAAC;YAED,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC;YACjB,MAAM,OAAO,GAAgB;gBAC3B,KAAK,EAAE,WAAW,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE;gBAC5C,IAAI,EAAE,MAAM,CAAC,OAAO;gBACpB,OAAO,EAAE,aAAa,CAAC,OAAO,IAAI,IAAI;gBACtC,eAAe,EAAE,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC;gBAClD,QAAQ,EAAE,EAAE;gBACZ,UAAU,EAAE,EAAE;aACf,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAC5C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC;QAC7E,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,SAAS;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;gBAClF,CAAC;gBACD,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,IAAI,EAAE,SAAS;oBACf,MAAM,EACJ,uBAAuB,SAAS,+DAA+D;wBAC/F,4DAA4D;iBAC/D,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;gBAC7B,kEAAkE;gBAClE,gDAAgD;gBAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;gBACzD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YACxF,CAAC;YAED,OAAO,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC;YAC3B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC;QAC1D,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,SAAS;YACnB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACpC,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,IAAI,iBAAiB,CAAC;YAClG,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;YACxD,OAAO,MAAM,CAAC,WAAW,CAAC;QAC5B,CAAC;QAED,SAAS;QAET,KAAK,CAAC,OAAO,CAAC,MAAM;YAClB,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAChC,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1F,CAAC;QAED,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACpD,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI;QAC1B,IAAI;YACF,IAAI,OAAO,KAAK,IAAI;gBAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,SAAiB,EAAE,OAAoB;IACtD,OAAO;QACL,SAAS;QACT,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,SAAiB;IACpC,OAAO,CACL,mBAAmB,SAAS,yEAAyE;QACrG,mGAAmG;QACnG,6DAA6D,CAC9D,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,MAAqB;IACtC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,aAAa;YAChB,OAAO,CACL,mBAAmB,MAAM,CAAC,SAAS,gEAAgE;gBACnG,iDAAiD,CAClD,CAAC;QACJ,KAAK,kBAAkB;YACrB,OAAO,mBAAmB,MAAM,CAAC,SAAS,2CAA2C,CAAC;QACxF,KAAK,SAAS;YACZ,OAAO,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvC,KAAK,QAAQ;YACX,OAAO,mBAAmB,MAAM,CAAC,SAAS,uCAAuC,CAAC;IACtF,CAAC;AACH,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Turning a stored target descriptor back into something clickable —
3
+ * `DEC-383` §2 and §3.
4
+ *
5
+ * This is where the anti-rot mechanism actually lives. A stored step carries
6
+ * `role` + `name` + `selector` and no `ref`, because a ref is scoped to the
7
+ * snapshot that issued it and a replay is always a later session. So every
8
+ * step re-resolves against a fresh snapshot, and the resolution is
9
+ * **exact-or-refuse**: exactly one element matching all three fields, or stop.
10
+ *
11
+ * **There is deliberately no fallback chain.** "Try the selector, else
12
+ * role+name" is the dangerous option and it is dangerous in a specific way: it
13
+ * turns a real disagreement into a quiet substitution, which is how a scenario
14
+ * keeps reporting green against a page that changed underneath it. A
15
+ * disagreement between the three fields is the highest-value signal this
16
+ * format can produce, and a fallback is exactly the code that throws it away.
17
+ *
18
+ * The refusal messages are load-bearing rather than decorative, per
19
+ * `DEC-384`: a stopped push that says "step 3
20
+ * failed to resolve" is what developers route around, and one that says "the
21
+ * button is still there; its accessible name changed from Delete to Remove" is
22
+ * a one-line fix. So every refusal names which step, which descriptor, which
23
+ * of the three fields disagreed, and what the page holds now.
24
+ *
25
+ * Lives beside the port rather than in `scenarios/`, on purpose: the format
26
+ * module imports nothing from the driver and is tested without a browser in
27
+ * the process, and this function needs a `PageSnapshot`.
28
+ */
29
+ import type { TargetDescriptor } from "../scenarios/index.ts";
30
+ import type { PageSnapshot, SnapshotElement } from "./driver.ts";
31
+ export type DescriptorField = "role" | "name" | "selector";
32
+ /**
33
+ * Why a descriptor did not resolve.
34
+ *
35
+ * Three kinds, kept apart because they send a developer to three different
36
+ * places. `zero` — nothing on the page answers to this at all. `ambiguous` —
37
+ * several do, and picking one would be reporting on a run nobody chose.
38
+ * `partial` — something is still there and one part of the descriptor has
39
+ * moved, which is the case worth the most and the reason `disagreed` exists.
40
+ */
41
+ export interface ResolutionFailure {
42
+ readonly kind: "zero" | "ambiguous" | "partial";
43
+ /** Which of role/name/selector no longer agree. Empty for `zero` and `ambiguous`, where nothing partial matched to disagree with. */
44
+ readonly disagreed: readonly DescriptorField[];
45
+ readonly message: string;
46
+ }
47
+ export type TargetResolution = {
48
+ readonly resolved: true;
49
+ readonly element: SnapshotElement;
50
+ } | {
51
+ readonly resolved: false;
52
+ readonly failure: ResolutionFailure;
53
+ };
54
+ /**
55
+ * Resolve one stored descriptor against a fresh snapshot.
56
+ *
57
+ * `where` is the caller's label for the thing being resolved — `"step 0"`,
58
+ * `"assertion 2"` — and it is required rather than optional because a refusal
59
+ * that does not say *which* step is the version this ruling exists to avoid.
60
+ */
61
+ export declare function resolveTarget(where: string, target: TargetDescriptor, snapshot: PageSnapshot): TargetResolution;
62
+ //# sourceMappingURL=scenario-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenario-resolve.d.ts","sourceRoot":"","sources":["../../src/browser/scenario-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE3D;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IAChD,qIAAqI;IACrI,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC9D;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAuBtE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,YAAY,GAAG,gBAAgB,CAgF/G"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Turning a stored target descriptor back into something clickable —
3
+ * `DEC-383` §2 and §3.
4
+ *
5
+ * This is where the anti-rot mechanism actually lives. A stored step carries
6
+ * `role` + `name` + `selector` and no `ref`, because a ref is scoped to the
7
+ * snapshot that issued it and a replay is always a later session. So every
8
+ * step re-resolves against a fresh snapshot, and the resolution is
9
+ * **exact-or-refuse**: exactly one element matching all three fields, or stop.
10
+ *
11
+ * **There is deliberately no fallback chain.** "Try the selector, else
12
+ * role+name" is the dangerous option and it is dangerous in a specific way: it
13
+ * turns a real disagreement into a quiet substitution, which is how a scenario
14
+ * keeps reporting green against a page that changed underneath it. A
15
+ * disagreement between the three fields is the highest-value signal this
16
+ * format can produce, and a fallback is exactly the code that throws it away.
17
+ *
18
+ * The refusal messages are load-bearing rather than decorative, per
19
+ * `DEC-384`: a stopped push that says "step 3
20
+ * failed to resolve" is what developers route around, and one that says "the
21
+ * button is still there; its accessible name changed from Delete to Remove" is
22
+ * a one-line fix. So every refusal names which step, which descriptor, which
23
+ * of the three fields disagreed, and what the page holds now.
24
+ *
25
+ * Lives beside the port rather than in `scenarios/`, on purpose: the format
26
+ * module imports nothing from the driver and is tested without a browser in
27
+ * the process, and this function needs a `PageSnapshot`.
28
+ */
29
+ const FIELDS = ["role", "name", "selector"];
30
+ function describe(target) {
31
+ return `role "${target.role}", name "${target.name}", selector "${target.selector}"`;
32
+ }
33
+ /** What the page holds, capped — a refusal a person has to scroll is one they stop reading. */
34
+ function pageContents(snapshot, limit = 8) {
35
+ if (snapshot.elements.length === 0)
36
+ return "The page presents no elements at all.";
37
+ const shown = snapshot.elements
38
+ .slice(0, limit)
39
+ .map((e) => `role "${e.role}", name "${e.name}", selector "${e.selector}"`)
40
+ .join("; ");
41
+ const rest = snapshot.totalElements - Math.min(limit, snapshot.elements.length);
42
+ return `The page holds ${snapshot.totalElements} element(s): ${shown}${rest > 0 ? `; and ${rest} more` : ""}.`;
43
+ }
44
+ function disagreementsWith(target, element) {
45
+ return FIELDS.filter((field) => element[field] !== target[field]);
46
+ }
47
+ /**
48
+ * Resolve one stored descriptor against a fresh snapshot.
49
+ *
50
+ * `where` is the caller's label for the thing being resolved — `"step 0"`,
51
+ * `"assertion 2"` — and it is required rather than optional because a refusal
52
+ * that does not say *which* step is the version this ruling exists to avoid.
53
+ */
54
+ export function resolveTarget(where, target, snapshot) {
55
+ const exact = snapshot.elements.filter((e) => e.role === target.role && e.name === target.name && e.selector === target.selector);
56
+ if (exact.length === 1)
57
+ return { resolved: true, element: exact[0] };
58
+ if (exact.length > 1) {
59
+ return {
60
+ resolved: false,
61
+ failure: {
62
+ kind: "ambiguous",
63
+ disagreed: [],
64
+ message: `${where}: ${exact.length} elements on ${snapshot.url} match ${describe(target)}. Resolution is ` +
65
+ "exactly one match or none — the first is not picked, because a replay that guessed which element " +
66
+ "it meant is reporting on a run nobody chose. Narrow the selector and re-save the step.",
67
+ },
68
+ };
69
+ }
70
+ const bySelector = snapshot.elements.filter((e) => e.selector === target.selector);
71
+ const byRoleName = snapshot.elements.filter((e) => e.role === target.role && e.name === target.name);
72
+ if (bySelector.length === 0 && byRoleName.length === 0) {
73
+ return {
74
+ resolved: false,
75
+ failure: {
76
+ kind: "zero",
77
+ disagreed: [],
78
+ message: `${where}: nothing on ${snapshot.url} matches ${describe(target)}, and nothing matches any part of ` +
79
+ `it either. ${pageContents(snapshot)} Either this element was removed, or the scenario was saved ` +
80
+ "against a different page than the one the replay reached.",
81
+ },
82
+ };
83
+ }
84
+ // The partial case, and the reason the format stores all three fields. Both
85
+ // halves are reported when both matched, because "the selector still points
86
+ // at something AND an element with this role and name still exists" is two
87
+ // candidates and a person is the one who knows which is theirs.
88
+ const parts = [];
89
+ const disagreed = new Set();
90
+ if (bySelector.length === 1) {
91
+ const found = bySelector[0];
92
+ for (const field of disagreementsWith(target, found))
93
+ disagreed.add(field);
94
+ const moved = disagreementsWith(target, found)
95
+ .map((field) => `${field} changed from "${target[field]}" to "${found[field]}"`)
96
+ .join(" and ");
97
+ parts.push(`selector "${target.selector}" still matches one element, but its ${moved}`);
98
+ }
99
+ else if (bySelector.length > 1) {
100
+ disagreed.add("selector");
101
+ parts.push(`selector "${target.selector}" now matches ${bySelector.length} elements, none of them with this role and name`);
102
+ }
103
+ if (byRoleName.length === 1) {
104
+ const found = byRoleName[0];
105
+ disagreed.add("selector");
106
+ parts.push(`an element with role "${target.role}" and name "${target.name}" is still on the page, but its ` +
107
+ `selector changed from "${target.selector}" to "${found.selector}"`);
108
+ }
109
+ else if (byRoleName.length > 1) {
110
+ disagreed.add("selector");
111
+ parts.push(`${byRoleName.length} elements still have role "${target.role}" and name "${target.name}", none of them at this selector`);
112
+ }
113
+ return {
114
+ resolved: false,
115
+ failure: {
116
+ kind: "partial",
117
+ // Ordered the way the descriptor is, so a caller comparing two failures
118
+ // is comparing sets rather than orderings.
119
+ disagreed: FIELDS.filter((field) => disagreed.has(field)),
120
+ message: `${where}: ${describe(target)} does not match exactly one element on ${snapshot.url}, but part of it ` +
121
+ `still does — ${parts.join("; ")}. The step is not run: substituting the half that still matches would ` +
122
+ "turn a real disagreement into a quiet one, and a scenario that does that keeps reporting green " +
123
+ "against a page that changed underneath it. Re-save the step if the change was intended.",
124
+ },
125
+ };
126
+ }
127
+ //# sourceMappingURL=scenario-resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenario-resolve.js","sourceRoot":"","sources":["../../src/browser/scenario-resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Replaying a stored scenario — §7's runner half.
3
+ *
4
+ * ## What this composes, and the four things it must not get wrong
5
+ *
6
+ * Everything below is composition: the port drives, `scenarios/` is the
7
+ * artifact, `scenario-resolve.ts` re-resolves a descriptor, `tool-support.ts`
8
+ * drains and correlates. What the runner adds is *order*, and four ordering
9
+ * mistakes each fail as flakiness or as a false pass rather than as an error,
10
+ * which is why each is named here rather than left to be rediscovered.
11
+ *
12
+ * 1. **`registry.use()` is called once per step.** It is not a lookup: it is
13
+ * asynchronous, it is the crash detector, and it touches the idle clock. A
14
+ * runner that resolved the session once and held the entry across N steps
15
+ * would be reaped mid-scenario at the five-minute idle timeout, and would
16
+ * read as a flaky test rather than as a design error.
17
+ * 2. **`drainEvidence()` drains, it does not peek.** Once per step, aggregated
18
+ * into the summary. A single read at the end loses every earlier step's
19
+ * observations — and would silently pass an assertion anchored at step 0,
20
+ * which is the false green this whole feature exists to prevent.
21
+ * 3. **A stored step carries no `ref`.** Every step re-resolves against a
22
+ * fresh snapshot, exact-or-refuse, with no fallback chain. See
23
+ * `scenario-resolve.ts`.
24
+ * 4. **Assertions evaluate here, at replay.** An empty assertion list replays
25
+ * as `unverified`, never `passed`.
26
+ *
27
+ * ## The outcome rule
28
+ *
29
+ * `DEC-384`, implemented rather than restated. A
30
+ * failed assertion is the scenario's own claim about the application, written
31
+ * down by a person, and it blocks. A console error or a failed request is
32
+ * reported and does not block — frequently true of a working page, and a check
33
+ * people disable protects nothing. No assertions is `unverified`. An
34
+ * unresolvable step is **neither pass nor fail**: Descry cannot tell a
35
+ * deliberate rename from a vanished button, and both produce a byte-identical
36
+ * unresolvable step, so it claims neither while still stopping the run.
37
+ *
38
+ * Three counts, **never summed** — `failed`, `unresolved`, `unverified` — and
39
+ * only failed assertions join `blockingTotal`, because folding `unresolved`
40
+ * into it would re-assert through arithmetic the defect claim the ruling
41
+ * refused to make in prose.
42
+ */
43
+ import type { Scenario, ScenarioActionKind, ScenarioAssertionKind } from "../scenarios/index.ts";
44
+ import type { ToolContext } from "../tools/kit.ts";
45
+ import type { BrowserEvidenceReport } from "./evidence.ts";
46
+ import { type BrowserGraphWrite } from "./graph-write.ts";
47
+ import type { BrowserSessionRegistry } from "./registry.ts";
48
+ import { type ResolutionFailure } from "./scenario-resolve.ts";
49
+ import { type DeadSession } from "./tool-support.ts";
50
+ export interface ScenarioStepResult {
51
+ readonly index: number;
52
+ readonly action: ScenarioActionKind;
53
+ /** `ran` — performed. `unresolved` — the descriptor did not resolve, and the replay stopped here. `skipped` — never attempted, because an earlier step stopped the replay. */
54
+ readonly status: "ran" | "unresolved" | "skipped";
55
+ readonly url: string;
56
+ readonly resolution: ResolutionFailure | null;
57
+ }
58
+ export interface ScenarioAssertionResult {
59
+ readonly index: number;
60
+ readonly kind: ScenarioAssertionKind;
61
+ readonly afterStep: number | null;
62
+ /**
63
+ * `unevaluable` and `notReached` are deliberately distinct from each other
64
+ * and from `failed`.
65
+ *
66
+ * `unevaluable` is a hole in the stored artifact — a `textEquals` with
67
+ * nothing to compare against. `notReached` is an assertion anchored at a
68
+ * step the replay never got to. Neither is a defect claim about the
69
+ * application, so neither may be reported as `failed`; only `unevaluable`
70
+ * joins `unresolved`, because `notReached` is already accounted for by the
71
+ * unresolved step that stopped the run and counting it twice would inflate
72
+ * a count the ruling requires to stay separate.
73
+ */
74
+ readonly status: "passed" | "failed" | "unevaluable" | "notReached";
75
+ readonly detail: string;
76
+ }
77
+ export interface ScenarioCounts {
78
+ readonly passed: number;
79
+ readonly failed: number;
80
+ readonly unresolved: number;
81
+ readonly unverified: number;
82
+ }
83
+ export interface ScenarioReplay {
84
+ readonly scenario: string;
85
+ readonly sessionId: string;
86
+ readonly outcome: "passed" | "failed" | "unresolved" | "unverified";
87
+ readonly counts: ScenarioCounts;
88
+ /** Only failed assertions. See this file's header for why `unresolved` is not folded in. */
89
+ readonly blockingTotal: number;
90
+ /**
91
+ * 0 clean, 1 blocking findings, 2 the check could not run — `descry check`'s
92
+ * existing vocabulary, computed here rather than in the CLI.
93
+ *
94
+ * The exit ruling governs the agentless CLI and explicitly does not govern
95
+ * MCP replies, so this is data on the reply rather than a verdict in it. It
96
+ * is computed in the one place that knows the outcome so the CLI box does
97
+ * not become a second implementation of the same rule, drifting from this
98
+ * one the first time either changes.
99
+ */
100
+ readonly exitCode: 0 | 1 | 2;
101
+ readonly steps: readonly ScenarioStepResult[];
102
+ readonly assertions: readonly ScenarioAssertionResult[];
103
+ /** One summary for the whole batch, aggregated from the per-step drains. */
104
+ readonly evidence: BrowserEvidenceReport;
105
+ readonly graph: BrowserGraphWrite;
106
+ /** Set when the session went away mid-replay — a crash, a reap, a disconnect. Null on a replay that ran to its own end. */
107
+ readonly sessionEnded: DeadSession | null;
108
+ readonly notes: readonly string[];
109
+ }
110
+ /** True when replaying this scenario would change the running application. A navigate-only scenario would not. */
111
+ export declare function mutates(scenario: Scenario): boolean;
112
+ export declare function replayScenario(ctx: ToolContext, registry: BrowserSessionRegistry, sessionId: string, scenario: Scenario): Promise<ScenarioReplay>;
113
+ //# sourceMappingURL=scenario-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenario-runner.d.ts","sourceRoot":"","sources":["../../src/browser/scenario-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAqB,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACpH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,KAAK,EAAuB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAkD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,8KAA8K;IAC9K,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,YAAY,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,YAAY,CAAC;IACpE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,4FAA4F;IAC5F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACxD,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,2HAA2H;IAC3H,QAAQ,CAAC,YAAY,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAID,kHAAkH;AAClH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEnD;AAqBD,wBAAsB,cAAc,CAClC,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,sBAAsB,EAChC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,cAAc,CAAC,CAmMzB"}