@descryy/mcp 0.1.2 → 0.3.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.
- package/dist/bin/descry-mcp.js +30 -2
- package/dist/bin/descry-mcp.js.map +1 -1
- package/dist/browser/driver.d.ts +236 -0
- package/dist/browser/driver.d.ts.map +1 -0
- package/dist/browser/driver.js +70 -0
- package/dist/browser/driver.js.map +1 -0
- package/dist/browser/evidence.d.ts +47 -0
- package/dist/browser/evidence.d.ts.map +1 -0
- package/dist/browser/evidence.js +55 -0
- package/dist/browser/evidence.js.map +1 -0
- package/dist/browser/fake-driver.d.ts +58 -0
- package/dist/browser/fake-driver.d.ts.map +1 -0
- package/dist/browser/fake-driver.js +161 -0
- package/dist/browser/fake-driver.js.map +1 -0
- package/dist/browser/graph-write.d.ts +86 -0
- package/dist/browser/graph-write.d.ts.map +1 -0
- package/dist/browser/graph-write.js +135 -0
- package/dist/browser/graph-write.js.map +1 -0
- package/dist/browser/playwright-driver.d.ts +65 -0
- package/dist/browser/playwright-driver.d.ts.map +1 -0
- package/dist/browser/playwright-driver.js +359 -0
- package/dist/browser/playwright-driver.js.map +1 -0
- package/dist/browser/provider.d.ts +49 -0
- package/dist/browser/provider.d.ts.map +1 -0
- package/dist/browser/provider.js +28 -0
- package/dist/browser/provider.js.map +1 -0
- package/dist/browser/registry.d.ts +182 -0
- package/dist/browser/registry.d.ts.map +1 -0
- package/dist/browser/registry.js +215 -0
- package/dist/browser/registry.js.map +1 -0
- package/dist/browser/scenario-resolve.d.ts +62 -0
- package/dist/browser/scenario-resolve.d.ts.map +1 -0
- package/dist/browser/scenario-resolve.js +127 -0
- package/dist/browser/scenario-resolve.js.map +1 -0
- package/dist/browser/scenario-runner.d.ts +113 -0
- package/dist/browser/scenario-runner.d.ts.map +1 -0
- package/dist/browser/scenario-runner.js +315 -0
- package/dist/browser/scenario-runner.js.map +1 -0
- package/dist/browser/stack-parser.d.ts +43 -0
- package/dist/browser/stack-parser.d.ts.map +1 -0
- package/dist/browser/stack-parser.js +112 -0
- package/dist/browser/stack-parser.js.map +1 -0
- package/dist/browser/tool-support.d.ts +113 -0
- package/dist/browser/tool-support.d.ts.map +1 -0
- package/dist/browser/tool-support.js +181 -0
- package/dist/browser/tool-support.js.map +1 -0
- package/dist/disclosure-ledger.d.ts +38 -0
- package/dist/disclosure-ledger.d.ts.map +1 -0
- package/dist/disclosure-ledger.js +40 -0
- package/dist/disclosure-ledger.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +12 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +12 -3
- package/dist/protocol.js.map +1 -1
- package/dist/render.d.ts +121 -10
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +96 -15
- package/dist/render.js.map +1 -1
- package/dist/runtime-registry.d.ts +63 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +131 -0
- package/dist/runtime-registry.js.map +1 -0
- package/dist/scenarios/index.d.ts +4 -0
- package/dist/scenarios/index.d.ts.map +1 -0
- package/dist/scenarios/index.js +4 -0
- package/dist/scenarios/index.js.map +1 -0
- package/dist/scenarios/parse.d.ts +32 -0
- package/dist/scenarios/parse.d.ts.map +1 -0
- package/dist/scenarios/parse.js +199 -0
- package/dist/scenarios/parse.js.map +1 -0
- package/dist/scenarios/scenario.d.ts +115 -0
- package/dist/scenarios/scenario.d.ts.map +1 -0
- package/dist/scenarios/scenario.js +41 -0
- package/dist/scenarios/scenario.js.map +1 -0
- package/dist/scenarios/storage.d.ts +51 -0
- package/dist/scenarios/storage.d.ts.map +1 -0
- package/dist/scenarios/storage.js +93 -0
- package/dist/scenarios/storage.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -3
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +142 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +266 -4
- package/dist/session.js.map +1 -1
- package/dist/tools/analyze.d.ts +41 -1
- package/dist/tools/analyze.d.ts.map +1 -1
- package/dist/tools/analyze.js +111 -9
- package/dist/tools/analyze.js.map +1 -1
- package/dist/tools/browser-click.d.ts +36 -0
- package/dist/tools/browser-click.d.ts.map +1 -0
- package/dist/tools/browser-click.js +117 -0
- package/dist/tools/browser-click.js.map +1 -0
- package/dist/tools/browser-close-session.d.ts +24 -0
- package/dist/tools/browser-close-session.d.ts.map +1 -0
- package/dist/tools/browser-close-session.js +63 -0
- package/dist/tools/browser-close-session.js.map +1 -0
- package/dist/tools/browser-fill.d.ts +49 -0
- package/dist/tools/browser-fill.d.ts.map +1 -0
- package/dist/tools/browser-fill.js +138 -0
- package/dist/tools/browser-fill.js.map +1 -0
- package/dist/tools/browser-navigate.d.ts +32 -0
- package/dist/tools/browser-navigate.d.ts.map +1 -0
- package/dist/tools/browser-navigate.js +103 -0
- package/dist/tools/browser-navigate.js.map +1 -0
- package/dist/tools/browser-run-scenario.d.ts +54 -0
- package/dist/tools/browser-run-scenario.d.ts.map +1 -0
- package/dist/tools/browser-run-scenario.js +228 -0
- package/dist/tools/browser-run-scenario.js.map +1 -0
- package/dist/tools/browser-save-scenario.d.ts +44 -0
- package/dist/tools/browser-save-scenario.d.ts.map +1 -0
- package/dist/tools/browser-save-scenario.js +298 -0
- package/dist/tools/browser-save-scenario.js.map +1 -0
- package/dist/tools/browser-snapshot.d.ts +60 -0
- package/dist/tools/browser-snapshot.d.ts.map +1 -0
- package/dist/tools/browser-snapshot.js +139 -0
- package/dist/tools/browser-snapshot.js.map +1 -0
- package/dist/tools/browser-start-session.d.ts +43 -0
- package/dist/tools/browser-start-session.d.ts.map +1 -0
- package/dist/tools/browser-start-session.js +272 -0
- package/dist/tools/browser-start-session.js.map +1 -0
- package/dist/tools/browser-type.d.ts +48 -0
- package/dist/tools/browser-type.d.ts.map +1 -0
- package/dist/tools/browser-type.js +136 -0
- package/dist/tools/browser-type.js.map +1 -0
- package/dist/tools/contracts.d.ts +15 -0
- package/dist/tools/contracts.d.ts.map +1 -1
- package/dist/tools/contracts.js +14 -3
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/cross-pr.d.ts.map +1 -1
- package/dist/tools/cross-pr.js +1 -0
- package/dist/tools/cross-pr.js.map +1 -1
- package/dist/tools/git-diff.d.ts.map +1 -1
- package/dist/tools/git-diff.js +104 -4
- package/dist/tools/git-diff.js.map +1 -1
- package/dist/tools/git-history.d.ts.map +1 -1
- package/dist/tools/git-history.js +1 -0
- package/dist/tools/git-history.js.map +1 -1
- package/dist/tools/history.js +1 -1
- package/dist/tools/history.js.map +1 -1
- package/dist/tools/impact.d.ts +9 -0
- package/dist/tools/impact.d.ts.map +1 -1
- package/dist/tools/impact.js +4 -4
- package/dist/tools/impact.js.map +1 -1
- package/dist/tools/index.d.ts +12 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/kit.d.ts +11 -1
- package/dist/tools/kit.d.ts.map +1 -1
- package/dist/tools/kit.js +3 -0
- package/dist/tools/kit.js.map +1 -1
- package/dist/tools/link-workspace.d.ts.map +1 -1
- package/dist/tools/link-workspace.js +13 -1
- package/dist/tools/link-workspace.js.map +1 -1
- package/dist/tools/mark-incident.d.ts +69 -0
- package/dist/tools/mark-incident.d.ts.map +1 -0
- package/dist/tools/mark-incident.js +212 -0
- package/dist/tools/mark-incident.js.map +1 -0
- package/dist/tools/observe-runtime.d.ts +292 -0
- package/dist/tools/observe-runtime.d.ts.map +1 -0
- package/dist/tools/observe-runtime.js +1192 -0
- package/dist/tools/observe-runtime.js.map +1 -0
- package/dist/tools/observe-tests.d.ts +125 -0
- package/dist/tools/observe-tests.d.ts.map +1 -0
- package/dist/tools/observe-tests.js +313 -0
- package/dist/tools/observe-tests.js.map +1 -0
- package/dist/tools/pr-analysis.d.ts +71 -13
- package/dist/tools/pr-analysis.d.ts.map +1 -1
- package/dist/tools/pr-analysis.js +55 -12
- package/dist/tools/pr-analysis.js.map +1 -1
- package/dist/tools/pre-push.d.ts +77 -0
- package/dist/tools/pre-push.d.ts.map +1 -0
- package/dist/tools/pre-push.js +250 -0
- package/dist/tools/pre-push.js.map +1 -0
- package/dist/tools/propagation.d.ts.map +1 -1
- package/dist/tools/propagation.js +1 -0
- package/dist/tools/propagation.js.map +1 -1
- package/dist/tools/questions.d.ts.map +1 -1
- package/dist/tools/questions.js +101 -14
- package/dist/tools/questions.js.map +1 -1
- package/dist/tools/refusal-fetch.d.ts.map +1 -1
- package/dist/tools/refusal-fetch.js +1 -0
- package/dist/tools/refusal-fetch.js.map +1 -1
- package/dist/tools/runtime-incident.d.ts +92 -0
- package/dist/tools/runtime-incident.d.ts.map +1 -0
- package/dist/tools/runtime-incident.js +144 -0
- package/dist/tools/runtime-incident.js.map +1 -0
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +1 -0
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/similar-incidents.d.ts +13 -0
- package/dist/tools/similar-incidents.d.ts.map +1 -1
- package/dist/tools/similar-incidents.js +22 -8
- package/dist/tools/similar-incidents.js.map +1 -1
- package/dist/tools/verification-status.d.ts +22 -0
- package/dist/tools/verification-status.d.ts.map +1 -1
- package/dist/tools/verification-status.js +52 -5
- package/dist/tools/verification-status.js.map +1 -1
- package/dist/tools/verify-claim.d.ts.map +1 -1
- package/dist/tools/verify-claim.js +5 -0
- package/dist/tools/verify-claim.js.map +1 -1
- package/package.json +16 -4
|
@@ -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":"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"}
|
|
@@ -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"}
|