@syrin/iris 0.5.0 → 0.6.10
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/cli.js +2587 -1027
- package/dist/index.js +414 -114
- package/dist/next.js +5 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.js +1903 -1005
- package/dist/test.js +1918 -1074
- package/dist/vite.d.ts +43 -0
- package/dist/vite.js +144 -0
- package/package.json +15 -10
package/dist/server.js
CHANGED
|
@@ -5,6 +5,30 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
// ../protocol/dist/constants.js
|
|
6
6
|
var IRIS_DEFAULT_PORT = 4400;
|
|
7
7
|
var IRIS_WS_PATH = "/iris";
|
|
8
|
+
var IRIS_PROTOCOL_VERSION = 1;
|
|
9
|
+
var TRANSPORT_LIMITS = {
|
|
10
|
+
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
11
|
+
MAX_MESSAGES_PER_SECOND: 1e3,
|
|
12
|
+
MAX_SESSIONS: 32,
|
|
13
|
+
MAX_PENDING_CONNECTIONS: 16,
|
|
14
|
+
HELLO_TIMEOUT_MS: 5e3,
|
|
15
|
+
MAX_BUFFER_BYTES: 8 * 1024 * 1024,
|
|
16
|
+
MAX_SESSION_ID_LENGTH: 128,
|
|
17
|
+
MAX_URL_LENGTH: 4096,
|
|
18
|
+
MAX_TITLE_LENGTH: 512,
|
|
19
|
+
MAX_ADAPTERS: 32,
|
|
20
|
+
MAX_ADAPTER_NAME_LENGTH: 128,
|
|
21
|
+
MAX_TOKEN_LENGTH: 512,
|
|
22
|
+
MAX_COMMAND_ID_LENGTH: 128,
|
|
23
|
+
MAX_COMMAND_NAME_LENGTH: 128,
|
|
24
|
+
MAX_REF_LENGTH: 128,
|
|
25
|
+
MAX_ERROR_LENGTH: 4096,
|
|
26
|
+
MAX_SERIALIZE_DEPTH: 8,
|
|
27
|
+
MAX_COLLECTION_ITEMS: 200,
|
|
28
|
+
MAX_OBJECT_KEYS: 200,
|
|
29
|
+
MAX_STRING_LENGTH: 64 * 1024
|
|
30
|
+
};
|
|
31
|
+
var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
|
|
8
32
|
var REPLAY_PROGRAM_VERSION = 1;
|
|
9
33
|
var IrisDir = {
|
|
10
34
|
ROOT: ".iris",
|
|
@@ -106,7 +130,7 @@ var ReplayStatus = {
|
|
|
106
130
|
DRIFT: "drift",
|
|
107
131
|
// an anchor missed (testid renamed / signal not observed) — legible drift returned
|
|
108
132
|
ERROR: "error"
|
|
109
|
-
// the flow
|
|
133
|
+
// the flow could not load or a resolved action failed
|
|
110
134
|
};
|
|
111
135
|
var DriftReason = {
|
|
112
136
|
TESTID_NOT_FOUND: "testid_not_found",
|
|
@@ -140,8 +164,10 @@ var HealStatus = {
|
|
|
140
164
|
// drift exists but no proposal cleared the confidence floor
|
|
141
165
|
NOTHING_TO_HEAL: "nothing_to_heal",
|
|
142
166
|
// replay was green
|
|
167
|
+
CONSEQUENCE_BROKEN: "consequence_broken",
|
|
168
|
+
// rebind resolves a locator but the flow's success consequence no longer fires — REFUSED (file untouched)
|
|
143
169
|
ERROR: "error"
|
|
144
|
-
// flow missing/malformed/invalid-name
|
|
170
|
+
// flow missing/malformed/invalid-name, or a resolved action failed
|
|
145
171
|
};
|
|
146
172
|
var HEAL_CONFIDENCE_MIN = 0.5;
|
|
147
173
|
var AnnotationTarget = {
|
|
@@ -157,7 +183,8 @@ var AnnotationErrorCode = {
|
|
|
157
183
|
var COMPILED_PREDICATE_PREFIX = "will";
|
|
158
184
|
var RING_BUFFER_DEFAULTS = {
|
|
159
185
|
MAX_EVENTS: 2e3,
|
|
160
|
-
MAX_AGE_MS: 6e4
|
|
186
|
+
MAX_AGE_MS: 6e4,
|
|
187
|
+
MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
|
|
161
188
|
};
|
|
162
189
|
var EventType = {
|
|
163
190
|
DOM_ADDED: "dom.added",
|
|
@@ -342,6 +369,8 @@ var MessageKind = {
|
|
|
342
369
|
|
|
343
370
|
// ../protocol/dist/messages.js
|
|
344
371
|
import { z } from "zod";
|
|
372
|
+
var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
|
|
373
|
+
var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
|
|
345
374
|
var HumanControlDataSchema = z.object({
|
|
346
375
|
kind: z.nativeEnum(HumanControlKind),
|
|
347
376
|
text: z.string().optional()
|
|
@@ -349,35 +378,37 @@ var HumanControlDataSchema = z.object({
|
|
|
349
378
|
var IrisEventSchema = z.object({
|
|
350
379
|
t: z.number(),
|
|
351
380
|
type: z.nativeEnum(EventType),
|
|
352
|
-
sessionId:
|
|
381
|
+
sessionId: sessionIdSchema,
|
|
353
382
|
/** Stable element reference this event concerns, when applicable (e.g. "e7"). */
|
|
354
|
-
ref:
|
|
383
|
+
ref: refSchema.optional(),
|
|
355
384
|
/** Event-type-specific payload. Kept open here; refined per observer at the edges. */
|
|
356
385
|
data: z.record(z.unknown()).default({})
|
|
357
386
|
});
|
|
358
387
|
var HelloMessageSchema = z.object({
|
|
359
388
|
kind: z.literal(MessageKind.HELLO),
|
|
360
|
-
protocolVersion: z.
|
|
361
|
-
sessionId:
|
|
362
|
-
url: z.string(),
|
|
363
|
-
title: z.string(),
|
|
364
|
-
adapters: z.array(z.string()),
|
|
389
|
+
protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
|
|
390
|
+
sessionId: sessionIdSchema,
|
|
391
|
+
url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
|
|
392
|
+
title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
|
|
393
|
+
adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
|
|
394
|
+
/** Optional browser/bridge pairing token. Required when the bridge configures one. */
|
|
395
|
+
token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
|
|
365
396
|
/** Whether the app has advertised a capability registry (iris.describe). */
|
|
366
397
|
hasCapabilities: z.boolean().optional()
|
|
367
398
|
});
|
|
368
399
|
var CommandMessageSchema = z.object({
|
|
369
400
|
kind: z.literal(MessageKind.COMMAND),
|
|
370
|
-
id: z.string(),
|
|
371
|
-
sessionId:
|
|
372
|
-
name: z.string(),
|
|
401
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
402
|
+
sessionId: sessionIdSchema.optional(),
|
|
403
|
+
name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
|
|
373
404
|
args: z.record(z.unknown()).default({})
|
|
374
405
|
});
|
|
375
406
|
var CommandResultSchema = z.object({
|
|
376
407
|
kind: z.literal(MessageKind.COMMAND_RESULT),
|
|
377
|
-
id: z.string(),
|
|
408
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
378
409
|
ok: z.boolean(),
|
|
379
410
|
result: z.unknown().optional(),
|
|
380
|
-
error: z.string().optional()
|
|
411
|
+
error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
|
|
381
412
|
});
|
|
382
413
|
var EventMessageSchema = z.object({
|
|
383
414
|
kind: z.literal(MessageKind.EVENT),
|
|
@@ -390,6 +421,25 @@ var IrisMessageSchema = z.discriminatedUnion("kind", [
|
|
|
390
421
|
EventMessageSchema
|
|
391
422
|
]);
|
|
392
423
|
|
|
424
|
+
// ../protocol/dist/security.js
|
|
425
|
+
var DANGEROUS_ACTION = /\b(delete|remove|destroy|erase|drop|terminate|revoke|reset|logout|log out|sign out|close account|cancel subscription|purchase|buy|pay|place order|confirm order|deploy|publish|send|transfer|withdraw|refund)\b/i;
|
|
426
|
+
function isLoopbackHostname(hostname) {
|
|
427
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
428
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
const octets = normalized.split(".");
|
|
432
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
|
|
433
|
+
if (!/^\d{1,3}$/.test(octet))
|
|
434
|
+
return false;
|
|
435
|
+
const value = Number(octet);
|
|
436
|
+
return value >= 0 && value <= 255;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function isDangerousActionText(text) {
|
|
440
|
+
return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
|
|
441
|
+
}
|
|
442
|
+
|
|
393
443
|
// ../protocol/dist/toon.js
|
|
394
444
|
var ROLE_MAP = {
|
|
395
445
|
button: "btn",
|
|
@@ -737,6 +787,7 @@ function createSharedServer() {
|
|
|
737
787
|
}
|
|
738
788
|
|
|
739
789
|
// ../server/dist/bridge.js
|
|
790
|
+
import { timingSafeEqual } from "crypto";
|
|
740
791
|
import * as http2 from "http";
|
|
741
792
|
import { WebSocketServer } from "ws";
|
|
742
793
|
|
|
@@ -744,14 +795,21 @@ import { WebSocketServer } from "ws";
|
|
|
744
795
|
var RingBuffer = class {
|
|
745
796
|
#maxEvents;
|
|
746
797
|
#maxAgeMs;
|
|
798
|
+
#maxBytes;
|
|
747
799
|
#events = [];
|
|
800
|
+
#eventBytes = [];
|
|
801
|
+
#totalBytes = 0;
|
|
748
802
|
#droppedCount = 0;
|
|
749
803
|
constructor(options = {}) {
|
|
750
804
|
this.#maxEvents = options.maxEvents ?? RING_BUFFER_DEFAULTS.MAX_EVENTS;
|
|
751
805
|
this.#maxAgeMs = options.maxAgeMs ?? RING_BUFFER_DEFAULTS.MAX_AGE_MS;
|
|
806
|
+
this.#maxBytes = options.maxBytes ?? RING_BUFFER_DEFAULTS.MAX_BYTES;
|
|
752
807
|
}
|
|
753
808
|
push(event, now) {
|
|
754
809
|
this.#events.push(event);
|
|
810
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
811
|
+
this.#eventBytes.push(bytes);
|
|
812
|
+
this.#totalBytes += bytes;
|
|
755
813
|
this.#evict(now);
|
|
756
814
|
}
|
|
757
815
|
/** Events at or after a given timestamp cursor. */
|
|
@@ -777,10 +835,14 @@ var RingBuffer = class {
|
|
|
777
835
|
#evict(now) {
|
|
778
836
|
const before = this.#events.length;
|
|
779
837
|
const cutoff = now - this.#maxAgeMs;
|
|
780
|
-
|
|
781
|
-
this.#events
|
|
838
|
+
while (this.#events.length > this.#maxEvents || this.#totalBytes > this.#maxBytes && this.#events.length > 0) {
|
|
839
|
+
this.#events.shift();
|
|
840
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
841
|
+
}
|
|
842
|
+
while ((this.#events[0]?.t ?? cutoff) < cutoff) {
|
|
843
|
+
this.#events.shift();
|
|
844
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
782
845
|
}
|
|
783
|
-
this.#events = this.#events.filter((e) => e.t >= cutoff);
|
|
784
846
|
this.#droppedCount += before - this.#events.length;
|
|
785
847
|
}
|
|
786
848
|
/** Snapshot of buffer health for the agent — total events held and cumulative drops since connect. */
|
|
@@ -1017,6 +1079,14 @@ var Session = class {
|
|
|
1017
1079
|
this.#pending.delete(id);
|
|
1018
1080
|
}
|
|
1019
1081
|
}
|
|
1082
|
+
/** End this transport without letting a stale socket remove its replacement session. */
|
|
1083
|
+
disconnect(reason) {
|
|
1084
|
+
this.rejectAll(reason);
|
|
1085
|
+
try {
|
|
1086
|
+
this.#socket.close(1008, reason);
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1020
1090
|
// ── Live-control: state machine + human→agent inbox (server-owned) ───────────────
|
|
1021
1091
|
getState() {
|
|
1022
1092
|
return this.#state;
|
|
@@ -1138,11 +1208,15 @@ var Session = class {
|
|
|
1138
1208
|
var SessionManager = class {
|
|
1139
1209
|
#sessions = /* @__PURE__ */ new Map();
|
|
1140
1210
|
add(session) {
|
|
1211
|
+
const previous = this.#sessions.get(session.id);
|
|
1141
1212
|
this.#sessions.set(session.id, session);
|
|
1213
|
+
return previous;
|
|
1142
1214
|
}
|
|
1143
|
-
remove(
|
|
1144
|
-
this.#sessions.get(
|
|
1145
|
-
|
|
1215
|
+
remove(session) {
|
|
1216
|
+
if (this.#sessions.get(session.id) !== session)
|
|
1217
|
+
return false;
|
|
1218
|
+
session.rejectAll("session disconnected");
|
|
1219
|
+
return this.#sessions.delete(session.id);
|
|
1146
1220
|
}
|
|
1147
1221
|
get(sessionId) {
|
|
1148
1222
|
return this.#sessions.get(sessionId);
|
|
@@ -1210,6 +1284,20 @@ var SessionManager = class {
|
|
|
1210
1284
|
};
|
|
1211
1285
|
|
|
1212
1286
|
// ../server/dist/bridge.js
|
|
1287
|
+
function normalizeOrigin(origin) {
|
|
1288
|
+
try {
|
|
1289
|
+
return new URL(origin).origin;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function tokensMatch(expected, received) {
|
|
1295
|
+
if (received === void 0)
|
|
1296
|
+
return false;
|
|
1297
|
+
const expectedBytes = Buffer.from(expected);
|
|
1298
|
+
const receivedBytes = Buffer.from(received);
|
|
1299
|
+
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
|
|
1300
|
+
}
|
|
1213
1301
|
function rawToString(raw) {
|
|
1214
1302
|
if (typeof raw === "string")
|
|
1215
1303
|
return raw;
|
|
@@ -1225,11 +1313,41 @@ var Bridge = class {
|
|
|
1225
1313
|
ready;
|
|
1226
1314
|
#wss;
|
|
1227
1315
|
#clock;
|
|
1316
|
+
#token;
|
|
1317
|
+
#allowedOrigins;
|
|
1318
|
+
#maxMessagesPerSecond;
|
|
1319
|
+
#maxSessions;
|
|
1320
|
+
#maxPendingConnections;
|
|
1321
|
+
#helloTimeoutMs;
|
|
1322
|
+
#pendingConnections = 0;
|
|
1228
1323
|
constructor(options) {
|
|
1324
|
+
const host = options.host ?? "127.0.0.1";
|
|
1325
|
+
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
1326
|
+
throw new Error(`Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`);
|
|
1327
|
+
}
|
|
1328
|
+
if (!isLoopbackHostname(host) && (options.token === void 0 || options.token.length === 0)) {
|
|
1329
|
+
throw new Error("a pairing token is required when the Iris bridge binds beyond localhost");
|
|
1330
|
+
}
|
|
1229
1331
|
this.#clock = options.clock ?? (() => Date.now());
|
|
1332
|
+
this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
|
|
1333
|
+
this.#allowedOrigins = new Set((options.allowedOrigins ?? []).map(normalizeOrigin).filter((origin) => origin !== null));
|
|
1334
|
+
this.#maxMessagesPerSecond = options.maxMessagesPerSecond ?? TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND;
|
|
1335
|
+
this.#maxSessions = options.maxSessions ?? TRANSPORT_LIMITS.MAX_SESSIONS;
|
|
1336
|
+
this.#maxPendingConnections = options.maxPendingConnections ?? TRANSPORT_LIMITS.MAX_PENDING_CONNECTIONS;
|
|
1337
|
+
this.#helloTimeoutMs = options.helloTimeoutMs ?? TRANSPORT_LIMITS.HELLO_TIMEOUT_MS;
|
|
1230
1338
|
if (options.server !== void 0) {
|
|
1231
1339
|
const srv = options.server;
|
|
1232
|
-
this.#wss = new WebSocketServer({
|
|
1340
|
+
this.#wss = new WebSocketServer({
|
|
1341
|
+
server: srv,
|
|
1342
|
+
path: IRIS_WS_PATH,
|
|
1343
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1344
|
+
verifyClient: ({ origin }, done) => {
|
|
1345
|
+
const allowed = this.#originAllowed(origin);
|
|
1346
|
+
if (!allowed)
|
|
1347
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1348
|
+
done(allowed, 403, "Forbidden");
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1233
1351
|
this.ready = new Promise((resolve) => {
|
|
1234
1352
|
if (srv.listening) {
|
|
1235
1353
|
resolve(srv.address().port);
|
|
@@ -1242,8 +1360,15 @@ var Bridge = class {
|
|
|
1242
1360
|
} else {
|
|
1243
1361
|
this.#wss = new WebSocketServer({
|
|
1244
1362
|
port: options.port,
|
|
1245
|
-
host
|
|
1246
|
-
path: IRIS_WS_PATH
|
|
1363
|
+
host,
|
|
1364
|
+
path: IRIS_WS_PATH,
|
|
1365
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1366
|
+
verifyClient: ({ origin }, done) => {
|
|
1367
|
+
const allowed = this.#originAllowed(origin);
|
|
1368
|
+
if (!allowed)
|
|
1369
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1370
|
+
done(allowed, 403, "Forbidden");
|
|
1371
|
+
}
|
|
1247
1372
|
});
|
|
1248
1373
|
this.ready = new Promise((resolve) => {
|
|
1249
1374
|
this.#wss.on("listening", () => {
|
|
@@ -1256,14 +1381,64 @@ var Bridge = class {
|
|
|
1256
1381
|
});
|
|
1257
1382
|
}
|
|
1258
1383
|
#onConnection(socket) {
|
|
1384
|
+
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
1385
|
+
socket.close(1013, "too many pending handshakes");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
this.#pendingConnections += 1;
|
|
1389
|
+
let awaitingHello = true;
|
|
1259
1390
|
let session;
|
|
1391
|
+
let messageWindowStartedAt = this.#clock();
|
|
1392
|
+
let messagesInWindow = 0;
|
|
1393
|
+
const releasePending = () => {
|
|
1394
|
+
if (!awaitingHello)
|
|
1395
|
+
return;
|
|
1396
|
+
awaitingHello = false;
|
|
1397
|
+
this.#pendingConnections -= 1;
|
|
1398
|
+
};
|
|
1399
|
+
const helloTimer = setTimeout(() => {
|
|
1400
|
+
if (!awaitingHello)
|
|
1401
|
+
return;
|
|
1402
|
+
releasePending();
|
|
1403
|
+
socket.close(1008, "hello timeout");
|
|
1404
|
+
}, this.#helloTimeoutMs);
|
|
1260
1405
|
socket.on("message", (raw) => {
|
|
1406
|
+
const now = this.#clock();
|
|
1407
|
+
if (now - messageWindowStartedAt >= 1e3) {
|
|
1408
|
+
messageWindowStartedAt = now;
|
|
1409
|
+
messagesInWindow = 0;
|
|
1410
|
+
}
|
|
1411
|
+
messagesInWindow += 1;
|
|
1412
|
+
if (messagesInWindow > this.#maxMessagesPerSecond) {
|
|
1413
|
+
log("message_rate_exceeded", {});
|
|
1414
|
+
socket.close(1008, "message rate exceeded");
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1261
1417
|
const parsed = this.#parse(rawToString(raw));
|
|
1262
|
-
if (parsed === null)
|
|
1418
|
+
if (parsed === null) {
|
|
1419
|
+
socket.close(1008, "invalid message");
|
|
1263
1420
|
return;
|
|
1421
|
+
}
|
|
1264
1422
|
if (parsed.kind === MessageKind.HELLO) {
|
|
1423
|
+
if (session !== void 0) {
|
|
1424
|
+
socket.close(1008, "hello already received");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (this.#token !== void 0 && !tokensMatch(this.#token, parsed.token)) {
|
|
1428
|
+
log("authentication_failed", {});
|
|
1429
|
+
socket.close(1008, "authentication failed");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const existing = this.sessions.get(parsed.sessionId);
|
|
1433
|
+
if (existing === void 0 && this.sessions.count() >= this.#maxSessions) {
|
|
1434
|
+
socket.close(1013, "session limit reached");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
clearTimeout(helloTimer);
|
|
1438
|
+
releasePending();
|
|
1265
1439
|
session = new Session(parsed, socket, this.#clock);
|
|
1266
|
-
this.sessions.add(session);
|
|
1440
|
+
const replaced = this.sessions.add(session);
|
|
1441
|
+
replaced?.disconnect("session replaced by a newer connection");
|
|
1267
1442
|
log("session_connected", { sessionId: session.id, url: session.url });
|
|
1268
1443
|
return;
|
|
1269
1444
|
}
|
|
@@ -1277,15 +1452,28 @@ var Bridge = class {
|
|
|
1277
1452
|
}
|
|
1278
1453
|
});
|
|
1279
1454
|
socket.on("close", () => {
|
|
1455
|
+
clearTimeout(helloTimer);
|
|
1456
|
+
releasePending();
|
|
1280
1457
|
if (session !== void 0) {
|
|
1281
|
-
this.sessions.remove(session
|
|
1282
|
-
|
|
1458
|
+
if (this.sessions.remove(session)) {
|
|
1459
|
+
log("session_disconnected", { sessionId: session.id });
|
|
1460
|
+
}
|
|
1283
1461
|
}
|
|
1284
1462
|
});
|
|
1285
1463
|
socket.on("error", (err) => {
|
|
1286
1464
|
log("socket_error", { error: err.message });
|
|
1287
1465
|
});
|
|
1288
1466
|
}
|
|
1467
|
+
#originAllowed(origin) {
|
|
1468
|
+
if (origin === void 0)
|
|
1469
|
+
return true;
|
|
1470
|
+
const normalized = normalizeOrigin(origin);
|
|
1471
|
+
if (normalized === null)
|
|
1472
|
+
return false;
|
|
1473
|
+
if (this.#allowedOrigins.has(normalized))
|
|
1474
|
+
return true;
|
|
1475
|
+
return isLoopbackHostname(new URL(normalized).hostname);
|
|
1476
|
+
}
|
|
1289
1477
|
#parse(text) {
|
|
1290
1478
|
let json;
|
|
1291
1479
|
try {
|
|
@@ -1302,6 +1490,8 @@ var Bridge = class {
|
|
|
1302
1490
|
}
|
|
1303
1491
|
close() {
|
|
1304
1492
|
return new Promise((resolve) => {
|
|
1493
|
+
for (const client of this.#wss.clients)
|
|
1494
|
+
client.terminate();
|
|
1305
1495
|
this.#wss.close(() => {
|
|
1306
1496
|
resolve();
|
|
1307
1497
|
});
|
|
@@ -1416,6 +1606,7 @@ var IrisTool = {
|
|
|
1416
1606
|
STATE: "iris_state",
|
|
1417
1607
|
CAPABILITIES: "iris_capabilities",
|
|
1418
1608
|
CONTRACT_SAVE: "iris_contract_save",
|
|
1609
|
+
DOMAIN: "iris_domain",
|
|
1419
1610
|
FLOW_SAVE: "iris_flow_save",
|
|
1420
1611
|
FLOW_LIST: "iris_flow_list",
|
|
1421
1612
|
FLOW_LOAD: "iris_flow_load",
|
|
@@ -1458,189 +1649,558 @@ var IrisTool = {
|
|
|
1458
1649
|
ROLLBACK: "iris_rollback"
|
|
1459
1650
|
};
|
|
1460
1651
|
|
|
1461
|
-
// ../server/dist/
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
1469
|
-
project: join(root, IrisDir.PROJECT_FILE),
|
|
1470
|
-
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
function visualPath(root, name) {
|
|
1474
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
1475
|
-
}
|
|
1476
|
-
function visualDiffPath(root, name) {
|
|
1477
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
1478
|
-
}
|
|
1479
|
-
function flowPath(root, name) {
|
|
1480
|
-
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
1481
|
-
}
|
|
1482
|
-
function isValidFlowName(name) {
|
|
1483
|
-
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
1484
|
-
}
|
|
1485
|
-
function baselinePath(root, name) {
|
|
1486
|
-
return join(root, IrisDir.BASELINES_SUBDIR, `${name}.json`);
|
|
1487
|
-
}
|
|
1488
|
-
async function ensureIrisDir(fs2, root) {
|
|
1489
|
-
const p = irisDirPaths(root);
|
|
1490
|
-
await fs2.mkdir(p.root);
|
|
1491
|
-
await fs2.mkdir(p.flows);
|
|
1492
|
-
await fs2.mkdir(p.baselines);
|
|
1493
|
-
}
|
|
1494
|
-
var JSON_INDENT = 2;
|
|
1495
|
-
function stableSerialize(capabilities, generatedAt) {
|
|
1496
|
-
const envelope = {
|
|
1497
|
-
version: CONTRACT_FILE_VERSION,
|
|
1498
|
-
generatedAt,
|
|
1499
|
-
capabilities: {
|
|
1500
|
-
testids: [...capabilities.testids].sort(),
|
|
1501
|
-
signals: [...capabilities.signals].sort(),
|
|
1502
|
-
stores: [...capabilities.stores].sort(),
|
|
1503
|
-
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
1652
|
+
// ../server/dist/tools/tools-helpers.js
|
|
1653
|
+
function parseInteractive(tree) {
|
|
1654
|
+
const items = [];
|
|
1655
|
+
for (const line of tree.split("\n")) {
|
|
1656
|
+
const match = /\(ref=(e\d+)\)/.exec(line);
|
|
1657
|
+
if (match !== null) {
|
|
1658
|
+
items.push({ ref: match[1] ?? "", desc: line.replace(/\s*\(ref=e\d+\)/, "").trim() });
|
|
1504
1659
|
}
|
|
1505
|
-
};
|
|
1506
|
-
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
1507
|
-
`;
|
|
1508
|
-
}
|
|
1509
|
-
async function writeContract(fs2, root, capabilities, now) {
|
|
1510
|
-
await ensureIrisDir(fs2, root);
|
|
1511
|
-
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
1512
|
-
}
|
|
1513
|
-
async function readContract(fs2, root) {
|
|
1514
|
-
const path = irisDirPaths(root).contract;
|
|
1515
|
-
if (!await fs2.exists(path))
|
|
1516
|
-
return { ok: false, reason: ContractReadError.MISSING };
|
|
1517
|
-
let text;
|
|
1518
|
-
try {
|
|
1519
|
-
text = await fs2.readFile(path);
|
|
1520
|
-
} catch (error) {
|
|
1521
|
-
return {
|
|
1522
|
-
ok: false,
|
|
1523
|
-
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
1524
|
-
};
|
|
1525
1660
|
}
|
|
1526
|
-
|
|
1527
|
-
try {
|
|
1528
|
-
parsed = JSON.parse(text);
|
|
1529
|
-
} catch {
|
|
1530
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1531
|
-
}
|
|
1532
|
-
const result = ContractFileSchema.safeParse(parsed);
|
|
1533
|
-
if (!result.success)
|
|
1534
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1535
|
-
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
1661
|
+
return items;
|
|
1536
1662
|
}
|
|
1537
|
-
|
|
1538
|
-
// ../server/dist/flows/flows.js
|
|
1539
1663
|
function asString(value) {
|
|
1540
1664
|
return typeof value === "string" ? value : void 0;
|
|
1541
1665
|
}
|
|
1666
|
+
function asNumber(value) {
|
|
1667
|
+
return typeof value === "number" ? value : void 0;
|
|
1668
|
+
}
|
|
1542
1669
|
function asRecord(value) {
|
|
1543
1670
|
return typeof value === "object" && value !== null ? value : {};
|
|
1544
1671
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1672
|
+
|
|
1673
|
+
// ../server/dist/flows/replay.js
|
|
1674
|
+
function asString2(value) {
|
|
1675
|
+
return typeof value === "string" ? value : void 0;
|
|
1547
1676
|
}
|
|
1548
|
-
function
|
|
1549
|
-
|
|
1550
|
-
const by = asString(sub["by"]);
|
|
1551
|
-
const value = asString(sub["value"]);
|
|
1552
|
-
const action = asString(sub["action"]);
|
|
1553
|
-
const args = asRecord(sub["args"]);
|
|
1554
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1555
|
-
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
1556
|
-
}
|
|
1557
|
-
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
1677
|
+
function asRecord2(value) {
|
|
1678
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1558
1679
|
}
|
|
1559
|
-
function
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
return step;
|
|
1680
|
+
function replayActionArgs(value, confirmDangerous = false) {
|
|
1681
|
+
const args = { ...asRecord2(value) };
|
|
1682
|
+
delete args[DANGEROUS_ACTION_CONFIRM_ARG];
|
|
1683
|
+
if (confirmDangerous)
|
|
1684
|
+
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
1685
|
+
return args;
|
|
1566
1686
|
}
|
|
1567
|
-
function
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
out2.expect = step.expect;
|
|
1578
|
-
return out2;
|
|
1687
|
+
function compileActStep(args, res) {
|
|
1688
|
+
const testid = asString2(asRecord2(res)["testid"]);
|
|
1689
|
+
const action = asString2(args["action"]) ?? "";
|
|
1690
|
+
const actArgs = replayActionArgs(args["args"]);
|
|
1691
|
+
if (testid !== void 0) {
|
|
1692
|
+
return {
|
|
1693
|
+
tool: IrisTool.ACT,
|
|
1694
|
+
stable: true,
|
|
1695
|
+
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
1696
|
+
};
|
|
1579
1697
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if (step.expect !== void 0)
|
|
1586
|
-
out.expect = step.expect;
|
|
1587
|
-
return out;
|
|
1698
|
+
return {
|
|
1699
|
+
tool: IrisTool.ACT,
|
|
1700
|
+
stable: false,
|
|
1701
|
+
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
1702
|
+
};
|
|
1588
1703
|
}
|
|
1589
|
-
function
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1704
|
+
function compileSequenceStep(args, res) {
|
|
1705
|
+
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
1706
|
+
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
1707
|
+
let stable = inputSteps.length > 0;
|
|
1708
|
+
const subSteps = inputSteps.map((raw, i) => {
|
|
1709
|
+
const step = asRecord2(raw);
|
|
1710
|
+
const action = asString2(step["action"]) ?? "";
|
|
1711
|
+
const stepArgs = replayActionArgs(step["args"]);
|
|
1712
|
+
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
1713
|
+
if (testid !== void 0) {
|
|
1714
|
+
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
1715
|
+
}
|
|
1716
|
+
stable = false;
|
|
1717
|
+
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
1595
1718
|
});
|
|
1596
|
-
|
|
1597
|
-
if (ann.dynamic.length > 0) {
|
|
1598
|
-
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
1599
|
-
}
|
|
1600
|
-
if (ann.success !== void 0)
|
|
1601
|
-
out.success = ann.success;
|
|
1602
|
-
return out;
|
|
1719
|
+
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
1603
1720
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
1617
|
-
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
1618
|
-
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
1619
|
-
*/
|
|
1620
|
-
#serialize(flow) {
|
|
1621
|
-
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
1622
|
-
`;
|
|
1721
|
+
async function resolveRef(session, step) {
|
|
1722
|
+
const by = asString2(step.by);
|
|
1723
|
+
const value = asString2(step.value);
|
|
1724
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1725
|
+
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
1726
|
+
if (!result.ok)
|
|
1727
|
+
throw new Error(result.error ?? "query failed");
|
|
1728
|
+
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
1729
|
+
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
1730
|
+
if (ref2 === void 0)
|
|
1731
|
+
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
1732
|
+
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
1623
1733
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1734
|
+
const ref = asString2(step.ref);
|
|
1735
|
+
if (ref === void 0 || ref.length === 0)
|
|
1736
|
+
throw new Error("step has no testid or ref to resolve");
|
|
1737
|
+
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
1738
|
+
}
|
|
1739
|
+
async function replayProgram(session, program, confirmDangerous = false) {
|
|
1740
|
+
const results = [];
|
|
1741
|
+
for (const step of program.steps) {
|
|
1742
|
+
try {
|
|
1743
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
1744
|
+
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
1745
|
+
const notes = [];
|
|
1746
|
+
const liveSteps = [];
|
|
1747
|
+
for (const raw of subs) {
|
|
1748
|
+
const sub = asRecord2(raw);
|
|
1749
|
+
const { ref, note } = await resolveRef(session, sub);
|
|
1750
|
+
if (note !== void 0)
|
|
1751
|
+
notes.push(note);
|
|
1752
|
+
liveSteps.push({
|
|
1753
|
+
ref,
|
|
1754
|
+
action: asString2(sub["action"]) ?? "",
|
|
1755
|
+
args: replayActionArgs(sub["args"], confirmDangerous)
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
1759
|
+
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
1760
|
+
if (!r.ok)
|
|
1761
|
+
break;
|
|
1762
|
+
} else {
|
|
1763
|
+
const { ref, note } = await resolveRef(session, step.args);
|
|
1764
|
+
const r = await session.command(IrisCommand.ACT, {
|
|
1765
|
+
ref,
|
|
1766
|
+
action: asString2(step.args["action"]) ?? "",
|
|
1767
|
+
args: replayActionArgs(step.args["args"], confirmDangerous)
|
|
1768
|
+
});
|
|
1769
|
+
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
1770
|
+
if (!r.ok)
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
results.push({
|
|
1775
|
+
tool: step.tool,
|
|
1776
|
+
ok: false,
|
|
1777
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1778
|
+
});
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return results;
|
|
1783
|
+
}
|
|
1784
|
+
function buildResult(tool, ok, error, notes) {
|
|
1785
|
+
const base = { tool, ok };
|
|
1786
|
+
if (!ok)
|
|
1787
|
+
base.error = error ?? "command failed";
|
|
1788
|
+
if (notes.length > 0)
|
|
1789
|
+
base.note = notes.join("; ");
|
|
1790
|
+
return base;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// ../server/dist/flows/flow-replay.js
|
|
1794
|
+
function editDistance(a, b) {
|
|
1795
|
+
const s = a.toLowerCase();
|
|
1796
|
+
const t = b.toLowerCase();
|
|
1797
|
+
const rows = s.length + 1;
|
|
1798
|
+
const cols = t.length + 1;
|
|
1799
|
+
const prev = new Array(cols);
|
|
1800
|
+
const curr = new Array(cols);
|
|
1801
|
+
for (let j = 0; j < cols; j++)
|
|
1802
|
+
prev[j] = j;
|
|
1803
|
+
for (let i = 1; i < rows; i++) {
|
|
1804
|
+
curr[0] = i;
|
|
1805
|
+
for (let j = 1; j < cols; j++) {
|
|
1806
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
1807
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1808
|
+
}
|
|
1809
|
+
for (let j = 0; j < cols; j++)
|
|
1810
|
+
prev[j] = curr[j] ?? 0;
|
|
1811
|
+
}
|
|
1812
|
+
return prev[cols - 1] ?? 0;
|
|
1813
|
+
}
|
|
1814
|
+
function nearestTestid(missing, present) {
|
|
1815
|
+
let best = null;
|
|
1816
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1817
|
+
for (const candidate of present) {
|
|
1818
|
+
const distance = editDistance(missing, candidate);
|
|
1819
|
+
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
1820
|
+
best = candidate;
|
|
1821
|
+
bestDistance = distance;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return best;
|
|
1825
|
+
}
|
|
1826
|
+
function readQuery(result) {
|
|
1827
|
+
if (!result.ok)
|
|
1828
|
+
return { refs: [] };
|
|
1829
|
+
const payload = asRecord(result.result);
|
|
1830
|
+
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
1831
|
+
const refs = elements.map((e) => asString(asRecord(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
1832
|
+
const rawHint = payload["hint"];
|
|
1833
|
+
if (typeof rawHint === "object" && rawHint !== null) {
|
|
1834
|
+
const hint = asRecord(rawHint);
|
|
1835
|
+
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
1836
|
+
return {
|
|
1837
|
+
refs,
|
|
1838
|
+
hint: {
|
|
1839
|
+
route: asString(hint["route"]) ?? "",
|
|
1840
|
+
presentTestids: present,
|
|
1841
|
+
presentRegions: [],
|
|
1842
|
+
knownEmptyState: hint["knownEmptyState"] === true
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return { refs };
|
|
1847
|
+
}
|
|
1848
|
+
function nearestIsAmbiguous(missing, present) {
|
|
1849
|
+
if (present.length < 2)
|
|
1850
|
+
return false;
|
|
1851
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1852
|
+
let count = 0;
|
|
1853
|
+
for (const candidate of present) {
|
|
1854
|
+
const distance = editDistance(missing, candidate);
|
|
1855
|
+
if (distance < min) {
|
|
1856
|
+
min = distance;
|
|
1857
|
+
count = 1;
|
|
1858
|
+
} else if (distance === min) {
|
|
1859
|
+
count += 1;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return count >= 2;
|
|
1863
|
+
}
|
|
1864
|
+
function testidDrift(value, hint) {
|
|
1865
|
+
const present = hint?.presentTestids ?? [];
|
|
1866
|
+
const drift = {
|
|
1867
|
+
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
1868
|
+
reason: `testid "${value}" not found`,
|
|
1869
|
+
anchor: value,
|
|
1870
|
+
nearest: nearestTestid(value, present)
|
|
1871
|
+
};
|
|
1872
|
+
if (nearestIsAmbiguous(value, present))
|
|
1873
|
+
drift.ambiguous = true;
|
|
1874
|
+
return drift;
|
|
1875
|
+
}
|
|
1876
|
+
function anchorLabel(anchor) {
|
|
1877
|
+
if (anchor.kind === AnchorKind.TESTID)
|
|
1878
|
+
return anchor.value;
|
|
1879
|
+
if (anchor.kind === AnchorKind.SIGNAL)
|
|
1880
|
+
return anchor.name;
|
|
1881
|
+
return anchor.name ?? anchor.role;
|
|
1882
|
+
}
|
|
1883
|
+
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous) {
|
|
1884
|
+
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
1885
|
+
const { refs, hint } = readQuery(queryResult);
|
|
1886
|
+
if (refs.length === 0) {
|
|
1887
|
+
return {
|
|
1888
|
+
step: index,
|
|
1889
|
+
tool: step.tool,
|
|
1890
|
+
anchor: value,
|
|
1891
|
+
ok: false,
|
|
1892
|
+
drift: testidDrift(value, hint)
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
const ref = refs[0] ?? "";
|
|
1896
|
+
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
1897
|
+
const act = await session.command(IrisCommand.ACT, {
|
|
1898
|
+
ref,
|
|
1899
|
+
action: step.action ?? "",
|
|
1900
|
+
args: replayActionArgs(step.args, confirmDangerous)
|
|
1901
|
+
});
|
|
1902
|
+
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
1903
|
+
if (!act.ok) {
|
|
1904
|
+
result.error = act.error ?? "command failed";
|
|
1905
|
+
if (note !== void 0)
|
|
1906
|
+
result.note = note;
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
const expectTestid = step.expect?.element?.testid;
|
|
1910
|
+
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
1911
|
+
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
1912
|
+
by: QueryBy.TESTID,
|
|
1913
|
+
value: expectTestid
|
|
1914
|
+
});
|
|
1915
|
+
const expectRefs = readQuery(expectQuery);
|
|
1916
|
+
if (expectRefs.refs.length === 0) {
|
|
1917
|
+
return {
|
|
1918
|
+
step: index,
|
|
1919
|
+
tool: step.tool,
|
|
1920
|
+
anchor: expectTestid,
|
|
1921
|
+
ok: false,
|
|
1922
|
+
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (note !== void 0)
|
|
1927
|
+
result.note = note;
|
|
1928
|
+
return result;
|
|
1929
|
+
}
|
|
1930
|
+
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
1931
|
+
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
1932
|
+
if (verdict.pass)
|
|
1933
|
+
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
1934
|
+
return {
|
|
1935
|
+
step: index,
|
|
1936
|
+
tool: step.tool,
|
|
1937
|
+
anchor: name,
|
|
1938
|
+
ok: false,
|
|
1939
|
+
drift: {
|
|
1940
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
1941
|
+
reason: `signal "${name}" not observed`,
|
|
1942
|
+
anchor: name,
|
|
1943
|
+
nearest: null
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false) {
|
|
1948
|
+
const results = [];
|
|
1949
|
+
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
1950
|
+
let index = 0;
|
|
1951
|
+
for (const step of flow.steps) {
|
|
1952
|
+
const label = anchorLabel(step.anchor);
|
|
1953
|
+
let result;
|
|
1954
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
1955
|
+
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
1956
|
+
} else {
|
|
1957
|
+
result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous);
|
|
1958
|
+
}
|
|
1959
|
+
results.push(result);
|
|
1960
|
+
if (result.drift !== void 0 || !result.ok)
|
|
1961
|
+
break;
|
|
1962
|
+
index += 1;
|
|
1963
|
+
}
|
|
1964
|
+
return results;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// ../server/dist/flows/heal.js
|
|
1968
|
+
function confidenceFor(from, to) {
|
|
1969
|
+
if (from === to)
|
|
1970
|
+
return 1;
|
|
1971
|
+
const span = Math.max(from.length, to.length);
|
|
1972
|
+
if (span === 0)
|
|
1973
|
+
return 1;
|
|
1974
|
+
const raw = 1 - editDistance(from, to) / span;
|
|
1975
|
+
if (raw >= 1)
|
|
1976
|
+
return 1;
|
|
1977
|
+
if (raw <= 0)
|
|
1978
|
+
return Number.EPSILON;
|
|
1979
|
+
return raw;
|
|
1980
|
+
}
|
|
1981
|
+
function applyHealChanges(flow, changes) {
|
|
1982
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
1983
|
+
for (const change of changes)
|
|
1984
|
+
byStep.set(change.step, change);
|
|
1985
|
+
const applied = [];
|
|
1986
|
+
const steps = flow.steps.map((step, index) => {
|
|
1987
|
+
const change = byStep.get(index);
|
|
1988
|
+
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1989
|
+
return step;
|
|
1990
|
+
}
|
|
1991
|
+
applied.push(change);
|
|
1992
|
+
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1993
|
+
});
|
|
1994
|
+
return { flow: { ...flow, steps }, applied };
|
|
1995
|
+
}
|
|
1996
|
+
function proposeRebindWith(drift, step, minConfidence) {
|
|
1997
|
+
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
1998
|
+
return void 0;
|
|
1999
|
+
if (drift.ambiguous === true)
|
|
2000
|
+
return void 0;
|
|
2001
|
+
const to = drift.nearest;
|
|
2002
|
+
if (to === null)
|
|
2003
|
+
return void 0;
|
|
2004
|
+
const confidence = confidenceFor(drift.anchor, to);
|
|
2005
|
+
if (confidence < minConfidence)
|
|
2006
|
+
return void 0;
|
|
2007
|
+
return { step, from: drift.anchor, to, confidence };
|
|
2008
|
+
}
|
|
2009
|
+
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
2010
|
+
const proposals = [];
|
|
2011
|
+
for (const step of steps) {
|
|
2012
|
+
if (step.drift === void 0)
|
|
2013
|
+
continue;
|
|
2014
|
+
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2015
|
+
if (proposal !== void 0)
|
|
2016
|
+
proposals.push(proposal);
|
|
2017
|
+
}
|
|
2018
|
+
return proposals;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// ../server/dist/project/iris-dir.js
|
|
2022
|
+
import { join } from "path";
|
|
2023
|
+
function irisDirPaths(root) {
|
|
2024
|
+
return {
|
|
2025
|
+
root,
|
|
2026
|
+
contract: join(root, IrisDir.CONTRACT_FILE),
|
|
2027
|
+
flows: join(root, IrisDir.FLOWS_SUBDIR),
|
|
2028
|
+
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
2029
|
+
project: join(root, IrisDir.PROJECT_FILE),
|
|
2030
|
+
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function visualPath(root, name) {
|
|
2034
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
2035
|
+
}
|
|
2036
|
+
function visualDiffPath(root, name) {
|
|
2037
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
2038
|
+
}
|
|
2039
|
+
function flowPath(root, name) {
|
|
2040
|
+
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
2041
|
+
}
|
|
2042
|
+
function isValidFlowName(name) {
|
|
2043
|
+
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
2044
|
+
}
|
|
2045
|
+
function baselinePath(root, name) {
|
|
2046
|
+
return join(root, IrisDir.BASELINES_SUBDIR, `${name}.json`);
|
|
2047
|
+
}
|
|
2048
|
+
async function ensureIrisDir(fs2, root) {
|
|
2049
|
+
const p = irisDirPaths(root);
|
|
2050
|
+
await fs2.mkdir(p.root);
|
|
2051
|
+
await fs2.mkdir(p.flows);
|
|
2052
|
+
await fs2.mkdir(p.baselines);
|
|
2053
|
+
}
|
|
2054
|
+
var JSON_INDENT = 2;
|
|
2055
|
+
function stableSerialize(capabilities, generatedAt) {
|
|
2056
|
+
const envelope = {
|
|
2057
|
+
version: CONTRACT_FILE_VERSION,
|
|
2058
|
+
generatedAt,
|
|
2059
|
+
capabilities: {
|
|
2060
|
+
testids: [...capabilities.testids].sort(),
|
|
2061
|
+
signals: [...capabilities.signals].sort(),
|
|
2062
|
+
stores: [...capabilities.stores].sort(),
|
|
2063
|
+
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
2067
|
+
`;
|
|
2068
|
+
}
|
|
2069
|
+
async function writeContract(fs2, root, capabilities, now) {
|
|
2070
|
+
await ensureIrisDir(fs2, root);
|
|
2071
|
+
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
2072
|
+
}
|
|
2073
|
+
async function readContract(fs2, root) {
|
|
2074
|
+
const path = irisDirPaths(root).contract;
|
|
2075
|
+
if (!await fs2.exists(path))
|
|
2076
|
+
return { ok: false, reason: ContractReadError.MISSING };
|
|
2077
|
+
let text;
|
|
2078
|
+
try {
|
|
2079
|
+
text = await fs2.readFile(path);
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return {
|
|
2082
|
+
ok: false,
|
|
2083
|
+
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
let parsed;
|
|
2087
|
+
try {
|
|
2088
|
+
parsed = JSON.parse(text);
|
|
2089
|
+
} catch {
|
|
2090
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2091
|
+
}
|
|
2092
|
+
const result = ContractFileSchema.safeParse(parsed);
|
|
2093
|
+
if (!result.success)
|
|
2094
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2095
|
+
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// ../server/dist/flows/flows.js
|
|
2099
|
+
function asString3(value) {
|
|
2100
|
+
return typeof value === "string" ? value : void 0;
|
|
2101
|
+
}
|
|
2102
|
+
function asRecord3(value) {
|
|
2103
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
2104
|
+
}
|
|
2105
|
+
function degradedAnchor() {
|
|
2106
|
+
return { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE };
|
|
2107
|
+
}
|
|
2108
|
+
function subStepToFlowStep(raw) {
|
|
2109
|
+
const sub = asRecord3(raw);
|
|
2110
|
+
const by = asString3(sub["by"]);
|
|
2111
|
+
const value = asString3(sub["value"]);
|
|
2112
|
+
const action = asString3(sub["action"]);
|
|
2113
|
+
const args = asRecord3(sub["args"]);
|
|
2114
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2115
|
+
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
2116
|
+
}
|
|
2117
|
+
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
2118
|
+
}
|
|
2119
|
+
function buildStep(tool, anchor, action, args, degraded) {
|
|
2120
|
+
const step = { tool, anchor, args };
|
|
2121
|
+
if (action !== void 0)
|
|
2122
|
+
step.action = action;
|
|
2123
|
+
if (degraded)
|
|
2124
|
+
step.degraded = true;
|
|
2125
|
+
return step;
|
|
2126
|
+
}
|
|
2127
|
+
function recordedStepToFlowStep(step) {
|
|
2128
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2129
|
+
const rawSubs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2130
|
+
const subs = rawSubs.map(subStepToFlowStep);
|
|
2131
|
+
const degraded = subs.some((s) => s.degraded === true);
|
|
2132
|
+
const anchor = subs[0]?.anchor ?? degradedAnchor();
|
|
2133
|
+
const out2 = { tool: IrisTool.ACT_SEQUENCE, anchor, steps: subs };
|
|
2134
|
+
if (degraded)
|
|
2135
|
+
out2.degraded = true;
|
|
2136
|
+
if (step.expect !== void 0)
|
|
2137
|
+
out2.expect = step.expect;
|
|
2138
|
+
return out2;
|
|
2139
|
+
}
|
|
2140
|
+
const by = asString3(step.args["by"]);
|
|
2141
|
+
const value = asString3(step.args["value"]);
|
|
2142
|
+
const action = asString3(step.args["action"]);
|
|
2143
|
+
const args = asRecord3(step.args["args"]);
|
|
2144
|
+
const out = by === QueryBy.TESTID && value !== void 0 ? buildStep(step.tool, { kind: AnchorKind.TESTID, value }, action, args, false) : buildStep(step.tool, degradedAnchor(), action, args, true);
|
|
2145
|
+
if (step.expect !== void 0)
|
|
2146
|
+
out.expect = step.expect;
|
|
2147
|
+
return out;
|
|
2148
|
+
}
|
|
2149
|
+
function withAnnotations(flow, ann) {
|
|
2150
|
+
if (ann === void 0)
|
|
2151
|
+
return flow;
|
|
2152
|
+
const steps = flow.steps.map((step, i) => {
|
|
2153
|
+
const expect = ann.stepExpect.get(i);
|
|
2154
|
+
return expect === void 0 ? step : { ...step, expect };
|
|
2155
|
+
});
|
|
2156
|
+
const out = { ...flow, steps };
|
|
2157
|
+
if (ann.dynamic.length > 0) {
|
|
2158
|
+
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
2159
|
+
}
|
|
2160
|
+
if (ann.success !== void 0)
|
|
2161
|
+
out.success = ann.success;
|
|
2162
|
+
return out;
|
|
2163
|
+
}
|
|
2164
|
+
var JSON_INDENT2 = 2;
|
|
2165
|
+
var FLOW_SUFFIX = ".json";
|
|
2166
|
+
var FlowStore = class {
|
|
2167
|
+
#fs;
|
|
2168
|
+
#root;
|
|
2169
|
+
#clock;
|
|
2170
|
+
constructor(fs2, root, clock) {
|
|
2171
|
+
this.#fs = fs2;
|
|
2172
|
+
this.#root = root;
|
|
2173
|
+
this.#clock = clock;
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
2177
|
+
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
2178
|
+
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
2179
|
+
*/
|
|
2180
|
+
#serialize(flow) {
|
|
2181
|
+
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
2182
|
+
`;
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
2186
|
+
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
2187
|
+
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
2188
|
+
*/
|
|
2189
|
+
async save(program, annotations) {
|
|
2190
|
+
if (!isValidFlowName(program.name)) {
|
|
2191
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
2192
|
+
}
|
|
2193
|
+
const steps = program.steps.map(recordedStepToFlowStep);
|
|
2194
|
+
const base = {
|
|
2195
|
+
version: FLOW_FILE_VERSION,
|
|
2196
|
+
name: program.name,
|
|
2197
|
+
createdAt: this.#clock.now(),
|
|
2198
|
+
steps
|
|
2199
|
+
};
|
|
2200
|
+
const flow = withAnnotations(base, annotations);
|
|
2201
|
+
await this.#fs.mkdir(irisDirPaths(this.#root).flows);
|
|
2202
|
+
await this.#fs.writeFile(flowPath(this.#root, program.name), this.#serialize(flow));
|
|
2203
|
+
const degraded = flow.steps.filter((s) => s.degraded === true).length;
|
|
1644
2204
|
return {
|
|
1645
2205
|
ok: true,
|
|
1646
2206
|
value: {
|
|
@@ -1694,19 +2254,7 @@ var FlowStore = class {
|
|
|
1694
2254
|
if (!loaded.ok)
|
|
1695
2255
|
return { ok: false, code: loaded.code };
|
|
1696
2256
|
const flow = loaded.value;
|
|
1697
|
-
const
|
|
1698
|
-
for (const change of changes)
|
|
1699
|
-
byStep.set(change.step, change);
|
|
1700
|
-
const applied = [];
|
|
1701
|
-
const steps = flow.steps.map((step, index) => {
|
|
1702
|
-
const change = byStep.get(index);
|
|
1703
|
-
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1704
|
-
return step;
|
|
1705
|
-
}
|
|
1706
|
-
applied.push(change);
|
|
1707
|
-
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1708
|
-
});
|
|
1709
|
-
const next = { ...flow, steps };
|
|
2257
|
+
const { flow: next, applied } = applyHealChanges(flow, changes);
|
|
1710
2258
|
await this.#fs.writeFile(flowPath(this.#root, name), this.#serialize(next));
|
|
1711
2259
|
return { ok: true, value: { name, changed: applied } };
|
|
1712
2260
|
}
|
|
@@ -1934,10 +2482,10 @@ function createNodeFileSystem() {
|
|
|
1934
2482
|
|
|
1935
2483
|
// ../server/dist/mcp.js
|
|
1936
2484
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1937
|
-
import { z as
|
|
2485
|
+
import { z as z17 } from "zod";
|
|
1938
2486
|
|
|
1939
2487
|
// ../server/dist/tools/tools.js
|
|
1940
|
-
import { z as
|
|
2488
|
+
import { z as z16 } from "zod";
|
|
1941
2489
|
|
|
1942
2490
|
// ../server/dist/input/real-input.js
|
|
1943
2491
|
var DriveError = class extends Error {
|
|
@@ -1998,8 +2546,10 @@ async function performGesture(page, action, box, args, sleep) {
|
|
|
1998
2546
|
}
|
|
1999
2547
|
return { performed: false, center };
|
|
2000
2548
|
}
|
|
2549
|
+
var HIDE_IRIS_CHROME_CSS = "[data-iris-overlay]{display:none !important}";
|
|
2550
|
+
var SCREENSHOT_DETERMINISM = { style: HIDE_IRIS_CHROME_CSS, animations: "disabled" };
|
|
2001
2551
|
async function capturePage(page, opts) {
|
|
2002
|
-
const buf = await page.screenshot(opts.clip !== void 0 ? { clip: opts.clip } : opts.fullPage === true ? { fullPage: true } : {});
|
|
2552
|
+
const buf = await page.screenshot(opts.clip !== void 0 ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip } : opts.fullPage === true ? { ...SCREENSHOT_DETERMINISM, fullPage: true } : { ...SCREENSHOT_DETERMINISM });
|
|
2003
2553
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2004
2554
|
}
|
|
2005
2555
|
var nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -2234,6 +2784,7 @@ var PredicateSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
|
|
|
2234
2784
|
name: z3.string().optional(),
|
|
2235
2785
|
dataMatches: z3.record(z3.unknown()).optional()
|
|
2236
2786
|
}),
|
|
2787
|
+
z3.object({ kind: z3.literal("settled"), quietMs: z3.number().positive().optional() }),
|
|
2237
2788
|
z3.object({ kind: z3.literal("allOf"), predicates: z3.array(PredicateSchema) }),
|
|
2238
2789
|
z3.object({ kind: z3.literal("anyOf"), predicates: z3.array(PredicateSchema) }),
|
|
2239
2790
|
z3.object({ kind: z3.literal("not"), predicate: PredicateSchema })
|
|
@@ -2430,6 +2981,39 @@ function evalSignal(events, p) {
|
|
|
2430
2981
|
evidence: sameName.length > 0 ? { nearMiss: sameName } : void 0
|
|
2431
2982
|
};
|
|
2432
2983
|
}
|
|
2984
|
+
var SETTLE_ACTIVITY = /* @__PURE__ */ new Set([
|
|
2985
|
+
EventType.NET_REQUEST,
|
|
2986
|
+
EventType.DOM_ADDED,
|
|
2987
|
+
EventType.DOM_REMOVED,
|
|
2988
|
+
EventType.DOM_ATTR
|
|
2989
|
+
]);
|
|
2990
|
+
var DEFAULT_QUIET_MS = 500;
|
|
2991
|
+
function evalSettled(events, p, now) {
|
|
2992
|
+
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
2993
|
+
let lastT = -1;
|
|
2994
|
+
let lastType;
|
|
2995
|
+
for (const e of events) {
|
|
2996
|
+
if (SETTLE_ACTIVITY.has(e.type) && e.t > lastT) {
|
|
2997
|
+
lastT = e.t;
|
|
2998
|
+
lastType = e.type;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (lastT < 0) {
|
|
3002
|
+
return {
|
|
3003
|
+
pass: true,
|
|
3004
|
+
evidence: { settled: true, quietForMs: null, note: "no activity to settle" }
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
const quietForMs = now - lastT;
|
|
3008
|
+
if (quietForMs >= quietMs) {
|
|
3009
|
+
return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
|
|
3010
|
+
}
|
|
3011
|
+
return {
|
|
3012
|
+
pass: false,
|
|
3013
|
+
failureReason: `not settled: last activity (${String(lastType)}) ${String(quietForMs)}ms ago, need ${String(quietMs)}ms quiet`,
|
|
3014
|
+
evidence: { quietForMs, lastActivity: lastType }
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
2433
3017
|
async function evaluatePredicate(session, predicate, since = 0) {
|
|
2434
3018
|
const events = session.eventsSince(since);
|
|
2435
3019
|
switch (predicate.kind) {
|
|
@@ -2447,6 +3031,8 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2447
3031
|
return evalAnimation(events, predicate);
|
|
2448
3032
|
case "signal":
|
|
2449
3033
|
return evalSignal(events, predicate);
|
|
3034
|
+
case "settled":
|
|
3035
|
+
return evalSettled(events, predicate, session.elapsed());
|
|
2450
3036
|
case "allOf": {
|
|
2451
3037
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since)));
|
|
2452
3038
|
const failed = results.find((r) => !r.pass);
|
|
@@ -2472,6 +3058,10 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2472
3058
|
function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
2473
3059
|
return new Promise((resolve) => {
|
|
2474
3060
|
let done = false;
|
|
3061
|
+
const failed = (error) => ({
|
|
3062
|
+
pass: false,
|
|
3063
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
3064
|
+
});
|
|
2475
3065
|
const finish = (result) => {
|
|
2476
3066
|
if (done)
|
|
2477
3067
|
return;
|
|
@@ -2485,6 +3075,8 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2485
3075
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2486
3076
|
if (r.pass)
|
|
2487
3077
|
finish(r);
|
|
3078
|
+
}).catch((error) => {
|
|
3079
|
+
finish(failed(error));
|
|
2488
3080
|
});
|
|
2489
3081
|
};
|
|
2490
3082
|
const unsub = session.onEvent(() => {
|
|
@@ -2498,141 +3090,30 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2498
3090
|
evidence: r.evidence,
|
|
2499
3091
|
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2500
3092
|
});
|
|
3093
|
+
}).catch((error) => {
|
|
3094
|
+
finish(failed(error));
|
|
2501
3095
|
});
|
|
2502
3096
|
}, timeoutMs);
|
|
2503
3097
|
check();
|
|
2504
3098
|
});
|
|
2505
3099
|
}
|
|
2506
3100
|
|
|
2507
|
-
// ../server/dist/flows/replay.js
|
|
2508
|
-
function asString2(value) {
|
|
2509
|
-
return typeof value === "string" ? value : void 0;
|
|
2510
|
-
}
|
|
2511
|
-
function asRecord2(value) {
|
|
2512
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
2513
|
-
}
|
|
2514
|
-
function compileActStep(args, res) {
|
|
2515
|
-
const testid = asString2(asRecord2(res)["testid"]);
|
|
2516
|
-
const action = asString2(args["action"]) ?? "";
|
|
2517
|
-
const actArgs = asRecord2(args["args"]);
|
|
2518
|
-
if (testid !== void 0) {
|
|
2519
|
-
return {
|
|
2520
|
-
tool: IrisTool.ACT,
|
|
2521
|
-
stable: true,
|
|
2522
|
-
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
return {
|
|
2526
|
-
tool: IrisTool.ACT,
|
|
2527
|
-
stable: false,
|
|
2528
|
-
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
2529
|
-
};
|
|
2530
|
-
}
|
|
2531
|
-
function compileSequenceStep(args, res) {
|
|
2532
|
-
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
2533
|
-
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
2534
|
-
let stable = inputSteps.length > 0;
|
|
2535
|
-
const subSteps = inputSteps.map((raw, i) => {
|
|
2536
|
-
const step = asRecord2(raw);
|
|
2537
|
-
const action = asString2(step["action"]) ?? "";
|
|
2538
|
-
const stepArgs = asRecord2(step["args"]);
|
|
2539
|
-
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
2540
|
-
if (testid !== void 0) {
|
|
2541
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
2542
|
-
}
|
|
2543
|
-
stable = false;
|
|
2544
|
-
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
2545
|
-
});
|
|
2546
|
-
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
2547
|
-
}
|
|
2548
|
-
async function resolveRef(session, step) {
|
|
2549
|
-
const by = asString2(step.by);
|
|
2550
|
-
const value = asString2(step.value);
|
|
2551
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2552
|
-
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
2553
|
-
if (!result.ok)
|
|
2554
|
-
throw new Error(result.error ?? "query failed");
|
|
2555
|
-
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
2556
|
-
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
2557
|
-
if (ref2 === void 0)
|
|
2558
|
-
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
2559
|
-
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
2560
|
-
}
|
|
2561
|
-
const ref = asString2(step.ref);
|
|
2562
|
-
if (ref === void 0 || ref.length === 0)
|
|
2563
|
-
throw new Error("step has no testid or ref to resolve");
|
|
2564
|
-
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
2565
|
-
}
|
|
2566
|
-
async function replayProgram(session, program) {
|
|
2567
|
-
const results = [];
|
|
2568
|
-
for (const step of program.steps) {
|
|
2569
|
-
try {
|
|
2570
|
-
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2571
|
-
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2572
|
-
const notes = [];
|
|
2573
|
-
const liveSteps = [];
|
|
2574
|
-
for (const raw of subs) {
|
|
2575
|
-
const sub = asRecord2(raw);
|
|
2576
|
-
const { ref, note } = await resolveRef(session, sub);
|
|
2577
|
-
if (note !== void 0)
|
|
2578
|
-
notes.push(note);
|
|
2579
|
-
liveSteps.push({
|
|
2580
|
-
ref,
|
|
2581
|
-
action: asString2(sub["action"]) ?? "",
|
|
2582
|
-
args: asRecord2(sub["args"])
|
|
2583
|
-
});
|
|
2584
|
-
}
|
|
2585
|
-
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
2586
|
-
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
2587
|
-
if (!r.ok)
|
|
2588
|
-
break;
|
|
2589
|
-
} else {
|
|
2590
|
-
const { ref, note } = await resolveRef(session, step.args);
|
|
2591
|
-
const r = await session.command(IrisCommand.ACT, {
|
|
2592
|
-
ref,
|
|
2593
|
-
action: asString2(step.args["action"]) ?? "",
|
|
2594
|
-
args: asRecord2(step.args["args"])
|
|
2595
|
-
});
|
|
2596
|
-
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
2597
|
-
if (!r.ok)
|
|
2598
|
-
break;
|
|
2599
|
-
}
|
|
2600
|
-
} catch (e) {
|
|
2601
|
-
results.push({
|
|
2602
|
-
tool: step.tool,
|
|
2603
|
-
ok: false,
|
|
2604
|
-
error: e instanceof Error ? e.message : String(e)
|
|
2605
|
-
});
|
|
2606
|
-
break;
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
return results;
|
|
2610
|
-
}
|
|
2611
|
-
function buildResult(tool, ok, error, notes) {
|
|
2612
|
-
const base = { tool, ok };
|
|
2613
|
-
if (!ok)
|
|
2614
|
-
base.error = error ?? "command failed";
|
|
2615
|
-
if (notes.length > 0)
|
|
2616
|
-
base.note = notes.join("; ");
|
|
2617
|
-
return base;
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
3101
|
// ../server/dist/events/event-filters.js
|
|
2621
|
-
function
|
|
3102
|
+
function asString4(value) {
|
|
2622
3103
|
return typeof value === "string" ? value : void 0;
|
|
2623
3104
|
}
|
|
2624
|
-
function
|
|
3105
|
+
function asNumber2(value) {
|
|
2625
3106
|
return typeof value === "number" ? value : void 0;
|
|
2626
3107
|
}
|
|
2627
3108
|
function matchNet(e, method, urlContains, status) {
|
|
2628
3109
|
const d = e.data;
|
|
2629
|
-
if (method !== void 0 &&
|
|
3110
|
+
if (method !== void 0 && asString4(d["method"])?.toUpperCase() !== method.toUpperCase()) {
|
|
2630
3111
|
return false;
|
|
2631
3112
|
}
|
|
2632
|
-
if (urlContains !== void 0 && !(
|
|
3113
|
+
if (urlContains !== void 0 && !(asString4(d["url"]) ?? "").includes(urlContains)) {
|
|
2633
3114
|
return false;
|
|
2634
3115
|
}
|
|
2635
|
-
if (status !== void 0 &&
|
|
3116
|
+
if (status !== void 0 && asNumber2(d["status"]) !== status)
|
|
2636
3117
|
return false;
|
|
2637
3118
|
return true;
|
|
2638
3119
|
}
|
|
@@ -2649,8 +3130,8 @@ function matchConsole(e, level) {
|
|
|
2649
3130
|
var HINT_SAMPLE_MAX = 5;
|
|
2650
3131
|
function netEmptyHint(allNet) {
|
|
2651
3132
|
const present = allNet.slice(-HINT_SAMPLE_MAX).reverse().map((e) => {
|
|
2652
|
-
const status =
|
|
2653
|
-
const base = { method:
|
|
3133
|
+
const status = asNumber2(e.data["status"]);
|
|
3134
|
+
const base = { method: asString4(e.data["method"]) ?? "", url: asString4(e.data["url"]) ?? "" };
|
|
2654
3135
|
return status === void 0 ? base : { ...base, status };
|
|
2655
3136
|
});
|
|
2656
3137
|
return { totalInWindow: allNet.length, present };
|
|
@@ -2681,6 +3162,8 @@ function refuseIfThrottled(session, refuse) {
|
|
|
2681
3162
|
}
|
|
2682
3163
|
|
|
2683
3164
|
// ../server/dist/session/output-budget.js
|
|
3165
|
+
var LARGE_TIMELINE_EVENTS = 80;
|
|
3166
|
+
var LARGE_TIMELINE_BYTES = 8e3;
|
|
2684
3167
|
function applyEventBudget(events, maxEvents) {
|
|
2685
3168
|
if (maxEvents === void 0 || maxEvents < 0 || events.length <= maxEvents) {
|
|
2686
3169
|
return { events, droppedOldest: 0 };
|
|
@@ -2691,8 +3174,92 @@ function applyEventBudget(events, maxEvents) {
|
|
|
2691
3174
|
};
|
|
2692
3175
|
}
|
|
2693
3176
|
function costHint(payload, events, droppedOldest = 0) {
|
|
2694
|
-
const
|
|
2695
|
-
|
|
3177
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3178
|
+
const bytes = json.length;
|
|
3179
|
+
const base = droppedOldest > 0 ? { events, bytes, droppedOldest } : { events, bytes };
|
|
3180
|
+
if (events >= LARGE_TIMELINE_EVENTS || bytes >= LARGE_TIMELINE_BYTES) {
|
|
3181
|
+
base.recommendation = `large timeline (${String(events)} events, ~${String(estimateTokens(json))} tokens) \u2014 pass filters:[...] (e.g. ["signal","net"]) or max_events to scope your next call and cut tokens`;
|
|
3182
|
+
}
|
|
3183
|
+
return base;
|
|
3184
|
+
}
|
|
3185
|
+
var CHARS_PER_TOKEN = 4;
|
|
3186
|
+
function estimateTokens(text) {
|
|
3187
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3188
|
+
}
|
|
3189
|
+
function sizeCost(payload) {
|
|
3190
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3191
|
+
return { bytes: json.length, tokens: estimateTokens(json) };
|
|
3192
|
+
}
|
|
3193
|
+
function withSizeCost(result) {
|
|
3194
|
+
if (typeof result !== "object" || result === null)
|
|
3195
|
+
return result;
|
|
3196
|
+
return { ...result, cost: sizeCost(result) };
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// ../server/dist/tools/snapshot-delta.js
|
|
3200
|
+
var SnapshotDeltaMode = {
|
|
3201
|
+
FULL: "full",
|
|
3202
|
+
DELTA: "delta",
|
|
3203
|
+
UNCHANGED: "unchanged"
|
|
3204
|
+
};
|
|
3205
|
+
function snapshotDelta(prevTree, nextTree) {
|
|
3206
|
+
if (prevTree === void 0)
|
|
3207
|
+
return { mode: SnapshotDeltaMode.FULL };
|
|
3208
|
+
const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
|
|
3209
|
+
if (added.length === 0 && removed.length === 0)
|
|
3210
|
+
return { mode: SnapshotDeltaMode.UNCHANGED };
|
|
3211
|
+
return {
|
|
3212
|
+
mode: SnapshotDeltaMode.DELTA,
|
|
3213
|
+
delta: { added, removed, addedCount: added.length, removedCount: removed.length }
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
var DEFAULT_MAX_ENTRIES = 50;
|
|
3217
|
+
var SnapshotCache = class {
|
|
3218
|
+
#map = /* @__PURE__ */ new Map();
|
|
3219
|
+
#max;
|
|
3220
|
+
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
3221
|
+
this.#max = max;
|
|
3222
|
+
}
|
|
3223
|
+
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
3224
|
+
recall(key, route) {
|
|
3225
|
+
const entry = this.#map.get(key);
|
|
3226
|
+
return entry !== void 0 && entry.route === route ? entry.tree : void 0;
|
|
3227
|
+
}
|
|
3228
|
+
remember(key, route, tree) {
|
|
3229
|
+
if (this.#map.size >= this.#max && !this.#map.has(key)) {
|
|
3230
|
+
const oldest = this.#map.keys().next().value;
|
|
3231
|
+
if (oldest !== void 0)
|
|
3232
|
+
this.#map.delete(oldest);
|
|
3233
|
+
}
|
|
3234
|
+
this.#map.set(key, { route, tree });
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
function snapshotCacheKey(sessionId, scope, mode) {
|
|
3238
|
+
return `${sessionId}\0${scope}\0${mode}`;
|
|
3239
|
+
}
|
|
3240
|
+
function applySnapshotDelta(raw, opts, cache) {
|
|
3241
|
+
if (typeof raw !== "object" || raw === null)
|
|
3242
|
+
return raw;
|
|
3243
|
+
const r = raw;
|
|
3244
|
+
if (typeof r["tree"] !== "string")
|
|
3245
|
+
return raw;
|
|
3246
|
+
const tree = r["tree"];
|
|
3247
|
+
const status = typeof r["status"] === "object" && r["status"] !== null ? r["status"] : {};
|
|
3248
|
+
const route = typeof status["route"] === "string" ? status["route"] : "";
|
|
3249
|
+
const key = snapshotCacheKey(opts.sessionId, opts.scope, opts.mode);
|
|
3250
|
+
if (!opts.diff) {
|
|
3251
|
+
cache.remember(key, route, tree);
|
|
3252
|
+
return raw;
|
|
3253
|
+
}
|
|
3254
|
+
const prev = cache.recall(key, route);
|
|
3255
|
+
cache.remember(key, route, tree);
|
|
3256
|
+
const decision = snapshotDelta(prev, tree);
|
|
3257
|
+
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
3258
|
+
return raw;
|
|
3259
|
+
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
3260
|
+
return { mode: SnapshotDeltaMode.UNCHANGED, status: r["status"] };
|
|
3261
|
+
}
|
|
3262
|
+
return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r["status"] };
|
|
2696
3263
|
}
|
|
2697
3264
|
|
|
2698
3265
|
// ../server/dist/session/state-select.js
|
|
@@ -2743,25 +3310,55 @@ function capDepth(value, maxDepth) {
|
|
|
2743
3310
|
return value;
|
|
2744
3311
|
}
|
|
2745
3312
|
|
|
2746
|
-
// ../server/dist/tools/
|
|
2747
|
-
function
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
3313
|
+
// ../server/dist/tools/query-paginate.js
|
|
3314
|
+
function paginateQueryResult(result, limit, countOnly) {
|
|
3315
|
+
if (typeof result !== "object" || result === null)
|
|
3316
|
+
return result;
|
|
3317
|
+
const record = result;
|
|
3318
|
+
const elements = record["elements"];
|
|
3319
|
+
if (!Array.isArray(elements))
|
|
3320
|
+
return result;
|
|
3321
|
+
const total = elements.length;
|
|
3322
|
+
if (countOnly) {
|
|
3323
|
+
const { elements: _dropped, ...rest } = record;
|
|
3324
|
+
return { ...rest, count: total };
|
|
2754
3325
|
}
|
|
2755
|
-
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
return
|
|
3326
|
+
if (limit !== void 0 && limit >= 0 && total > limit) {
|
|
3327
|
+
return { ...record, elements: elements.slice(0, limit), total, truncated: true };
|
|
3328
|
+
}
|
|
3329
|
+
return result;
|
|
2759
3330
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
3331
|
+
|
|
3332
|
+
// ../server/dist/tools/assert-grade.js
|
|
3333
|
+
var PRESENCE_ONLY_ADVICE = "This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion \u2014 or allOf it with one \u2014 so green means the feature actually worked.";
|
|
3334
|
+
function walk(predicate) {
|
|
3335
|
+
switch (predicate.kind) {
|
|
3336
|
+
case "signal":
|
|
3337
|
+
case "net":
|
|
3338
|
+
return { consequence: true, presence: false };
|
|
3339
|
+
case "element":
|
|
3340
|
+
case "text":
|
|
3341
|
+
return { consequence: false, presence: true };
|
|
3342
|
+
case "route":
|
|
3343
|
+
case "console":
|
|
3344
|
+
case "animation":
|
|
3345
|
+
case "settled":
|
|
3346
|
+
return { consequence: false, presence: false };
|
|
3347
|
+
case "allOf":
|
|
3348
|
+
case "anyOf": {
|
|
3349
|
+
const subs = predicate.predicates.map(walk);
|
|
3350
|
+
return {
|
|
3351
|
+
consequence: subs.some((s) => s.consequence),
|
|
3352
|
+
presence: subs.some((s) => s.presence)
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
case "not":
|
|
3356
|
+
return walk(predicate.predicate);
|
|
3357
|
+
}
|
|
2762
3358
|
}
|
|
2763
|
-
function
|
|
2764
|
-
|
|
3359
|
+
function isPresenceOnlyAssertion(predicate) {
|
|
3360
|
+
const kinds = walk(predicate);
|
|
3361
|
+
return kinds.presence && !kinds.consequence;
|
|
2765
3362
|
}
|
|
2766
3363
|
|
|
2767
3364
|
// ../server/dist/tools/contract-tools.js
|
|
@@ -2795,7 +3392,7 @@ var CONTRACT_TOOLS = [
|
|
|
2795
3392
|
throw new Error(r.reason === ContractReadError.MISSING ? "no .iris/contract.json on disk \u2014 run iris_contract_save first (or omit fromDisk to read the live session)" : ".iris/contract.json is malformed \u2014 fix or regenerate it with iris_contract_save");
|
|
2796
3393
|
return { ...r.capabilities, source: "disk", generatedAt: r.generatedAt };
|
|
2797
3394
|
}
|
|
2798
|
-
const caps = await commandOrThrow(deps,
|
|
3395
|
+
const caps = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2799
3396
|
return { ...caps, source: "live" };
|
|
2800
3397
|
}
|
|
2801
3398
|
},
|
|
@@ -2810,7 +3407,7 @@ var CONTRACT_TOOLS = [
|
|
|
2810
3407
|
signalCount: z4.number()
|
|
2811
3408
|
},
|
|
2812
3409
|
handler: async (deps, args) => {
|
|
2813
|
-
const res = await commandOrThrow(deps,
|
|
3410
|
+
const res = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2814
3411
|
const caps = CapabilitiesSchema.parse(res);
|
|
2815
3412
|
await writeContract(deps.fs, deps.irisRoot, caps, deps.now);
|
|
2816
3413
|
return {
|
|
@@ -2823,10 +3420,345 @@ var CONTRACT_TOOLS = [
|
|
|
2823
3420
|
}
|
|
2824
3421
|
];
|
|
2825
3422
|
|
|
3423
|
+
// ../server/dist/domain/domain-tools.js
|
|
3424
|
+
import { z as z5 } from "zod";
|
|
3425
|
+
|
|
3426
|
+
// ../server/dist/flows/flow-classify.js
|
|
3427
|
+
var FlowAssertionGrade = {
|
|
3428
|
+
/** At least one step (or the success end-condition) asserts a signal/network consequence. */
|
|
3429
|
+
ASSERTED: "asserted",
|
|
3430
|
+
/** Only element-presence checks — a healed-but-wrong locator could still pass. */
|
|
3431
|
+
PRESENCE_ONLY: "presence-only",
|
|
3432
|
+
/** Performs actions but asserts nothing observable — passes even if the feature is broken. */
|
|
3433
|
+
ASSERTION_FREE: "assertion-free"
|
|
3434
|
+
};
|
|
3435
|
+
var ASSERTION_FREE_WARNING = "This flow performs actions but asserts no observable consequence \u2014 it will pass even if the feature is broken. Add a consequence assertion with iris_annotate (assert-signal / assert-net) or a success-state.";
|
|
3436
|
+
var PRESENCE_ONLY_WARNING = "This flow only checks element presence, not an observable consequence (signal/network). A locator healed to the wrong element can still pass it. Add a consequence assertion (assert-signal / assert-net / success-state).";
|
|
3437
|
+
function expectIsConsequence(e) {
|
|
3438
|
+
return e !== void 0 && (e.signal !== void 0 || e.net !== void 0);
|
|
3439
|
+
}
|
|
3440
|
+
function expectIsWeak(e) {
|
|
3441
|
+
return e !== void 0 && e.element !== void 0 && e.signal === void 0 && e.net === void 0;
|
|
3442
|
+
}
|
|
3443
|
+
function flattenSteps(steps) {
|
|
3444
|
+
const out = [];
|
|
3445
|
+
for (const s of steps) {
|
|
3446
|
+
out.push(s);
|
|
3447
|
+
if (s.steps !== void 0)
|
|
3448
|
+
out.push(...flattenSteps(s.steps));
|
|
3449
|
+
}
|
|
3450
|
+
return out;
|
|
3451
|
+
}
|
|
3452
|
+
function classifyFlowAssertions(flow) {
|
|
3453
|
+
const all = flattenSteps(flow.steps);
|
|
3454
|
+
let consequenceSteps = 0;
|
|
3455
|
+
let weakSteps = 0;
|
|
3456
|
+
for (const s of all) {
|
|
3457
|
+
if (expectIsConsequence(s.expect))
|
|
3458
|
+
consequenceSteps++;
|
|
3459
|
+
else if (expectIsWeak(s.expect))
|
|
3460
|
+
weakSteps++;
|
|
3461
|
+
}
|
|
3462
|
+
const successIsConsequence = expectIsConsequence(flow.success);
|
|
3463
|
+
const successIsWeak = expectIsWeak(flow.success);
|
|
3464
|
+
const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence;
|
|
3465
|
+
const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
|
|
3466
|
+
let grade;
|
|
3467
|
+
let warning;
|
|
3468
|
+
if (hasConsequenceAssertion) {
|
|
3469
|
+
grade = FlowAssertionGrade.ASSERTED;
|
|
3470
|
+
} else if (hasAnyAssertion) {
|
|
3471
|
+
grade = FlowAssertionGrade.PRESENCE_ONLY;
|
|
3472
|
+
warning = PRESENCE_ONLY_WARNING;
|
|
3473
|
+
} else {
|
|
3474
|
+
grade = FlowAssertionGrade.ASSERTION_FREE;
|
|
3475
|
+
warning = ASSERTION_FREE_WARNING;
|
|
3476
|
+
}
|
|
3477
|
+
return {
|
|
3478
|
+
grade,
|
|
3479
|
+
hasConsequenceAssertion,
|
|
3480
|
+
totalSteps: all.length,
|
|
3481
|
+
consequenceSteps,
|
|
3482
|
+
weakSteps,
|
|
3483
|
+
successIsConsequence,
|
|
3484
|
+
...warning !== void 0 ? { warning } : {}
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
// ../server/dist/flows/flow-success.js
|
|
3489
|
+
function dynamicTestids(flow) {
|
|
3490
|
+
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3491
|
+
}
|
|
3492
|
+
function successLabel(success) {
|
|
3493
|
+
if (success.signal !== void 0)
|
|
3494
|
+
return success.signal;
|
|
3495
|
+
if (success.net !== void 0)
|
|
3496
|
+
return success.net.urlContains ?? success.net.method ?? "net";
|
|
3497
|
+
return success.element?.testid ?? success.element?.name ?? success.element?.role ?? "success";
|
|
3498
|
+
}
|
|
3499
|
+
function successToPredicate(success, dynamic) {
|
|
3500
|
+
const parts = [];
|
|
3501
|
+
if (success.signal !== void 0) {
|
|
3502
|
+
parts.push(success.signalData !== void 0 ? { kind: "signal", name: success.signal, dataMatches: success.signalData } : { kind: "signal", name: success.signal });
|
|
3503
|
+
}
|
|
3504
|
+
if (success.net !== void 0) {
|
|
3505
|
+
const net = { kind: "net" };
|
|
3506
|
+
if (success.net.method !== void 0)
|
|
3507
|
+
net.method = success.net.method;
|
|
3508
|
+
if (success.net.urlContains !== void 0)
|
|
3509
|
+
net.urlContains = success.net.urlContains;
|
|
3510
|
+
if (success.net.status !== void 0)
|
|
3511
|
+
net.status = success.net.status;
|
|
3512
|
+
parts.push(net);
|
|
3513
|
+
}
|
|
3514
|
+
const element = success.element;
|
|
3515
|
+
if (element !== void 0) {
|
|
3516
|
+
const testid = element.testid;
|
|
3517
|
+
if (testid === void 0 || !dynamic.has(testid)) {
|
|
3518
|
+
const query = {};
|
|
3519
|
+
if (testid !== void 0)
|
|
3520
|
+
query["testid"] = testid;
|
|
3521
|
+
if (element.role !== void 0)
|
|
3522
|
+
query["role"] = element.role;
|
|
3523
|
+
if (element.name !== void 0)
|
|
3524
|
+
query["name"] = element.name;
|
|
3525
|
+
if (Object.keys(query).length > 0)
|
|
3526
|
+
parts.push({ kind: "element", query });
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const [first] = parts;
|
|
3530
|
+
if (parts.length === 0)
|
|
3531
|
+
return void 0;
|
|
3532
|
+
if (parts.length === 1 && first !== void 0)
|
|
3533
|
+
return first;
|
|
3534
|
+
return { kind: "allOf", predicates: parts };
|
|
3535
|
+
}
|
|
3536
|
+
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
3537
|
+
if (success === void 0)
|
|
3538
|
+
return { pass: true };
|
|
3539
|
+
const predicate = successToPredicate(success, dynamic);
|
|
3540
|
+
if (predicate === void 0)
|
|
3541
|
+
return { pass: true };
|
|
3542
|
+
return waitForSignal(session, predicate, timeoutMs, since);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// ../server/dist/domain/flow-risk.js
|
|
3546
|
+
var RiskLevel = {
|
|
3547
|
+
HIGH: "high",
|
|
3548
|
+
MEDIUM: "medium",
|
|
3549
|
+
LOW: "low",
|
|
3550
|
+
UNKNOWN: "unknown"
|
|
3551
|
+
};
|
|
3552
|
+
var RANK = {
|
|
3553
|
+
[RiskLevel.HIGH]: 3,
|
|
3554
|
+
[RiskLevel.MEDIUM]: 2,
|
|
3555
|
+
[RiskLevel.UNKNOWN]: 1,
|
|
3556
|
+
[RiskLevel.LOW]: 0
|
|
3557
|
+
};
|
|
3558
|
+
function latestRun(name, runs) {
|
|
3559
|
+
let best;
|
|
3560
|
+
for (const run of runs) {
|
|
3561
|
+
if (run.name === name && (best === void 0 || run.at > best.at))
|
|
3562
|
+
best = run;
|
|
3563
|
+
}
|
|
3564
|
+
return best;
|
|
3565
|
+
}
|
|
3566
|
+
function runRisk(run) {
|
|
3567
|
+
if (run === void 0)
|
|
3568
|
+
return { level: RiskLevel.UNKNOWN, reason: "never run" };
|
|
3569
|
+
if (run.status === RunStatus.ERROR || run.status === RunStatus.FAIL) {
|
|
3570
|
+
return { level: RiskLevel.HIGH, reason: "last run failed" };
|
|
3571
|
+
}
|
|
3572
|
+
if (run.status === RunStatus.DRIFT)
|
|
3573
|
+
return { level: RiskLevel.HIGH, reason: "last run drifted" };
|
|
3574
|
+
const errors = (run.evidence?.consoleErrors ?? 0) + (run.evidence?.networkErrors ?? 0);
|
|
3575
|
+
if (errors > 0) {
|
|
3576
|
+
return {
|
|
3577
|
+
level: RiskLevel.MEDIUM,
|
|
3578
|
+
reason: `last run passed but logged ${String(errors)} error(s)`
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
return { level: RiskLevel.LOW, reason: "last run passed clean" };
|
|
3582
|
+
}
|
|
3583
|
+
function gradeRisk(grade) {
|
|
3584
|
+
if (grade === FlowAssertionGrade.ASSERTION_FREE) {
|
|
3585
|
+
return {
|
|
3586
|
+
level: RiskLevel.MEDIUM,
|
|
3587
|
+
reason: "asserts no consequence \u2014 a green run proves little"
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
if (grade === FlowAssertionGrade.PRESENCE_ONLY) {
|
|
3591
|
+
return { level: RiskLevel.LOW, reason: "presence-only assertion" };
|
|
3592
|
+
}
|
|
3593
|
+
return { level: RiskLevel.LOW, reason: "asserts a consequence" };
|
|
3594
|
+
}
|
|
3595
|
+
function flowRisk(grade, run) {
|
|
3596
|
+
const r = runRisk(run);
|
|
3597
|
+
const g = gradeRisk(grade);
|
|
3598
|
+
const top = RANK[r.level] >= RANK[g.level] ? r : g;
|
|
3599
|
+
return run === void 0 ? { level: top.level, reason: top.reason } : { level: top.level, reason: top.reason, lastStatus: run.status };
|
|
3600
|
+
}
|
|
3601
|
+
function rankByRisk(entries) {
|
|
3602
|
+
return [...entries].sort((a, b) => RANK[b.risk.level] - RANK[a.risk.level] || a.name.localeCompare(b.name)).map((e) => e.name);
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// ../server/dist/domain/domain-model.js
|
|
3606
|
+
function flatten(steps) {
|
|
3607
|
+
const out = [];
|
|
3608
|
+
for (const s of steps) {
|
|
3609
|
+
out.push(s);
|
|
3610
|
+
if (s.steps !== void 0)
|
|
3611
|
+
out.push(...flatten(s.steps));
|
|
3612
|
+
}
|
|
3613
|
+
return out;
|
|
3614
|
+
}
|
|
3615
|
+
function flowSignals(flow) {
|
|
3616
|
+
const set = /* @__PURE__ */ new Set();
|
|
3617
|
+
for (const step of flatten(flow.steps)) {
|
|
3618
|
+
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
3619
|
+
set.add(step.anchor.name);
|
|
3620
|
+
if (step.expect?.signal !== void 0)
|
|
3621
|
+
set.add(step.expect.signal);
|
|
3622
|
+
}
|
|
3623
|
+
if (flow.success?.signal !== void 0)
|
|
3624
|
+
set.add(flow.success.signal);
|
|
3625
|
+
return [...set];
|
|
3626
|
+
}
|
|
3627
|
+
function flowTestids(flow) {
|
|
3628
|
+
const set = /* @__PURE__ */ new Set();
|
|
3629
|
+
for (const step of flatten(flow.steps)) {
|
|
3630
|
+
if (step.anchor.kind === AnchorKind.TESTID)
|
|
3631
|
+
set.add(step.anchor.value);
|
|
3632
|
+
if (step.expect?.element?.testid !== void 0)
|
|
3633
|
+
set.add(step.expect.element.testid);
|
|
3634
|
+
}
|
|
3635
|
+
if (flow.success?.element?.testid !== void 0)
|
|
3636
|
+
set.add(flow.success.element.testid);
|
|
3637
|
+
return [...set];
|
|
3638
|
+
}
|
|
3639
|
+
var EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
3640
|
+
function buildDomainModel(flows, contract, runs = []) {
|
|
3641
|
+
const caps = contract ?? EMPTY_CONTRACT;
|
|
3642
|
+
const hasHistory = runs.length > 0;
|
|
3643
|
+
const flowSummaries = flows.map((flow) => {
|
|
3644
|
+
const c = classifyFlowAssertions(flow);
|
|
3645
|
+
const summary = {
|
|
3646
|
+
name: flow.name,
|
|
3647
|
+
steps: c.totalSteps,
|
|
3648
|
+
grade: c.grade,
|
|
3649
|
+
asserts: c.hasConsequenceAssertion,
|
|
3650
|
+
signals: flowSignals(flow),
|
|
3651
|
+
testids: flowTestids(flow)
|
|
3652
|
+
};
|
|
3653
|
+
if (flow.success !== void 0)
|
|
3654
|
+
summary.mustHold = successLabel(flow.success);
|
|
3655
|
+
if (c.warning !== void 0)
|
|
3656
|
+
summary.warning = c.warning;
|
|
3657
|
+
if (hasHistory)
|
|
3658
|
+
summary.risk = flowRisk(c.grade, latestRun(flow.name, runs));
|
|
3659
|
+
return summary;
|
|
3660
|
+
});
|
|
3661
|
+
const testedSignals = new Set(flowSummaries.flatMap((f) => f.signals));
|
|
3662
|
+
const testedTestids = new Set(flowSummaries.flatMap((f) => f.testids));
|
|
3663
|
+
const coverage = {
|
|
3664
|
+
asserted: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTED).length,
|
|
3665
|
+
presenceOnly: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.PRESENCE_ONLY).length,
|
|
3666
|
+
assertionFree: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTION_FREE).length
|
|
3667
|
+
};
|
|
3668
|
+
const gaps = {
|
|
3669
|
+
unassertedFlows: flowSummaries.filter((f) => !f.asserts).map((f) => f.name),
|
|
3670
|
+
declaredUntestedSignals: caps.signals.filter((s) => !testedSignals.has(s)),
|
|
3671
|
+
declaredUntestedTestids: caps.testids.filter((t) => !testedTestids.has(t))
|
|
3672
|
+
};
|
|
3673
|
+
const riskRanked = hasHistory ? rankByRisk(flowSummaries.filter((f) => f.risk !== void 0).map((f) => ({ name: f.name, risk: f.risk }))) : [];
|
|
3674
|
+
const top = riskRanked[0];
|
|
3675
|
+
const topFlow = top === void 0 ? void 0 : flowSummaries.find((f) => f.name === top);
|
|
3676
|
+
const topRisk = topFlow?.risk !== void 0 && (topFlow.risk.level === RiskLevel.HIGH || topFlow.risk.level === RiskLevel.MEDIUM) ? { name: topFlow.name, reason: topFlow.risk.reason } : void 0;
|
|
3677
|
+
return {
|
|
3678
|
+
flowCount: flows.length,
|
|
3679
|
+
flows: flowSummaries,
|
|
3680
|
+
declared: { testids: caps.testids.length, signals: caps.signals, stores: caps.stores },
|
|
3681
|
+
coverage,
|
|
3682
|
+
gaps,
|
|
3683
|
+
riskRanked,
|
|
3684
|
+
summary: buildSummary(flows.length, coverage, gaps, topRisk)
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
3688
|
+
if (flowCount === 0) {
|
|
3689
|
+
return "No saved flows yet \u2014 record the critical journeys (iris_record_start) so the agent learns the app.";
|
|
3690
|
+
}
|
|
3691
|
+
const parts = [
|
|
3692
|
+
`${String(flowCount)} flow${flowCount === 1 ? "" : "s"}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`
|
|
3693
|
+
];
|
|
3694
|
+
if (topRisk !== void 0) {
|
|
3695
|
+
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
3696
|
+
}
|
|
3697
|
+
if (gaps.declaredUntestedSignals.length > 0) {
|
|
3698
|
+
parts.push(`${String(gaps.declaredUntestedSignals.length)} declared signal(s) no flow asserts (${gaps.declaredUntestedSignals.join(", ")})`);
|
|
3699
|
+
}
|
|
3700
|
+
if (gaps.unassertedFlows.length > 0) {
|
|
3701
|
+
parts.push(`${String(gaps.unassertedFlows.length)} flow(s) assert no consequence`);
|
|
3702
|
+
}
|
|
3703
|
+
return parts.join(". ") + ".";
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// ../server/dist/domain/domain-tools.js
|
|
3707
|
+
var DOMAIN_TOOLS = [
|
|
3708
|
+
{
|
|
3709
|
+
name: IrisTool.DOMAIN,
|
|
3710
|
+
description: "Read the app domain model BEFORE testing: every saved flow with its assertion grade, the consequence that MUST hold for it (mustHold = what it actually tests), the anchors/signals it exercises, plus GAPS \u2014 declared signals/testids that NO flow asserts (untested intent), and flows that assert no observable consequence. Use this to decide what to test and where the real risk is, instead of crawling the whole app. Reads .iris/flows/ + .iris/contract.json (no browser needed).",
|
|
3711
|
+
inputSchema: {},
|
|
3712
|
+
outputSchema: {
|
|
3713
|
+
flowCount: z5.number(),
|
|
3714
|
+
flows: z5.array(z5.object({
|
|
3715
|
+
name: z5.string(),
|
|
3716
|
+
steps: z5.number(),
|
|
3717
|
+
grade: z5.string(),
|
|
3718
|
+
asserts: z5.boolean(),
|
|
3719
|
+
mustHold: z5.string().optional().describe("The success consequence that must hold for this flow (what it actually tests)."),
|
|
3720
|
+
warning: z5.string().optional(),
|
|
3721
|
+
signals: z5.array(z5.string()),
|
|
3722
|
+
testids: z5.array(z5.string())
|
|
3723
|
+
})),
|
|
3724
|
+
declared: z5.object({
|
|
3725
|
+
testids: z5.number(),
|
|
3726
|
+
signals: z5.array(z5.string()),
|
|
3727
|
+
stores: z5.array(z5.string())
|
|
3728
|
+
}),
|
|
3729
|
+
coverage: z5.object({
|
|
3730
|
+
asserted: z5.number(),
|
|
3731
|
+
presenceOnly: z5.number(),
|
|
3732
|
+
assertionFree: z5.number()
|
|
3733
|
+
}),
|
|
3734
|
+
gaps: z5.object({
|
|
3735
|
+
unassertedFlows: z5.array(z5.string()),
|
|
3736
|
+
declaredUntestedSignals: z5.array(z5.string()),
|
|
3737
|
+
declaredUntestedTestids: z5.array(z5.string())
|
|
3738
|
+
}),
|
|
3739
|
+
riskRanked: z5.array(z5.string()).describe("Flow names worst-risk first (run history + assertion quality). Test these first."),
|
|
3740
|
+
summary: z5.string()
|
|
3741
|
+
},
|
|
3742
|
+
handler: async (deps) => {
|
|
3743
|
+
const names = await deps.flows.list();
|
|
3744
|
+
const flows = [];
|
|
3745
|
+
for (const name of names) {
|
|
3746
|
+
const loaded = await deps.flows.load(name);
|
|
3747
|
+
if (loaded.ok)
|
|
3748
|
+
flows.push(loaded.value);
|
|
3749
|
+
}
|
|
3750
|
+
const contract = await readContract(deps.fs, deps.irisRoot);
|
|
3751
|
+
const project = await deps.project.read();
|
|
3752
|
+
const runs = project.ok ? project.file.runs : [];
|
|
3753
|
+
return buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
];
|
|
3757
|
+
|
|
2826
3758
|
// ../server/dist/tools/browser-tools.js
|
|
2827
|
-
import { z as
|
|
3759
|
+
import { z as z6 } from "zod";
|
|
2828
3760
|
var sessionIdShape2 = {
|
|
2829
|
-
sessionId:
|
|
3761
|
+
sessionId: z6.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
2830
3762
|
};
|
|
2831
3763
|
async function commandOrThrow2(deps, sessionId, name, args) {
|
|
2832
3764
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -2840,34 +3772,38 @@ var BROWSER_TOOLS = [
|
|
|
2840
3772
|
name: IrisTool.NAVIGATE,
|
|
2841
3773
|
description: "Navigate the connected browser tab to a URL. The SDK reconnects automatically after the page loads. Use iris_sessions to confirm the new tab is connected before acting.",
|
|
2842
3774
|
inputSchema: {
|
|
2843
|
-
url:
|
|
3775
|
+
url: z6.string().describe("The URL to navigate to."),
|
|
2844
3776
|
...sessionIdShape2
|
|
2845
3777
|
},
|
|
2846
3778
|
outputSchema: {
|
|
2847
|
-
ok:
|
|
2848
|
-
url:
|
|
2849
|
-
reason:
|
|
3779
|
+
ok: z6.boolean(),
|
|
3780
|
+
url: z6.string().optional(),
|
|
3781
|
+
reason: z6.string().optional()
|
|
2850
3782
|
},
|
|
2851
3783
|
handler: async (deps, args) => {
|
|
2852
|
-
const url =
|
|
3784
|
+
const url = asString(args["url"]);
|
|
2853
3785
|
if (url === void 0 || url.length === 0)
|
|
2854
3786
|
return { ok: false, reason: "url required" };
|
|
2855
|
-
await commandOrThrow2(deps,
|
|
2856
|
-
return {
|
|
3787
|
+
const result = await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
3788
|
+
return {
|
|
3789
|
+
ok: result.ok === true,
|
|
3790
|
+
...typeof result.url === "string" ? { url: result.url } : {},
|
|
3791
|
+
...typeof result.reason === "string" ? { reason: result.reason } : {}
|
|
3792
|
+
};
|
|
2857
3793
|
}
|
|
2858
3794
|
},
|
|
2859
3795
|
{
|
|
2860
3796
|
name: IrisTool.REFRESH,
|
|
2861
3797
|
description: "Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.",
|
|
2862
3798
|
inputSchema: {
|
|
2863
|
-
hard:
|
|
3799
|
+
hard: z6.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
2864
3800
|
...sessionIdShape2
|
|
2865
3801
|
},
|
|
2866
3802
|
outputSchema: {
|
|
2867
|
-
ok:
|
|
3803
|
+
ok: z6.boolean()
|
|
2868
3804
|
},
|
|
2869
3805
|
handler: async (deps, args) => {
|
|
2870
|
-
await commandOrThrow2(deps,
|
|
3806
|
+
await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.REFRESH, {
|
|
2871
3807
|
hard: args["hard"] === true
|
|
2872
3808
|
});
|
|
2873
3809
|
return { ok: true };
|
|
@@ -2876,200 +3812,7 @@ var BROWSER_TOOLS = [
|
|
|
2876
3812
|
];
|
|
2877
3813
|
|
|
2878
3814
|
// ../server/dist/flows/flow-tools.js
|
|
2879
|
-
import { z as
|
|
2880
|
-
|
|
2881
|
-
// ../server/dist/flows/flow-replay.js
|
|
2882
|
-
function editDistance(a, b) {
|
|
2883
|
-
const s = a.toLowerCase();
|
|
2884
|
-
const t = b.toLowerCase();
|
|
2885
|
-
const rows = s.length + 1;
|
|
2886
|
-
const cols = t.length + 1;
|
|
2887
|
-
const prev = new Array(cols);
|
|
2888
|
-
const curr = new Array(cols);
|
|
2889
|
-
for (let j = 0; j < cols; j++)
|
|
2890
|
-
prev[j] = j;
|
|
2891
|
-
for (let i = 1; i < rows; i++) {
|
|
2892
|
-
curr[0] = i;
|
|
2893
|
-
for (let j = 1; j < cols; j++) {
|
|
2894
|
-
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
2895
|
-
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
2896
|
-
}
|
|
2897
|
-
for (let j = 0; j < cols; j++)
|
|
2898
|
-
prev[j] = curr[j] ?? 0;
|
|
2899
|
-
}
|
|
2900
|
-
return prev[cols - 1] ?? 0;
|
|
2901
|
-
}
|
|
2902
|
-
function nearestTestid(missing, present) {
|
|
2903
|
-
let best = null;
|
|
2904
|
-
let bestDistance = Number.POSITIVE_INFINITY;
|
|
2905
|
-
for (const candidate of present) {
|
|
2906
|
-
const distance = editDistance(missing, candidate);
|
|
2907
|
-
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
2908
|
-
best = candidate;
|
|
2909
|
-
bestDistance = distance;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
return best;
|
|
2913
|
-
}
|
|
2914
|
-
function readQuery(result) {
|
|
2915
|
-
if (!result.ok)
|
|
2916
|
-
return { refs: [] };
|
|
2917
|
-
const payload = asRecord3(result.result);
|
|
2918
|
-
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
2919
|
-
const refs = elements.map((e) => asString4(asRecord3(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
2920
|
-
const rawHint = payload["hint"];
|
|
2921
|
-
if (typeof rawHint === "object" && rawHint !== null) {
|
|
2922
|
-
const hint = asRecord3(rawHint);
|
|
2923
|
-
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
2924
|
-
return {
|
|
2925
|
-
refs,
|
|
2926
|
-
hint: {
|
|
2927
|
-
route: asString4(hint["route"]) ?? "",
|
|
2928
|
-
presentTestids: present,
|
|
2929
|
-
presentRegions: [],
|
|
2930
|
-
knownEmptyState: hint["knownEmptyState"] === true
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2933
|
-
}
|
|
2934
|
-
return { refs };
|
|
2935
|
-
}
|
|
2936
|
-
function testidDrift(value, hint) {
|
|
2937
|
-
return {
|
|
2938
|
-
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
2939
|
-
reason: `testid "${value}" not found`,
|
|
2940
|
-
anchor: value,
|
|
2941
|
-
nearest: nearestTestid(value, hint?.presentTestids ?? [])
|
|
2942
|
-
};
|
|
2943
|
-
}
|
|
2944
|
-
function anchorLabel(anchor) {
|
|
2945
|
-
if (anchor.kind === AnchorKind.TESTID)
|
|
2946
|
-
return anchor.value;
|
|
2947
|
-
if (anchor.kind === AnchorKind.SIGNAL)
|
|
2948
|
-
return anchor.name;
|
|
2949
|
-
return anchor.name ?? anchor.role;
|
|
2950
|
-
}
|
|
2951
|
-
async function runTestidStep(session, step, index, value, dynamic) {
|
|
2952
|
-
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
2953
|
-
const { refs, hint } = readQuery(queryResult);
|
|
2954
|
-
if (refs.length === 0) {
|
|
2955
|
-
return {
|
|
2956
|
-
step: index,
|
|
2957
|
-
tool: step.tool,
|
|
2958
|
-
anchor: value,
|
|
2959
|
-
ok: false,
|
|
2960
|
-
drift: testidDrift(value, hint)
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
|
-
const ref = refs[0] ?? "";
|
|
2964
|
-
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
2965
|
-
const act = await session.command(IrisCommand.ACT, {
|
|
2966
|
-
ref,
|
|
2967
|
-
action: step.action ?? "",
|
|
2968
|
-
args: step.args ?? {}
|
|
2969
|
-
});
|
|
2970
|
-
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
2971
|
-
if (!act.ok) {
|
|
2972
|
-
result.error = act.error ?? "command failed";
|
|
2973
|
-
if (note !== void 0)
|
|
2974
|
-
result.note = note;
|
|
2975
|
-
return result;
|
|
2976
|
-
}
|
|
2977
|
-
const expectTestid = step.expect?.element?.testid;
|
|
2978
|
-
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
2979
|
-
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
2980
|
-
by: QueryBy.TESTID,
|
|
2981
|
-
value: expectTestid
|
|
2982
|
-
});
|
|
2983
|
-
const expectRefs = readQuery(expectQuery);
|
|
2984
|
-
if (expectRefs.refs.length === 0) {
|
|
2985
|
-
return {
|
|
2986
|
-
step: index,
|
|
2987
|
-
tool: step.tool,
|
|
2988
|
-
anchor: expectTestid,
|
|
2989
|
-
ok: false,
|
|
2990
|
-
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
2991
|
-
};
|
|
2992
|
-
}
|
|
2993
|
-
}
|
|
2994
|
-
if (note !== void 0)
|
|
2995
|
-
result.note = note;
|
|
2996
|
-
return result;
|
|
2997
|
-
}
|
|
2998
|
-
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
2999
|
-
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
3000
|
-
if (verdict.pass)
|
|
3001
|
-
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
3002
|
-
return {
|
|
3003
|
-
step: index,
|
|
3004
|
-
tool: step.tool,
|
|
3005
|
-
anchor: name,
|
|
3006
|
-
ok: false,
|
|
3007
|
-
drift: {
|
|
3008
|
-
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
3009
|
-
reason: `signal "${name}" not observed`,
|
|
3010
|
-
anchor: name,
|
|
3011
|
-
nearest: null
|
|
3012
|
-
}
|
|
3013
|
-
};
|
|
3014
|
-
}
|
|
3015
|
-
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs) {
|
|
3016
|
-
const results = [];
|
|
3017
|
-
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3018
|
-
let index = 0;
|
|
3019
|
-
for (const step of flow.steps) {
|
|
3020
|
-
const label = anchorLabel(step.anchor);
|
|
3021
|
-
let result;
|
|
3022
|
-
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
3023
|
-
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
3024
|
-
} else {
|
|
3025
|
-
result = await runTestidStep(session, step, index, label, dynamic);
|
|
3026
|
-
}
|
|
3027
|
-
results.push(result);
|
|
3028
|
-
if (result.drift !== void 0 || !result.ok)
|
|
3029
|
-
break;
|
|
3030
|
-
index += 1;
|
|
3031
|
-
}
|
|
3032
|
-
return results;
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
// ../server/dist/flows/heal.js
|
|
3036
|
-
function confidenceFor(from, to) {
|
|
3037
|
-
if (from === to)
|
|
3038
|
-
return 1;
|
|
3039
|
-
const span = Math.max(from.length, to.length);
|
|
3040
|
-
if (span === 0)
|
|
3041
|
-
return 1;
|
|
3042
|
-
const raw = 1 - editDistance(from, to) / span;
|
|
3043
|
-
if (raw >= 1)
|
|
3044
|
-
return 1;
|
|
3045
|
-
if (raw <= 0)
|
|
3046
|
-
return Number.EPSILON;
|
|
3047
|
-
return raw;
|
|
3048
|
-
}
|
|
3049
|
-
function proposeRebindWith(drift, step, minConfidence) {
|
|
3050
|
-
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
3051
|
-
return void 0;
|
|
3052
|
-
const to = drift.nearest;
|
|
3053
|
-
if (to === null)
|
|
3054
|
-
return void 0;
|
|
3055
|
-
const confidence = confidenceFor(drift.anchor, to);
|
|
3056
|
-
if (confidence < minConfidence)
|
|
3057
|
-
return void 0;
|
|
3058
|
-
return { step, from: drift.anchor, to, confidence };
|
|
3059
|
-
}
|
|
3060
|
-
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
3061
|
-
const proposals = [];
|
|
3062
|
-
for (const step of steps) {
|
|
3063
|
-
if (step.drift === void 0)
|
|
3064
|
-
continue;
|
|
3065
|
-
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
3066
|
-
if (proposal !== void 0)
|
|
3067
|
-
proposals.push(proposal);
|
|
3068
|
-
}
|
|
3069
|
-
return proposals;
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
// ../server/dist/flows/flow-tools.js
|
|
3815
|
+
import { z as z7 } from "zod";
|
|
3073
3816
|
function latestRecordedFlow(events) {
|
|
3074
3817
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
3075
3818
|
const event = events[i];
|
|
@@ -3115,18 +3858,26 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs) {
|
|
|
3115
3858
|
var FLOW_TOOLS = [
|
|
3116
3859
|
{
|
|
3117
3860
|
name: IrisTool.FLOW_SAVE,
|
|
3118
|
-
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty } or
|
|
3861
|
+
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty, assertions } \u2014 `assertions.grade` is asserted | presence-only | assertion-free: a flow that only acts (or only checks element presence) will pass even if the feature breaks, so when grade is not "asserted" follow assertions.warning and add a consequence assertion via iris_annotate (assert-signal / assert-net / success-state).',
|
|
3119
3862
|
inputSchema: {
|
|
3120
|
-
flowName:
|
|
3863
|
+
flowName: z7.string().describe("Name for the flow file (saved to .iris/flows/<flowName>.json). Use again in iris_flow_load/iris_flow_replay.")
|
|
3121
3864
|
},
|
|
3122
3865
|
outputSchema: {
|
|
3123
|
-
saved:
|
|
3124
|
-
path:
|
|
3125
|
-
stepCount:
|
|
3126
|
-
degraded:
|
|
3866
|
+
saved: z7.boolean(),
|
|
3867
|
+
path: z7.string(),
|
|
3868
|
+
stepCount: z7.number().optional(),
|
|
3869
|
+
degraded: z7.number().optional(),
|
|
3870
|
+
assertions: z7.object({
|
|
3871
|
+
grade: z7.string().describe("asserted | presence-only | assertion-free"),
|
|
3872
|
+
hasConsequenceAssertion: z7.boolean(),
|
|
3873
|
+
totalSteps: z7.number(),
|
|
3874
|
+
consequenceSteps: z7.number(),
|
|
3875
|
+
weakSteps: z7.number(),
|
|
3876
|
+
warning: z7.string().optional()
|
|
3877
|
+
}).optional()
|
|
3127
3878
|
},
|
|
3128
3879
|
handler: (deps, args) => {
|
|
3129
|
-
const name =
|
|
3880
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3130
3881
|
const program = deps.recordings.getCompiled(name);
|
|
3131
3882
|
if (program === void 0) {
|
|
3132
3883
|
return Promise.resolve({
|
|
@@ -3140,10 +3891,12 @@ var FLOW_TOOLS = [
|
|
|
3140
3891
|
dynamic: deps.annotations.dynamic(name),
|
|
3141
3892
|
...success !== void 0 ? { success } : {}
|
|
3142
3893
|
};
|
|
3143
|
-
return deps.flows.save(program, annotations).then((res) => {
|
|
3144
|
-
if (res.ok)
|
|
3145
|
-
|
|
3146
|
-
|
|
3894
|
+
return deps.flows.save(program, annotations).then(async (res) => {
|
|
3895
|
+
if (!res.ok)
|
|
3896
|
+
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3897
|
+
deps.annotations.clear(name);
|
|
3898
|
+
const loaded = await deps.flows.load(res.value.name);
|
|
3899
|
+
return loaded.ok ? { ...res.value, assertions: classifyFlowAssertions(loaded.value) } : res.value;
|
|
3147
3900
|
});
|
|
3148
3901
|
}
|
|
3149
3902
|
},
|
|
@@ -3152,22 +3905,27 @@ var FLOW_TOOLS = [
|
|
|
3152
3905
|
description: "List saved flow names under .iris/flows (a fresh agent learns the demonstrated journeys without a browser).",
|
|
3153
3906
|
inputSchema: {},
|
|
3154
3907
|
outputSchema: {
|
|
3155
|
-
flows:
|
|
3908
|
+
flows: z7.array(z7.object({ name: z7.string(), path: z7.string(), createdAt: z7.number().optional() }))
|
|
3156
3909
|
},
|
|
3157
|
-
|
|
3910
|
+
// Return {name, path} objects to MATCH the declared outputSchema. Returning bare name strings
|
|
3911
|
+
// (the prior bug) made schema-validating MCP clients reject the result ("expected object,
|
|
3912
|
+
// received string") — caught driving the live demo.
|
|
3913
|
+
handler: (deps) => deps.flows.list().then((names) => ({
|
|
3914
|
+
flows: names.map((name) => ({ name, path: flowPath(deps.irisRoot, name) }))
|
|
3915
|
+
}))
|
|
3158
3916
|
},
|
|
3159
3917
|
{
|
|
3160
3918
|
name: IrisTool.FLOW_LOAD,
|
|
3161
3919
|
description: "Read + validate a saved flow by flowName from .iris/flows/<flowName>.json. Returns the FlowFile (version, flowName, createdAt, anchored steps) or a structured { error, code }.",
|
|
3162
3920
|
inputSchema: {
|
|
3163
|
-
flowName:
|
|
3921
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list.")
|
|
3164
3922
|
},
|
|
3165
3923
|
outputSchema: {
|
|
3166
|
-
flowName:
|
|
3167
|
-
steps:
|
|
3168
|
-
createdAt:
|
|
3924
|
+
flowName: z7.string(),
|
|
3925
|
+
steps: z7.array(z7.unknown()),
|
|
3926
|
+
createdAt: z7.number().optional()
|
|
3169
3927
|
},
|
|
3170
|
-
handler: (deps, args) => deps.flows.load(
|
|
3928
|
+
handler: (deps, args) => deps.flows.load(asString(args["flowName"]) ?? "").then((res) => {
|
|
3171
3929
|
if (!res.ok)
|
|
3172
3930
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3173
3931
|
const { name, ...rest } = res.value;
|
|
@@ -3176,20 +3934,21 @@ var FLOW_TOOLS = [
|
|
|
3176
3934
|
},
|
|
3177
3935
|
{
|
|
3178
3936
|
name: IrisTool.FLOW_REPLAY,
|
|
3179
|
-
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] };
|
|
3937
|
+
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action failures are status:error with a structured code (distinct from contract-changed drift).`,
|
|
3180
3938
|
inputSchema: {
|
|
3181
|
-
flowName:
|
|
3182
|
-
|
|
3939
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list."),
|
|
3940
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
3941
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3183
3942
|
},
|
|
3184
3943
|
outputSchema: {
|
|
3185
|
-
status:
|
|
3186
|
-
steps:
|
|
3187
|
-
proposals:
|
|
3188
|
-
error:
|
|
3944
|
+
status: z7.string().describe("ok | drift | error"),
|
|
3945
|
+
steps: z7.array(z7.unknown()),
|
|
3946
|
+
proposals: z7.array(z7.unknown()).optional(),
|
|
3947
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3189
3948
|
},
|
|
3190
3949
|
handler: async (deps, args) => {
|
|
3191
3950
|
const startedAt = deps.now();
|
|
3192
|
-
const name =
|
|
3951
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3193
3952
|
const loaded = await deps.flows.load(name);
|
|
3194
3953
|
if (!loaded.ok) {
|
|
3195
3954
|
await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt);
|
|
@@ -3200,12 +3959,35 @@ var FLOW_TOOLS = [
|
|
|
3200
3959
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3201
3960
|
};
|
|
3202
3961
|
}
|
|
3203
|
-
const session = deps.sessions.resolve(
|
|
3204
|
-
const
|
|
3962
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3963
|
+
const replayFloor = session.elapsed();
|
|
3964
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3965
|
+
const stepsClean = steps.length > 0 && steps.every((s) => s.ok && s.drift === void 0);
|
|
3966
|
+
if (stepsClean && loaded.value.success !== void 0) {
|
|
3967
|
+
const verdict = await assertSuccess(session, loaded.value.success, dynamicTestids(loaded.value), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, replayFloor);
|
|
3968
|
+
const row = {
|
|
3969
|
+
step: steps.length,
|
|
3970
|
+
tool: "success",
|
|
3971
|
+
anchor: successLabel(loaded.value.success),
|
|
3972
|
+
ok: verdict.pass,
|
|
3973
|
+
...verdict.pass ? {} : { error: verdict.failureReason ?? "flow.success not satisfied" }
|
|
3974
|
+
};
|
|
3975
|
+
steps.push(row);
|
|
3976
|
+
}
|
|
3205
3977
|
const driftSteps = steps.filter((s) => s.drift !== void 0).length;
|
|
3206
3978
|
const allOk = steps.every((s) => s.ok);
|
|
3207
|
-
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.
|
|
3979
|
+
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
3208
3980
|
await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt);
|
|
3981
|
+
const failed = steps.find((step) => !step.ok && step.drift === void 0);
|
|
3982
|
+
if (failed !== void 0) {
|
|
3983
|
+
const message = failed.error ?? "flow action failed";
|
|
3984
|
+
return {
|
|
3985
|
+
name,
|
|
3986
|
+
status,
|
|
3987
|
+
steps,
|
|
3988
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3209
3991
|
return { name, status, steps };
|
|
3210
3992
|
}
|
|
3211
3993
|
},
|
|
@@ -3213,20 +3995,20 @@ var FLOW_TOOLS = [
|
|
|
3213
3995
|
name: IrisTool.FLOW_SAVE_RECORDED,
|
|
3214
3996
|
description: "Persist the HUMAN-recorded flow from the live tab. The recorder toolbar compiles the human's real clicks/inputs into a semantically anchored FlowFile in-page and emits it; this tool reads the LATEST recorded-flow from the session and writes it to .iris/flows/<name>.json (no recompilation \u2014 the browser already resolved every anchor). Pass `name` to override the recorded name. Returns { name, stepCount, degraded, empty } or { error, code } (code flow_no_recorded when no recording is present).",
|
|
3215
3997
|
inputSchema: {
|
|
3216
|
-
flowName:
|
|
3998
|
+
flowName: z7.string().optional().describe("Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name."),
|
|
3217
3999
|
...{
|
|
3218
|
-
sessionId:
|
|
4000
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3219
4001
|
}
|
|
3220
4002
|
},
|
|
3221
4003
|
outputSchema: {
|
|
3222
|
-
flowName:
|
|
3223
|
-
stepCount:
|
|
3224
|
-
degraded:
|
|
3225
|
-
error:
|
|
3226
|
-
code:
|
|
4004
|
+
flowName: z7.string().optional(),
|
|
4005
|
+
stepCount: z7.number().optional(),
|
|
4006
|
+
degraded: z7.number().optional(),
|
|
4007
|
+
error: z7.string().optional(),
|
|
4008
|
+
code: z7.string().optional()
|
|
3227
4009
|
},
|
|
3228
4010
|
handler: async (deps, args) => {
|
|
3229
|
-
const session = deps.sessions.resolve(
|
|
4011
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3230
4012
|
const recorded = latestRecordedFlow(session.eventsSince(0));
|
|
3231
4013
|
if (recorded === void 0) {
|
|
3232
4014
|
return {
|
|
@@ -3234,7 +4016,7 @@ var FLOW_TOOLS = [
|
|
|
3234
4016
|
code: RecordedSaveError.NO_RECORDED_FLOW
|
|
3235
4017
|
};
|
|
3236
4018
|
}
|
|
3237
|
-
const override =
|
|
4019
|
+
const override = asString(args["flowName"]);
|
|
3238
4020
|
const flow = override !== void 0 ? { ...recorded.flow, name: override } : recorded.flow;
|
|
3239
4021
|
const res = await deps.flows.saveFlow(flow);
|
|
3240
4022
|
if (!res.ok)
|
|
@@ -3245,35 +4027,38 @@ var FLOW_TOOLS = [
|
|
|
3245
4027
|
},
|
|
3246
4028
|
{
|
|
3247
4029
|
name: IrisTool.FLOW_HEAL,
|
|
3248
|
-
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
4030
|
+
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. Before writing, apply re-replays the healed flow and re-asserts its success consequence: if the rebound locator resolves but the consequence no longer fires, the write is REFUSED (status:consequence_broken) \u2014 it heals the locator, never the intent. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|consequence_broken|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
3249
4031
|
inputSchema: {
|
|
3250
|
-
flowName:
|
|
3251
|
-
apply:
|
|
3252
|
-
|
|
4032
|
+
flowName: z7.string().describe("Flow file name to heal (from iris_flow_list)."),
|
|
4033
|
+
apply: z7.boolean().optional(),
|
|
4034
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this heal replay only."),
|
|
4035
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3253
4036
|
},
|
|
3254
4037
|
outputSchema: {
|
|
3255
|
-
flowName:
|
|
3256
|
-
status:
|
|
3257
|
-
applied:
|
|
3258
|
-
proposals:
|
|
3259
|
-
changed:
|
|
3260
|
-
message:
|
|
3261
|
-
error:
|
|
4038
|
+
flowName: z7.string(),
|
|
4039
|
+
status: z7.string(),
|
|
4040
|
+
applied: z7.boolean(),
|
|
4041
|
+
proposals: z7.array(z7.unknown()),
|
|
4042
|
+
changed: z7.array(z7.unknown()),
|
|
4043
|
+
message: z7.string(),
|
|
4044
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3262
4045
|
},
|
|
3263
4046
|
handler: (deps, args) => healFlow(deps, args).then(({ name, ...rest }) => ({ flowName: name, ...rest }))
|
|
3264
4047
|
}
|
|
3265
4048
|
];
|
|
3266
4049
|
var HEAL_MESSAGES = {
|
|
3267
4050
|
NOTHING: "nothing to heal \u2014 every anchor resolved on replay",
|
|
3268
|
-
HEALED: "rewrote drifted testid anchors to their nearest surviving match",
|
|
4051
|
+
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
3269
4052
|
DRIFT_DRY: "confident rebind(s) proposed \u2014 re-run with apply:true to write them to disk",
|
|
3270
|
-
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand
|
|
4053
|
+
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand`,
|
|
4054
|
+
HEALED_UNVERIFIED: "rewrote drifted testid anchors \u2014 but this flow declares no success consequence, so the rebind resolves a locator without proving the intent still holds. Add a success-state assertion (iris_annotate) so future heals can be verified.",
|
|
4055
|
+
CONSEQUENCE_BROKEN: "rebind resolves the drifted locator to a surviving element, but the healed flow no longer satisfies its success consequence \u2014 refusing to write (a heal that loses the intent would ship a green-but-dead test). Fix by hand and verify"
|
|
3271
4056
|
};
|
|
3272
4057
|
function toChange(proposal) {
|
|
3273
4058
|
return { step: proposal.step, from: proposal.from, to: proposal.to };
|
|
3274
4059
|
}
|
|
3275
4060
|
async function healFlow(deps, args) {
|
|
3276
|
-
const name =
|
|
4061
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3277
4062
|
const apply = args["apply"] === true;
|
|
3278
4063
|
const loaded = await deps.flows.load(name);
|
|
3279
4064
|
if (!loaded.ok) {
|
|
@@ -3287,9 +4072,22 @@ async function healFlow(deps, args) {
|
|
|
3287
4072
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3288
4073
|
};
|
|
3289
4074
|
}
|
|
3290
|
-
const session = deps.sessions.resolve(
|
|
3291
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS);
|
|
4075
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4076
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3292
4077
|
const drifted = steps.some((s) => s.drift !== void 0);
|
|
4078
|
+
const failed = steps.find((s) => !s.ok && s.drift === void 0);
|
|
4079
|
+
if (failed !== void 0) {
|
|
4080
|
+
const message = failed.error ?? "flow replay failed before an anchor could be healed";
|
|
4081
|
+
return {
|
|
4082
|
+
name,
|
|
4083
|
+
status: HealStatus.ERROR,
|
|
4084
|
+
applied: false,
|
|
4085
|
+
proposals: [],
|
|
4086
|
+
changed: [],
|
|
4087
|
+
message,
|
|
4088
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
3293
4091
|
if (!drifted) {
|
|
3294
4092
|
return {
|
|
3295
4093
|
name,
|
|
@@ -3321,6 +4119,23 @@ async function healFlow(deps, args) {
|
|
|
3321
4119
|
message: HEAL_MESSAGES.DRIFT_DRY
|
|
3322
4120
|
};
|
|
3323
4121
|
}
|
|
4122
|
+
const { flow: healed } = applyHealChanges(loaded.value, proposals.map(toChange));
|
|
4123
|
+
if (healed.success !== void 0) {
|
|
4124
|
+
const verifyFloor = session.elapsed();
|
|
4125
|
+
const verifySteps = await replayFlow(session, healed, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4126
|
+
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === void 0);
|
|
4127
|
+
const verdict = verifyClean ? await assertSuccess(session, healed.success, dynamicTestids(healed), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, verifyFloor) : { pass: false, failureReason: "healed flow did not replay cleanly" };
|
|
4128
|
+
if (!verdict.pass) {
|
|
4129
|
+
return {
|
|
4130
|
+
name,
|
|
4131
|
+
status: HealStatus.CONSEQUENCE_BROKEN,
|
|
4132
|
+
applied: false,
|
|
4133
|
+
proposals,
|
|
4134
|
+
changed: [],
|
|
4135
|
+
message: `${HEAL_MESSAGES.CONSEQUENCE_BROKEN} (${successLabel(healed.success)}: ${verdict.failureReason ?? "not satisfied"})`
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
3324
4139
|
const written = await deps.flows.heal(name, proposals.map(toChange));
|
|
3325
4140
|
if (!written.ok) {
|
|
3326
4141
|
return {
|
|
@@ -3339,14 +4154,14 @@ async function healFlow(deps, args) {
|
|
|
3339
4154
|
applied: written.value.changed.length > 0,
|
|
3340
4155
|
proposals,
|
|
3341
4156
|
changed: written.value.changed,
|
|
3342
|
-
message: HEAL_MESSAGES.HEALED
|
|
4157
|
+
message: loaded.value.success !== void 0 ? HEAL_MESSAGES.HEALED : HEAL_MESSAGES.HEALED_UNVERIFIED
|
|
3343
4158
|
};
|
|
3344
4159
|
}
|
|
3345
4160
|
|
|
3346
4161
|
// ../server/dist/project/project-tools.js
|
|
3347
|
-
import { z as
|
|
4162
|
+
import { z as z8 } from "zod";
|
|
3348
4163
|
var sessionIdShape3 = {
|
|
3349
|
-
sessionId:
|
|
4164
|
+
sessionId: z8.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3350
4165
|
};
|
|
3351
4166
|
var REGRESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
3352
4167
|
RunStatus.FAIL,
|
|
@@ -3387,12 +4202,12 @@ var PROJECT_TOOLS = [
|
|
|
3387
4202
|
name: IrisTool.PROJECT,
|
|
3388
4203
|
description: 'Read cross-run history from .iris/project.json \u2014 the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". Returns { runs, learned?, lastRun?, diff? } or { error, reason } when no/invalid history exists.',
|
|
3389
4204
|
inputSchema: {
|
|
3390
|
-
name:
|
|
4205
|
+
name: z8.string().optional().describe("Filter runs by this name. Omit to return all runs."),
|
|
3391
4206
|
...sessionIdShape3
|
|
3392
4207
|
},
|
|
3393
4208
|
outputSchema: {
|
|
3394
|
-
runs:
|
|
3395
|
-
diff:
|
|
4209
|
+
runs: z8.array(z8.unknown()),
|
|
4210
|
+
diff: z8.unknown().optional()
|
|
3396
4211
|
},
|
|
3397
4212
|
handler: async (deps, args) => {
|
|
3398
4213
|
const read = await deps.project.read();
|
|
@@ -3402,7 +4217,7 @@ var PROJECT_TOOLS = [
|
|
|
3402
4217
|
reason: read.reason
|
|
3403
4218
|
};
|
|
3404
4219
|
}
|
|
3405
|
-
const name =
|
|
4220
|
+
const name = asString(args["name"]);
|
|
3406
4221
|
if (name === void 0) {
|
|
3407
4222
|
return { runs: read.file.runs, learned: read.file.learned };
|
|
3408
4223
|
}
|
|
@@ -3420,22 +4235,22 @@ var PROJECT_TOOLS = [
|
|
|
3420
4235
|
name: IrisTool.RUN_RECORD,
|
|
3421
4236
|
description: "Explicitly record a run outcome into .iris/project.json (the manual companion to the auto-record on iris_flow_replay). Use it to log the result of an assertion sequence or a manual journey so future runs can diff against it. Returns { recorded:true, name, status }.",
|
|
3422
4237
|
inputSchema: {
|
|
3423
|
-
name:
|
|
3424
|
-
status:
|
|
3425
|
-
kind:
|
|
3426
|
-
summary:
|
|
4238
|
+
name: z8.string().describe("Run name for grouping in iris_project history."),
|
|
4239
|
+
status: z8.nativeEnum(RunStatus).describe("Outcome: pass | fail | drift | error"),
|
|
4240
|
+
kind: z8.nativeEnum(RunKind).optional(),
|
|
4241
|
+
summary: z8.string().optional().describe("One-line human summary of what this run covered."),
|
|
3427
4242
|
...sessionIdShape3
|
|
3428
4243
|
},
|
|
3429
4244
|
outputSchema: {
|
|
3430
|
-
recorded:
|
|
3431
|
-
runName:
|
|
3432
|
-
status:
|
|
4245
|
+
recorded: z8.boolean(),
|
|
4246
|
+
runName: z8.string(),
|
|
4247
|
+
status: z8.string()
|
|
3433
4248
|
},
|
|
3434
4249
|
handler: async (deps, args) => {
|
|
3435
|
-
const name =
|
|
4250
|
+
const name = asString(args["name"]) ?? "";
|
|
3436
4251
|
const status = args["status"];
|
|
3437
4252
|
const kindArg = args["kind"];
|
|
3438
|
-
const summary =
|
|
4253
|
+
const summary = asString(args["summary"]);
|
|
3439
4254
|
await deps.project.recordRun({
|
|
3440
4255
|
kind: typeof kindArg === "string" ? kindArg : RunKind.MANUAL,
|
|
3441
4256
|
name,
|
|
@@ -3448,7 +4263,7 @@ var PROJECT_TOOLS = [
|
|
|
3448
4263
|
];
|
|
3449
4264
|
|
|
3450
4265
|
// ../server/dist/visual/visual-tools.js
|
|
3451
|
-
import { z as
|
|
4266
|
+
import { z as z9 } from "zod";
|
|
3452
4267
|
|
|
3453
4268
|
// ../server/dist/visual/visual-diff.js
|
|
3454
4269
|
async function loadDeps() {
|
|
@@ -3599,13 +4414,13 @@ var VisualStore = class {
|
|
|
3599
4414
|
|
|
3600
4415
|
// ../server/dist/visual/visual-tools.js
|
|
3601
4416
|
var sessionIdShape4 = {
|
|
3602
|
-
sessionId:
|
|
4417
|
+
sessionId: z9.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3603
4418
|
};
|
|
3604
|
-
var rectShape =
|
|
3605
|
-
x:
|
|
3606
|
-
y:
|
|
3607
|
-
width:
|
|
3608
|
-
height:
|
|
4419
|
+
var rectShape = z9.object({
|
|
4420
|
+
x: z9.number(),
|
|
4421
|
+
y: z9.number(),
|
|
4422
|
+
width: z9.number(),
|
|
4423
|
+
height: z9.number()
|
|
3609
4424
|
});
|
|
3610
4425
|
function screenshotProvider(deps) {
|
|
3611
4426
|
const p = deps.realInput;
|
|
@@ -3617,11 +4432,11 @@ var noProvider = {
|
|
|
3617
4432
|
recommendation: VISUAL_NO_PROVIDER_RECOMMENDATION
|
|
3618
4433
|
};
|
|
3619
4434
|
function asBox(value) {
|
|
3620
|
-
const b =
|
|
3621
|
-
const x =
|
|
3622
|
-
const y =
|
|
3623
|
-
const w =
|
|
3624
|
-
const h =
|
|
4435
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
4436
|
+
const x = asNumber(b["x"]);
|
|
4437
|
+
const y = asNumber(b["y"]);
|
|
4438
|
+
const w = asNumber(b["width"]);
|
|
4439
|
+
const h = asNumber(b["height"]);
|
|
3625
4440
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
3626
4441
|
return void 0;
|
|
3627
4442
|
if (w <= 0 || h <= 0)
|
|
@@ -3631,12 +4446,12 @@ function asBox(value) {
|
|
|
3631
4446
|
async function buildOpts(deps, sessionId, args) {
|
|
3632
4447
|
const clipArg = args["clip"];
|
|
3633
4448
|
if (clipArg !== void 0) {
|
|
3634
|
-
const c =
|
|
4449
|
+
const c = asRecord(clipArg);
|
|
3635
4450
|
const box = asBox({ box: c });
|
|
3636
4451
|
if (box !== void 0)
|
|
3637
4452
|
return { clip: box };
|
|
3638
4453
|
}
|
|
3639
|
-
const ref =
|
|
4454
|
+
const ref = asString(args["ref"]);
|
|
3640
4455
|
if (ref !== void 0) {
|
|
3641
4456
|
const session = deps.sessions.resolve(sessionId);
|
|
3642
4457
|
const res = await session.command(IrisCommand.INSPECT, { ref });
|
|
@@ -3662,31 +4477,31 @@ var VISUAL_TOOLS = [
|
|
|
3662
4477
|
name: IrisTool.SCREENSHOT,
|
|
3663
4478
|
description: "Capture a pixel screenshot of the DRIVEN page (needs `iris drive`/IRIS_CDP_URL \u2014 the SDK has no screenshotter) and save it as a visual baseline at .iris/visual/<name>.png. { fullPage } for the whole scroll height, { ref } or { clip:{x,y,width,height} } for one element/region. Returns { saved:true, name, path, bytes } or { ok:false, reason } when no driven browser is attached.",
|
|
3664
4479
|
inputSchema: {
|
|
3665
|
-
name:
|
|
3666
|
-
fullPage:
|
|
3667
|
-
ref:
|
|
4480
|
+
name: z9.string().describe("Baseline name \u2014 saved as .iris/visual/<name>.png. Use the same name in iris_visual_diff to compare."),
|
|
4481
|
+
fullPage: z9.boolean().optional().describe("Capture the full scroll height. Default: viewport only."),
|
|
4482
|
+
ref: z9.string().optional().describe("Element ref to screenshot (scopes to element bounding box). Omit for full page."),
|
|
3668
4483
|
clip: rectShape.optional().describe("Explicit { x, y, width, height } clip rectangle in page coordinates."),
|
|
3669
4484
|
...sessionIdShape4
|
|
3670
4485
|
},
|
|
3671
4486
|
outputSchema: {
|
|
3672
|
-
ok:
|
|
3673
|
-
saved:
|
|
3674
|
-
name:
|
|
3675
|
-
path:
|
|
3676
|
-
bytes:
|
|
3677
|
-
reason:
|
|
3678
|
-
recommendation:
|
|
4487
|
+
ok: z9.boolean(),
|
|
4488
|
+
saved: z9.boolean().optional(),
|
|
4489
|
+
name: z9.string().optional(),
|
|
4490
|
+
path: z9.string().optional(),
|
|
4491
|
+
bytes: z9.number().optional(),
|
|
4492
|
+
reason: z9.string().optional(),
|
|
4493
|
+
recommendation: z9.string().optional()
|
|
3679
4494
|
},
|
|
3680
4495
|
handler: async (deps, args) => {
|
|
3681
4496
|
const provider = screenshotProvider(deps);
|
|
3682
4497
|
if (provider === void 0)
|
|
3683
4498
|
return noProvider;
|
|
3684
|
-
const sessionId =
|
|
4499
|
+
const sessionId = asString(args["sessionId"]);
|
|
3685
4500
|
const session = deps.sessions.resolve(sessionId);
|
|
3686
4501
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3687
4502
|
if (png === void 0)
|
|
3688
4503
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3689
|
-
const name =
|
|
4504
|
+
const name = asString(args["name"]) ?? "default";
|
|
3690
4505
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3691
4506
|
const path = await store.saveBaseline(name, png);
|
|
3692
4507
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
@@ -3696,39 +4511,39 @@ var VISUAL_TOOLS = [
|
|
|
3696
4511
|
name: IrisTool.VISUAL_DIFF,
|
|
3697
4512
|
description: "Perceptually diff the DRIVEN page against a saved visual baseline (see iris_screenshot). { masks:[{x,y,width,height}] } neutralizes volatile regions; { maxRatio } sets the pass tolerance (default 0). Returns { matched, changedPixels, totalPixels, ratio, region?, diffPath, dimensionMismatch } \u2014 the overlay diff is written to .iris/visual/<baseline>.diff.png \u2014 or { ok:false, reason } (no-provider / baseline-missing).",
|
|
3698
4513
|
inputSchema: {
|
|
3699
|
-
baseline:
|
|
3700
|
-
fullPage:
|
|
3701
|
-
ref:
|
|
4514
|
+
baseline: z9.string().describe("Baseline screenshot name (from iris_screenshot). Used to compare with the current screenshot."),
|
|
4515
|
+
fullPage: z9.boolean().optional(),
|
|
4516
|
+
ref: z9.string().optional(),
|
|
3702
4517
|
clip: rectShape.optional(),
|
|
3703
|
-
masks:
|
|
3704
|
-
maxRatio:
|
|
3705
|
-
threshold:
|
|
4518
|
+
masks: z9.array(rectShape).optional(),
|
|
4519
|
+
maxRatio: z9.number().optional(),
|
|
4520
|
+
threshold: z9.number().optional().describe("Pixel difference threshold (0\u20131). Default: 0.01."),
|
|
3706
4521
|
...sessionIdShape4
|
|
3707
4522
|
},
|
|
3708
4523
|
outputSchema: {
|
|
3709
|
-
ok:
|
|
3710
|
-
match:
|
|
3711
|
-
diffPct:
|
|
3712
|
-
diffPath:
|
|
3713
|
-
reason:
|
|
4524
|
+
ok: z9.boolean(),
|
|
4525
|
+
match: z9.boolean().optional(),
|
|
4526
|
+
diffPct: z9.number().optional(),
|
|
4527
|
+
diffPath: z9.string().optional(),
|
|
4528
|
+
reason: z9.string().optional()
|
|
3714
4529
|
},
|
|
3715
4530
|
handler: async (deps, args) => {
|
|
3716
4531
|
const provider = screenshotProvider(deps);
|
|
3717
4532
|
if (provider === void 0)
|
|
3718
4533
|
return noProvider;
|
|
3719
|
-
const baseline =
|
|
4534
|
+
const baseline = asString(args["baseline"]) ?? "";
|
|
3720
4535
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3721
4536
|
const baselineBytes = await store.readBaseline(baseline);
|
|
3722
4537
|
if (baselineBytes === void 0)
|
|
3723
4538
|
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
3724
|
-
const sessionId =
|
|
4539
|
+
const sessionId = asString(args["sessionId"]);
|
|
3725
4540
|
const session = deps.sessions.resolve(sessionId);
|
|
3726
4541
|
const current = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3727
4542
|
if (current === void 0)
|
|
3728
4543
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3729
4544
|
const masks = rectsFrom(args["masks"]);
|
|
3730
|
-
const threshold =
|
|
3731
|
-
const maxRatio =
|
|
4545
|
+
const threshold = asNumber(args["threshold"]);
|
|
4546
|
+
const maxRatio = asNumber(args["maxRatio"]);
|
|
3732
4547
|
const result = await diffPng(baselineBytes, current, {
|
|
3733
4548
|
...threshold !== void 0 ? { threshold } : {},
|
|
3734
4549
|
...maxRatio !== void 0 ? { maxRatio } : {},
|
|
@@ -3745,7 +4560,7 @@ var VISUAL_TOOLS = [
|
|
|
3745
4560
|
];
|
|
3746
4561
|
|
|
3747
4562
|
// ../server/dist/crawl/crawl-tools.js
|
|
3748
|
-
import { z as
|
|
4563
|
+
import { z as z10 } from "zod";
|
|
3749
4564
|
|
|
3750
4565
|
// ../server/dist/crawl/crawl.js
|
|
3751
4566
|
function isActivity(e) {
|
|
@@ -3758,7 +4573,7 @@ function failedRequests(events, floor) {
|
|
|
3758
4573
|
return events.filter((e) => {
|
|
3759
4574
|
if (e.type !== EventType.NET_REQUEST)
|
|
3760
4575
|
return false;
|
|
3761
|
-
const status =
|
|
4576
|
+
const status = asNumber(e.data["status"]);
|
|
3762
4577
|
return status !== void 0 && status >= floor;
|
|
3763
4578
|
});
|
|
3764
4579
|
}
|
|
@@ -3788,7 +4603,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3788
4603
|
const act = await session.command(IrisCommand.ACT, {
|
|
3789
4604
|
ref: item.ref,
|
|
3790
4605
|
action: ActionType.CLICK,
|
|
3791
|
-
args: {}
|
|
4606
|
+
args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {}
|
|
3792
4607
|
});
|
|
3793
4608
|
await sleep(settleMs);
|
|
3794
4609
|
const events = session.eventsSince(since);
|
|
@@ -3799,14 +4614,14 @@ async function crawl(session, opts, sleep) {
|
|
|
3799
4614
|
kind: CrawlAnomalyKind.CONSOLE_ERROR,
|
|
3800
4615
|
ref: item.ref,
|
|
3801
4616
|
desc: item.desc,
|
|
3802
|
-
detail:
|
|
4617
|
+
detail: asString(e.data["message"]) ?? e.type
|
|
3803
4618
|
});
|
|
3804
4619
|
}
|
|
3805
4620
|
for (const e of failedRequests(events, CRAWL_DEFAULTS.FAILED_STATUS)) {
|
|
3806
4621
|
counts.failedRequests += 1;
|
|
3807
|
-
const method =
|
|
3808
|
-
const url =
|
|
3809
|
-
const status =
|
|
4622
|
+
const method = asString(e.data["method"]) ?? "";
|
|
4623
|
+
const url = asString(e.data["url"]) ?? "";
|
|
4624
|
+
const status = asNumber(e.data["status"]);
|
|
3810
4625
|
anomalies.push({
|
|
3811
4626
|
kind: CrawlAnomalyKind.FAILED_REQUEST,
|
|
3812
4627
|
ref: item.ref,
|
|
@@ -3814,7 +4629,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3814
4629
|
detail: `${method} ${url} \u2192 ${status ?? ""}`.trim()
|
|
3815
4630
|
});
|
|
3816
4631
|
}
|
|
3817
|
-
const dispatched =
|
|
4632
|
+
const dispatched = asRecord(act.result)["dispatched"] !== false && act.ok;
|
|
3818
4633
|
if (dispatched && errs.length === 0 && !events.some(isActivity)) {
|
|
3819
4634
|
counts.deadControls += 1;
|
|
3820
4635
|
anomalies.push({
|
|
@@ -3842,32 +4657,34 @@ var CRAWL_TOOLS = [
|
|
|
3842
4657
|
name: IrisTool.CRAWL,
|
|
3843
4658
|
description: "Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script: console errors, failed requests (status \u2265 400), and DEAD controls (dispatched but the app did nothing). DESTRUCTIVE \u2014 it really clicks (may navigate/mutate state); use iris_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.",
|
|
3844
4659
|
inputSchema: {
|
|
3845
|
-
maxSteps:
|
|
3846
|
-
settleMs:
|
|
3847
|
-
scope:
|
|
3848
|
-
|
|
4660
|
+
maxSteps: z10.number().optional().describe("Maximum number of controls to click. Default: 25."),
|
|
4661
|
+
settleMs: z10.number().optional().describe("Milliseconds to wait after each click for the app to react. Default: 500."),
|
|
4662
|
+
scope: z10.string().optional().describe("CSS selector or element ref to restrict crawling to a subtree."),
|
|
4663
|
+
confirmDangerous: z10.boolean().optional().describe("Set true to allow controls classified as destructive. Default false; those controls are blocked by the browser."),
|
|
4664
|
+
sessionId: z10.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3849
4665
|
},
|
|
3850
4666
|
outputSchema: {
|
|
3851
|
-
interactiveFound:
|
|
3852
|
-
stepsRun:
|
|
3853
|
-
anomalies:
|
|
3854
|
-
kind:
|
|
3855
|
-
ref:
|
|
3856
|
-
desc:
|
|
3857
|
-
detail:
|
|
4667
|
+
interactiveFound: z10.number(),
|
|
4668
|
+
stepsRun: z10.number(),
|
|
4669
|
+
anomalies: z10.array(z10.object({
|
|
4670
|
+
kind: z10.string(),
|
|
4671
|
+
ref: z10.string(),
|
|
4672
|
+
desc: z10.string(),
|
|
4673
|
+
detail: z10.string().optional()
|
|
3858
4674
|
})),
|
|
3859
|
-
counts:
|
|
3860
|
-
truncated:
|
|
4675
|
+
counts: z10.record(z10.number()),
|
|
4676
|
+
truncated: z10.boolean()
|
|
3861
4677
|
},
|
|
3862
4678
|
handler: (deps, args) => {
|
|
3863
|
-
const session = deps.sessions.resolve(
|
|
3864
|
-
const maxSteps =
|
|
3865
|
-
const settleMs =
|
|
3866
|
-
const scope =
|
|
4679
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4680
|
+
const maxSteps = asNumber(args["maxSteps"]);
|
|
4681
|
+
const settleMs = asNumber(args["settleMs"]);
|
|
4682
|
+
const scope = asString(args["scope"]);
|
|
3867
4683
|
const opts = {
|
|
3868
4684
|
...maxSteps !== void 0 ? { maxSteps } : {},
|
|
3869
4685
|
...settleMs !== void 0 ? { settleMs } : {},
|
|
3870
|
-
...scope !== void 0 ? { scope } : {}
|
|
4686
|
+
...scope !== void 0 ? { scope } : {},
|
|
4687
|
+
...args["confirmDangerous"] === true ? { confirmDangerous: true } : {}
|
|
3871
4688
|
};
|
|
3872
4689
|
return crawl(session, opts, nodeSleep2);
|
|
3873
4690
|
}
|
|
@@ -3875,7 +4692,7 @@ var CRAWL_TOOLS = [
|
|
|
3875
4692
|
];
|
|
3876
4693
|
|
|
3877
4694
|
// ../server/dist/input/scroll-tools.js
|
|
3878
|
-
import { z as
|
|
4695
|
+
import { z as z11 } from "zod";
|
|
3879
4696
|
|
|
3880
4697
|
// ../server/dist/input/scroll-find.js
|
|
3881
4698
|
async function queryFirst(session, q) {
|
|
@@ -3884,9 +4701,9 @@ async function queryFirst(session, q) {
|
|
|
3884
4701
|
value: q.value,
|
|
3885
4702
|
...q.name !== void 0 ? { name: q.name } : {}
|
|
3886
4703
|
});
|
|
3887
|
-
const elements =
|
|
4704
|
+
const elements = asRecord(res.result)["elements"];
|
|
3888
4705
|
if (Array.isArray(elements) && elements.length > 0)
|
|
3889
|
-
return
|
|
4706
|
+
return asRecord(elements[0]);
|
|
3890
4707
|
return void 0;
|
|
3891
4708
|
}
|
|
3892
4709
|
async function scrollToFind(session, q, opts = {}) {
|
|
@@ -3905,7 +4722,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3905
4722
|
const hit = await queryFirst(session, q);
|
|
3906
4723
|
if (hit !== void 0)
|
|
3907
4724
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
3908
|
-
const data =
|
|
4725
|
+
const data = asRecord(sr.result);
|
|
3909
4726
|
if (data["atEnd"] === true || data["scrolled"] === false) {
|
|
3910
4727
|
return { found: false, scrolls, exhausted: true };
|
|
3911
4728
|
}
|
|
@@ -3913,7 +4730,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3913
4730
|
for (let i = 0; i < max; i += 1) {
|
|
3914
4731
|
const sr = await session.command(IrisCommand.SCROLL, q.container !== void 0 ? { ref: q.container } : {});
|
|
3915
4732
|
scrolls += 1;
|
|
3916
|
-
const data =
|
|
4733
|
+
const data = asRecord(sr.result);
|
|
3917
4734
|
const hit = await queryFirst(session, q);
|
|
3918
4735
|
if (hit !== void 0)
|
|
3919
4736
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
@@ -3930,58 +4747,58 @@ var SCROLL_TOOLS = [
|
|
|
3930
4747
|
name: IrisTool.SCROLL_TO,
|
|
3931
4748
|
description: "Find an element in a VIRTUALIZED list that has not rendered yet. Pass `by` (role|text|testid|label|placeholder|alt) and `value` (query string) to identify the target row. Scrolls the container until the row mounts, the list ends, or maxScrolls (default 20) is spent. Pass targetIndex + totalCount for bisection \u2014 jumps directly to the estimated offset in one scroll (e.g. targetIndex:800 totalCount:1000 jumps to 80% of scrollHeight). Returns { found, element?, scrolls, exhausted }.",
|
|
3932
4749
|
inputSchema: {
|
|
3933
|
-
by:
|
|
3934
|
-
value:
|
|
3935
|
-
name:
|
|
3936
|
-
container:
|
|
3937
|
-
maxScrolls:
|
|
3938
|
-
targetIndex:
|
|
3939
|
-
totalCount:
|
|
3940
|
-
sessionId:
|
|
4750
|
+
by: z11.string().describe("Query strategy for finding the target: role | text | testid | label | placeholder | alt"),
|
|
4751
|
+
value: z11.string().describe("Query value for the selected strategy (the element to scroll into view)."),
|
|
4752
|
+
name: z11.string().optional().describe("Optional accessible name filter when using by=role."),
|
|
4753
|
+
container: z11.string().optional().describe("Element ref for the scrollable container. Omit to scroll the document."),
|
|
4754
|
+
maxScrolls: z11.number().optional().describe("Maximum number of scroll steps before giving up. Default: 20."),
|
|
4755
|
+
targetIndex: z11.number().optional().describe("Known row index of the target in the list. Combine with totalCount for bisection \u2014 jumps directly to the estimated offset."),
|
|
4756
|
+
totalCount: z11.number().optional().describe("Total item count in the virtualized list. Required for bisection with targetIndex."),
|
|
4757
|
+
sessionId: z11.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3941
4758
|
},
|
|
3942
4759
|
outputSchema: {
|
|
3943
|
-
found:
|
|
3944
|
-
element:
|
|
3945
|
-
scrolls:
|
|
3946
|
-
exhausted:
|
|
4760
|
+
found: z11.boolean(),
|
|
4761
|
+
element: z11.object({ ref: z11.string(), role: z11.string(), name: z11.string() }).optional(),
|
|
4762
|
+
scrolls: z11.number(),
|
|
4763
|
+
exhausted: z11.boolean()
|
|
3947
4764
|
},
|
|
3948
4765
|
handler: (deps, args) => {
|
|
3949
|
-
const session = deps.sessions.resolve(
|
|
3950
|
-
const name =
|
|
3951
|
-
const container =
|
|
3952
|
-
const targetIndex =
|
|
3953
|
-
const totalCount =
|
|
4766
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4767
|
+
const name = asString(args["name"]);
|
|
4768
|
+
const container = asString(args["container"]);
|
|
4769
|
+
const targetIndex = asNumber(args["targetIndex"]);
|
|
4770
|
+
const totalCount = asNumber(args["totalCount"]);
|
|
3954
4771
|
const q = {
|
|
3955
|
-
by:
|
|
3956
|
-
value:
|
|
4772
|
+
by: asString(args["by"]) ?? "",
|
|
4773
|
+
value: asString(args["value"]) ?? "",
|
|
3957
4774
|
...name !== void 0 ? { name } : {},
|
|
3958
4775
|
...container !== void 0 ? { container } : {},
|
|
3959
4776
|
...targetIndex !== void 0 ? { targetIndex } : {},
|
|
3960
4777
|
...totalCount !== void 0 ? { totalCount } : {}
|
|
3961
4778
|
};
|
|
3962
|
-
const maxScrolls =
|
|
4779
|
+
const maxScrolls = asNumber(args["maxScrolls"]);
|
|
3963
4780
|
return scrollToFind(session, q, maxScrolls !== void 0 ? { maxScrolls } : {});
|
|
3964
4781
|
}
|
|
3965
4782
|
}
|
|
3966
4783
|
];
|
|
3967
4784
|
|
|
3968
4785
|
// ../server/dist/session/session-tools.js
|
|
3969
|
-
import { z as
|
|
4786
|
+
import { z as z12 } from "zod";
|
|
3970
4787
|
var SESSION_TOOLS = [
|
|
3971
4788
|
{
|
|
3972
4789
|
name: IrisTool.SESSION,
|
|
3973
4790
|
description: "Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before it AUTO-ENDS (page glow off, the floating panel is kept so the human can read + Copy/Export the run). Default 5min. Raise it for slow apps, lower it for quick checks. The auto-end is enforced SERVER-SIDE (immune to background-tab throttling) and also fires if you (the MCP client) disconnect \u2014 so a forgotten or crashed session never leaves the HUD running forever. If you go quiet and then act again, the session revives automatically. Returns { applied, idleEndMs }.",
|
|
3974
4791
|
inputSchema: {
|
|
3975
|
-
idleEndMs:
|
|
3976
|
-
sessionId:
|
|
4792
|
+
idleEndMs: z12.number().optional().describe("Idle window in milliseconds after which the presenter session auto-ends. Default: 300000 (5 min). Raise for slow apps."),
|
|
4793
|
+
sessionId: z12.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3977
4794
|
},
|
|
3978
4795
|
outputSchema: {
|
|
3979
|
-
applied:
|
|
3980
|
-
idleEndMs:
|
|
4796
|
+
applied: z12.boolean(),
|
|
4797
|
+
idleEndMs: z12.number().optional()
|
|
3981
4798
|
},
|
|
3982
4799
|
handler: async (deps, args) => {
|
|
3983
|
-
const session = deps.sessions.resolve(
|
|
3984
|
-
const idleEndMs =
|
|
4800
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4801
|
+
const idleEndMs = asNumber(args["idleEndMs"]);
|
|
3985
4802
|
if (idleEndMs !== void 0)
|
|
3986
4803
|
session.setIdleEndMs(idleEndMs);
|
|
3987
4804
|
const res = await session.command(IrisCommand.SESSION_CONFIG, idleEndMs !== void 0 ? { idleEndMs } : {});
|
|
@@ -3993,7 +4810,7 @@ var SESSION_TOOLS = [
|
|
|
3993
4810
|
];
|
|
3994
4811
|
|
|
3995
4812
|
// ../server/dist/flows/annotate-tools.js
|
|
3996
|
-
import { z as
|
|
4813
|
+
import { z as z13 } from "zod";
|
|
3997
4814
|
|
|
3998
4815
|
// ../server/dist/flows/annotate.js
|
|
3999
4816
|
function compileAnnotation(a, stepCount) {
|
|
@@ -4070,23 +4887,23 @@ var ANNOTATE_TOOLS = [
|
|
|
4070
4887
|
description: "Attach a STRUCTURED annotation to the active recording, compiling it into the flow. kind: assert-signal { name, dataMatches? } \u2192 the last step asserts that signal; assert-visible { testid } \u2192 the last step asserts that element is present; mark-dynamic { testid } \u2192 the flow records that region as LLM-dynamic (replay won't assert its content); success-state { signal | testid } \u2192 the flow golden end-condition. Folded onto disk by iris_flow_save. Returns { ok:true, target:step|flow, compiled } (e.g. \"will assert signal diff:shown\") or { ok:false, code } (annotate_no_recording | annotate_no_step | annotate_unknown_kind | annotate_missing_field). FIRST CUT: structured only \u2014 a free natural-language string is rejected (annotate_unknown_kind), never guessed into a predicate. Pass `flow` to target a named recording (defaults to 'default'); `name` is the assert-signal's SIGNAL name, not the recording.",
|
|
4071
4888
|
inputSchema: {
|
|
4072
4889
|
// `flow` selects the recording; `name`/`signal`/`testid`/`dataMatches` are the annotation fields.
|
|
4073
|
-
flow:
|
|
4074
|
-
kind:
|
|
4075
|
-
name:
|
|
4076
|
-
testid:
|
|
4077
|
-
signal:
|
|
4078
|
-
dataMatches:
|
|
4079
|
-
sessionId:
|
|
4080
|
-
annotation:
|
|
4890
|
+
flow: z13.string().optional().describe("Named recording to annotate. Defaults to 'default'."),
|
|
4891
|
+
kind: z13.string().describe("Annotation kind: assert-signal | assert-visible | mark-dynamic | success-state."),
|
|
4892
|
+
name: z13.string().optional().describe("Signal name for assert-signal annotations."),
|
|
4893
|
+
testid: z13.string().optional().describe("data-testid value for assert-visible / mark-dynamic / success-state annotations."),
|
|
4894
|
+
signal: z13.string().optional().describe("Signal name for success-state annotations."),
|
|
4895
|
+
dataMatches: z13.record(z13.unknown()).optional().describe("Key/value pairs the signal payload must match (assert-signal only)."),
|
|
4896
|
+
sessionId: z13.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open."),
|
|
4897
|
+
annotation: z13.unknown().optional().describe("Structured annotation: { kind, name, dataMatches? } for assert-signal; { kind, testid } for assert-visible / mark-dynamic; { kind, signal?, testid? } for success-state.")
|
|
4081
4898
|
},
|
|
4082
4899
|
outputSchema: {
|
|
4083
|
-
ok:
|
|
4084
|
-
target:
|
|
4085
|
-
compiled:
|
|
4086
|
-
code:
|
|
4900
|
+
ok: z13.boolean(),
|
|
4901
|
+
target: z13.string().optional(),
|
|
4902
|
+
compiled: z13.string().optional(),
|
|
4903
|
+
code: z13.string().optional()
|
|
4087
4904
|
},
|
|
4088
4905
|
handler: (deps, args) => {
|
|
4089
|
-
const name =
|
|
4906
|
+
const name = asString(args["flow"]) ?? DEFAULT_RECORDING;
|
|
4090
4907
|
const parsed = AnnotationSchema.safeParse(args);
|
|
4091
4908
|
if (!parsed.success) {
|
|
4092
4909
|
return Promise.resolve({ ok: false, code: AnnotationErrorCode.UNKNOWN_KIND });
|
|
@@ -4115,19 +4932,19 @@ var ANNOTATE_TOOLS = [
|
|
|
4115
4932
|
];
|
|
4116
4933
|
|
|
4117
4934
|
// ../server/dist/session/live-control-tools.js
|
|
4118
|
-
import { z as
|
|
4935
|
+
import { z as z14 } from "zod";
|
|
4119
4936
|
var sessionIdShape5 = {
|
|
4120
|
-
sessionId:
|
|
4937
|
+
sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4121
4938
|
};
|
|
4122
4939
|
var LIVE_CONTROL_TOOLS = [
|
|
4123
4940
|
{
|
|
4124
4941
|
name: IrisTool.END_SESSION,
|
|
4125
4942
|
description: 'End this live testing session. Sets the server state to "ended", tells the human panel (PRESENTER), and stops driving. Optional `summary` is shown in the panel. Idempotent.',
|
|
4126
|
-
inputSchema: { summary:
|
|
4127
|
-
outputSchema: { ended:
|
|
4943
|
+
inputSchema: { summary: z14.string().optional(), ...sessionIdShape5 },
|
|
4944
|
+
outputSchema: { ended: z14.boolean(), sessionId: z14.string() },
|
|
4128
4945
|
handler: (deps, args) => {
|
|
4129
|
-
const session = deps.sessions.resolve(
|
|
4130
|
-
session.setState(SessionState.ENDED,
|
|
4946
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4947
|
+
session.setState(SessionState.ENDED, asString(args["summary"]));
|
|
4131
4948
|
return Promise.resolve({ ended: true, sessionId: session.id });
|
|
4132
4949
|
}
|
|
4133
4950
|
},
|
|
@@ -4135,9 +4952,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4135
4952
|
name: IrisTool.RESUME,
|
|
4136
4953
|
description: 'Clear a human pause and resume driving the page. Sets state "active" and syncs the panel (PRESENTER). Call after you have addressed the human guidance returned by a paused iris_act.',
|
|
4137
4954
|
inputSchema: { ...sessionIdShape5 },
|
|
4138
|
-
outputSchema: { ok:
|
|
4955
|
+
outputSchema: { ok: z14.boolean() },
|
|
4139
4956
|
handler: (deps, args) => {
|
|
4140
|
-
const session = deps.sessions.resolve(
|
|
4957
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4141
4958
|
session.setState(SessionState.ACTIVE);
|
|
4142
4959
|
return Promise.resolve({ ok: true });
|
|
4143
4960
|
}
|
|
@@ -4146,9 +4963,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4146
4963
|
name: IrisTool.MESSAGES,
|
|
4147
4964
|
description: "Drain and return any messages the human queued from the panel since the last poll. Use to explicitly check for human guidance without acting.",
|
|
4148
4965
|
inputSchema: { ...sessionIdShape5 },
|
|
4149
|
-
outputSchema: { messages:
|
|
4966
|
+
outputSchema: { messages: z14.array(z14.unknown()) },
|
|
4150
4967
|
handler: (deps, args) => {
|
|
4151
|
-
const session = deps.sessions.resolve(
|
|
4968
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4152
4969
|
return Promise.resolve({ messages: session.drainInbox() });
|
|
4153
4970
|
}
|
|
4154
4971
|
}
|
|
@@ -4174,7 +4991,7 @@ function withControl(session, result) {
|
|
|
4174
4991
|
}
|
|
4175
4992
|
|
|
4176
4993
|
// ../server/dist/update/update-tools.js
|
|
4177
|
-
import { z as
|
|
4994
|
+
import { z as z15 } from "zod";
|
|
4178
4995
|
|
|
4179
4996
|
// ../server/dist/update/update-checker.js
|
|
4180
4997
|
import * as fs from "fs";
|
|
@@ -4368,14 +5185,14 @@ var UPDATE_TOOLS = [
|
|
|
4368
5185
|
description: "Returns the running Iris version, latest available version, release changelog, and any breaking changes. Call this at the start of a session or when unexpected tool behavior suggests a version mismatch.",
|
|
4369
5186
|
inputSchema: {},
|
|
4370
5187
|
outputSchema: {
|
|
4371
|
-
currentVersion:
|
|
4372
|
-
latestVersion:
|
|
4373
|
-
updateAvailable:
|
|
4374
|
-
executionKind:
|
|
4375
|
-
changelog:
|
|
4376
|
-
breakingChanges:
|
|
4377
|
-
rollbackAvailable:
|
|
4378
|
-
previousVersion:
|
|
5188
|
+
currentVersion: z15.string().describe("The Iris server version currently running."),
|
|
5189
|
+
latestVersion: z15.string().optional().describe("Latest published version on npm."),
|
|
5190
|
+
updateAvailable: z15.boolean().describe("True when a newer version is available to install."),
|
|
5191
|
+
executionKind: z15.string().describe('How iris was launched: "npx" (no install needed \u2014 restart applies update), "global" (npm install -g), or "local" (project node_modules).'),
|
|
5192
|
+
changelog: z15.string().optional().describe("Release notes for the latest version."),
|
|
5193
|
+
breakingChanges: z15.array(z15.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
|
|
5194
|
+
rollbackAvailable: z15.boolean().describe("True when a previous version is stored and can be restored."),
|
|
5195
|
+
previousVersion: z15.string().optional().describe("The version that would be restored on rollback.")
|
|
4379
5196
|
},
|
|
4380
5197
|
handler: async (_deps) => {
|
|
4381
5198
|
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
@@ -4395,11 +5212,11 @@ var UPDATE_TOOLS = [
|
|
|
4395
5212
|
name: IrisTool.APPLY_UPDATE,
|
|
4396
5213
|
description: 'Install the latest Iris server version and restart. Strategy depends on how iris was launched (check executionKind from iris_version_info): "global" and "local" installs run npm install then exit; "npx" just exits \u2014 Claude Code restarts and npx re-resolves the latest version from npm automatically. The MCP connection briefly drops during restart.',
|
|
4397
5214
|
inputSchema: {
|
|
4398
|
-
confirm:
|
|
5215
|
+
confirm: z15.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
|
|
4399
5216
|
},
|
|
4400
5217
|
outputSchema: {
|
|
4401
|
-
ok:
|
|
4402
|
-
message:
|
|
5218
|
+
ok: z15.boolean(),
|
|
5219
|
+
message: z15.string().optional()
|
|
4403
5220
|
},
|
|
4404
5221
|
handler: async (_deps, args) => {
|
|
4405
5222
|
if (args["confirm"] !== true) {
|
|
@@ -4417,11 +5234,11 @@ var UPDATE_TOOLS = [
|
|
|
4417
5234
|
name: IrisTool.ROLLBACK,
|
|
4418
5235
|
description: "Restore the previous Iris server version and restart. Use when an update introduced a regression. The MCP connection will briefly drop \u2014 Claude Code restarts the process automatically with the restored binary.",
|
|
4419
5236
|
inputSchema: {
|
|
4420
|
-
confirm:
|
|
5237
|
+
confirm: z15.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
|
|
4421
5238
|
},
|
|
4422
5239
|
outputSchema: {
|
|
4423
|
-
ok:
|
|
4424
|
-
message:
|
|
5240
|
+
ok: z15.boolean(),
|
|
5241
|
+
message: z15.string().optional()
|
|
4425
5242
|
},
|
|
4426
5243
|
handler: async (_deps, args) => {
|
|
4427
5244
|
if (args["confirm"] !== true) {
|
|
@@ -4443,7 +5260,7 @@ async function snapshotTree(deps, sessionId) {
|
|
|
4443
5260
|
return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
|
|
4444
5261
|
}
|
|
4445
5262
|
var sessionIdShape6 = {
|
|
4446
|
-
sessionId:
|
|
5263
|
+
sessionId: z16.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
|
|
4447
5264
|
};
|
|
4448
5265
|
async function commandOrThrow3(deps, sessionId, name, args) {
|
|
4449
5266
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4453,11 +5270,11 @@ async function commandOrThrow3(deps, sessionId, name, args) {
|
|
|
4453
5270
|
return result.result;
|
|
4454
5271
|
}
|
|
4455
5272
|
function asBox2(value) {
|
|
4456
|
-
const b =
|
|
4457
|
-
const x =
|
|
4458
|
-
const y =
|
|
4459
|
-
const w =
|
|
4460
|
-
const h =
|
|
5273
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
5274
|
+
const x = asNumber(b["x"]);
|
|
5275
|
+
const y = asNumber(b["y"]);
|
|
5276
|
+
const w = asNumber(b["width"]);
|
|
5277
|
+
const h = asNumber(b["height"]);
|
|
4461
5278
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
4462
5279
|
return void 0;
|
|
4463
5280
|
if (w <= 0 || h <= 0)
|
|
@@ -4473,29 +5290,51 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4473
5290
|
return synthetic();
|
|
4474
5291
|
if (!isPointerAction(action))
|
|
4475
5292
|
return synthetic(InputModeReason.NOT_POINTER);
|
|
4476
|
-
const inner =
|
|
5293
|
+
const inner = asRecord(args["args"]);
|
|
4477
5294
|
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && inner["native"] !== true) {
|
|
4478
5295
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
4479
5296
|
}
|
|
4480
5297
|
if (!await provider.isAvailableFor(session.url))
|
|
4481
5298
|
return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
|
|
4482
|
-
const
|
|
5299
|
+
const inspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, { ref });
|
|
5300
|
+
const confirmed = inner[DANGEROUS_ACTION_CONFIRM_ARG] === true;
|
|
5301
|
+
const dangerousDescriptorText = (value2) => {
|
|
5302
|
+
const descriptor = asRecord(value2);
|
|
5303
|
+
return [
|
|
5304
|
+
asString(descriptor["name"]) ?? "",
|
|
5305
|
+
asString(descriptor["text"]) ?? "",
|
|
5306
|
+
asString(descriptor["value"]) ?? "",
|
|
5307
|
+
asString(descriptor["href"]) ?? "",
|
|
5308
|
+
asString(descriptor["formAction"]) ?? "",
|
|
5309
|
+
asString(descriptor["formText"]) ?? ""
|
|
5310
|
+
].join(" ");
|
|
5311
|
+
};
|
|
5312
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !confirmed && isDangerousActionText(dangerousDescriptorText(inspected))) {
|
|
5313
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5314
|
+
}
|
|
5315
|
+
const box = asBox2(inspected);
|
|
4483
5316
|
if (box === void 0)
|
|
4484
5317
|
return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
|
|
4485
5318
|
let toBox;
|
|
4486
5319
|
if (action === ActionType.DRAG) {
|
|
4487
|
-
const toRef =
|
|
5320
|
+
const toRef = asString(inner["toRef"]);
|
|
4488
5321
|
if (toRef === void 0)
|
|
4489
5322
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4490
|
-
|
|
5323
|
+
const targetInspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, {
|
|
5324
|
+
ref: toRef
|
|
5325
|
+
});
|
|
5326
|
+
if (!confirmed && isDangerousActionText(`${dangerousDescriptorText(inspected)} ${dangerousDescriptorText(targetInspected)}`)) {
|
|
5327
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5328
|
+
}
|
|
5329
|
+
toBox = asBox2(targetInspected);
|
|
4491
5330
|
if (toBox === void 0)
|
|
4492
5331
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4493
5332
|
}
|
|
4494
5333
|
const performArgs = {};
|
|
4495
|
-
const value =
|
|
5334
|
+
const value = asString(inner["value"]);
|
|
4496
5335
|
if (value !== void 0)
|
|
4497
5336
|
performArgs.value = value;
|
|
4498
|
-
const text =
|
|
5337
|
+
const text = asString(inner["text"]);
|
|
4499
5338
|
if (text !== void 0)
|
|
4500
5339
|
performArgs.text = text;
|
|
4501
5340
|
if (toBox !== void 0)
|
|
@@ -4514,23 +5353,24 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4514
5353
|
};
|
|
4515
5354
|
}
|
|
4516
5355
|
}
|
|
5356
|
+
var SNAPSHOT_CACHE = new SnapshotCache();
|
|
4517
5357
|
var TOOLS = [
|
|
4518
5358
|
{
|
|
4519
5359
|
name: IrisTool.SESSIONS,
|
|
4520
5360
|
description: "List connected browser sessions (tab url/title, sessionId, last-seen, health: hidden/focused/throttled, and `realInputAvailable` \u2014 true when native CDP/launched real input is driving this tab), plus a `recommendation` pointing to `iris drive` when a tab is hidden/throttled and may be un-scriptable from here.",
|
|
4521
5361
|
inputSchema: {},
|
|
4522
5362
|
outputSchema: {
|
|
4523
|
-
sessions:
|
|
4524
|
-
sessionId:
|
|
4525
|
-
url:
|
|
4526
|
-
title:
|
|
4527
|
-
lastSeenMs:
|
|
4528
|
-
throttled:
|
|
4529
|
-
focused:
|
|
4530
|
-
hidden:
|
|
4531
|
-
realInputAvailable:
|
|
4532
|
-
stale:
|
|
4533
|
-
recommendation:
|
|
5363
|
+
sessions: z16.array(z16.object({
|
|
5364
|
+
sessionId: z16.string(),
|
|
5365
|
+
url: z16.string(),
|
|
5366
|
+
title: z16.string().optional(),
|
|
5367
|
+
lastSeenMs: z16.number(),
|
|
5368
|
+
throttled: z16.boolean(),
|
|
5369
|
+
focused: z16.boolean(),
|
|
5370
|
+
hidden: z16.boolean(),
|
|
5371
|
+
realInputAvailable: z16.boolean().optional(),
|
|
5372
|
+
stale: z16.boolean().optional(),
|
|
5373
|
+
recommendation: z16.string().optional()
|
|
4534
5374
|
})).describe("Connected browser sessions with health state.")
|
|
4535
5375
|
},
|
|
4536
5376
|
handler: async (deps) => {
|
|
@@ -4544,71 +5384,95 @@ var TOOLS = [
|
|
|
4544
5384
|
},
|
|
4545
5385
|
{
|
|
4546
5386
|
name: IrisTool.SNAPSHOT,
|
|
4547
|
-
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
|
|
5387
|
+
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now. The result carries cost:{ bytes, tokens } (estimated) \u2014 if it is large, re-scope (pass `scope`) or use mode:interactive/status instead of reading the whole tree. Pass diff:true after your first snapshot to get back ONLY what changed since your last look (mode:delta with added/removed, or mode:unchanged) \u2014 far fewer tokens and no stale tree to mis-read; a route change resets it to a full snapshot automatically.",
|
|
4548
5388
|
inputSchema: {
|
|
4549
|
-
scope:
|
|
4550
|
-
mode:
|
|
5389
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
|
|
5390
|
+
mode: z16.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
|
|
5391
|
+
diff: z16.boolean().optional().describe("Return only what changed since your last snapshot of the same scope/mode (mode:delta|unchanged). First call (or after a route change) still returns the full tree."),
|
|
4551
5392
|
...sessionIdShape6
|
|
4552
5393
|
},
|
|
4553
5394
|
outputSchema: {
|
|
4554
|
-
tree:
|
|
4555
|
-
status:
|
|
5395
|
+
tree: z16.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
|
|
5396
|
+
status: z16.object({ route: z16.string(), title: z16.string().optional() }).optional(),
|
|
5397
|
+
mode: z16.string().optional().describe("delta | unchanged when diff:true returned a change set."),
|
|
5398
|
+
delta: z16.object({
|
|
5399
|
+
added: z16.array(z16.string()),
|
|
5400
|
+
removed: z16.array(z16.string()),
|
|
5401
|
+
addedCount: z16.number(),
|
|
5402
|
+
removedCount: z16.number()
|
|
5403
|
+
}).optional().describe("Only present on a diff:true call that found changes."),
|
|
5404
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 re-scope if large.")
|
|
4556
5405
|
},
|
|
4557
|
-
handler: (deps, args) =>
|
|
4558
|
-
|
|
4559
|
-
mode
|
|
4560
|
-
|
|
5406
|
+
handler: (deps, args) => {
|
|
5407
|
+
const sessionId = asString(args["sessionId"]);
|
|
5408
|
+
const mode = asString(args["mode"]) ?? SnapshotMode.FULL;
|
|
5409
|
+
return commandOrThrow3(deps, sessionId, IrisCommand.SNAPSHOT, {
|
|
5410
|
+
scope: args["scope"],
|
|
5411
|
+
mode
|
|
5412
|
+
}).then((raw) => withSizeCost(applySnapshotDelta(raw, {
|
|
5413
|
+
sessionId: sessionId ?? "default",
|
|
5414
|
+
scope: asString(args["scope"]) ?? "",
|
|
5415
|
+
mode,
|
|
5416
|
+
diff: args["diff"] === true
|
|
5417
|
+
}, SNAPSHOT_CACHE)));
|
|
5418
|
+
}
|
|
4561
5419
|
},
|
|
4562
5420
|
{
|
|
4563
5421
|
name: IrisTool.QUERY,
|
|
4564
|
-
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
5422
|
+
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. Pass `limit` to cap descriptors (broad role queries can be large) or `count_only:true` for just the match count \u2014 both cut tokens. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
4565
5423
|
inputSchema: {
|
|
4566
|
-
by:
|
|
4567
|
-
value:
|
|
4568
|
-
name:
|
|
4569
|
-
scope:
|
|
5424
|
+
by: z16.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
|
|
5425
|
+
value: z16.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
|
|
5426
|
+
name: z16.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
|
|
5427
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
|
|
5428
|
+
limit: z16.number().optional().describe("Cap the returned descriptors to the first N (cuts tokens on broad queries). If more matched, the result carries total + truncated:true so the trim is never silent \u2014 narrow with name/scope."),
|
|
5429
|
+
count_only: z16.boolean().optional().describe('Return just { count } (no element descriptors) \u2014 use when you only need "how many match?" and not their refs.'),
|
|
4570
5430
|
...sessionIdShape6
|
|
4571
5431
|
},
|
|
4572
5432
|
outputSchema: {
|
|
4573
|
-
elements:
|
|
4574
|
-
ref:
|
|
4575
|
-
role:
|
|
4576
|
-
name:
|
|
4577
|
-
value:
|
|
4578
|
-
states:
|
|
4579
|
-
visible:
|
|
4580
|
-
})),
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
5433
|
+
elements: z16.array(z16.object({
|
|
5434
|
+
ref: z16.string(),
|
|
5435
|
+
role: z16.string(),
|
|
5436
|
+
name: z16.string(),
|
|
5437
|
+
value: z16.string().optional(),
|
|
5438
|
+
states: z16.array(z16.string()),
|
|
5439
|
+
visible: z16.boolean()
|
|
5440
|
+
})).optional(),
|
|
5441
|
+
count: z16.number().optional().describe("Match count \u2014 present when count_only is set."),
|
|
5442
|
+
total: z16.number().optional().describe("Total matches before `limit` truncation \u2014 present only when truncated."),
|
|
5443
|
+
truncated: z16.boolean().optional().describe("True when `limit` dropped some matches."),
|
|
5444
|
+
hint: z16.object({
|
|
5445
|
+
route: z16.string(),
|
|
5446
|
+
presentTestids: z16.array(z16.string()),
|
|
5447
|
+
knownEmptyState: z16.boolean()
|
|
5448
|
+
}).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss."),
|
|
5449
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 narrow with `name`/`scope`/`limit` if large.")
|
|
4586
5450
|
},
|
|
4587
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5451
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.QUERY, {
|
|
4588
5452
|
by: args["by"],
|
|
4589
5453
|
value: args["value"],
|
|
4590
5454
|
name: args["name"],
|
|
4591
5455
|
scope: args["scope"]
|
|
4592
|
-
})
|
|
5456
|
+
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args["limit"]), args["count_only"] === true)))
|
|
4593
5457
|
},
|
|
4594
5458
|
{
|
|
4595
5459
|
name: IrisTool.INSPECT,
|
|
4596
5460
|
description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
|
|
4597
5461
|
inputSchema: {
|
|
4598
|
-
ref:
|
|
5462
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
4599
5463
|
...sessionIdShape6
|
|
4600
5464
|
},
|
|
4601
5465
|
outputSchema: {
|
|
4602
|
-
ref:
|
|
4603
|
-
role:
|
|
4604
|
-
name:
|
|
4605
|
-
value:
|
|
4606
|
-
states:
|
|
4607
|
-
visible:
|
|
4608
|
-
box:
|
|
4609
|
-
component:
|
|
5466
|
+
ref: z16.string(),
|
|
5467
|
+
role: z16.string(),
|
|
5468
|
+
name: z16.string(),
|
|
5469
|
+
value: z16.string().optional(),
|
|
5470
|
+
states: z16.array(z16.string()),
|
|
5471
|
+
visible: z16.boolean(),
|
|
5472
|
+
box: z16.object({ x: z16.number(), y: z16.number(), width: z16.number(), height: z16.number() }).optional(),
|
|
5473
|
+
component: z16.object({ name: z16.string().optional(), sourceFile: z16.string().optional() }).optional()
|
|
4610
5474
|
},
|
|
4611
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5475
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.INSPECT, {
|
|
4612
5476
|
ref: args["ref"]
|
|
4613
5477
|
})
|
|
4614
5478
|
},
|
|
@@ -4616,30 +5480,30 @@ var TOOLS = [
|
|
|
4616
5480
|
name: IrisTool.ACT,
|
|
4617
5481
|
description: 'Execute one action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Returns immediately with a `since` cursor \u2014 observe the reaction with iris_observe. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. occluded=true means the click point is covered by another element (a real user could not click it) \u2014 synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured \u2014 pass args.native:true to force a trusted native click (file pickers, clipboard). inputModeReason explains any real\u2192synthetic choice so it is never silent. Full model (real-input, throttled tabs, `iris drive`): docs/usage.md \xA718.',
|
|
4618
5482
|
inputSchema: {
|
|
4619
|
-
ref:
|
|
4620
|
-
action:
|
|
4621
|
-
args:
|
|
4622
|
-
refuseWhenThrottled:
|
|
5483
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
5484
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5485
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { native: true } to force a trusted native click, { confirmDangerous: true } to allow a potentially destructive control."),
|
|
5486
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw instead of silently sending synthetic events when the tab is throttled/backgrounded. Default: false (synthetic events are still sent)."),
|
|
4623
5487
|
...sessionIdShape6
|
|
4624
5488
|
},
|
|
4625
5489
|
outputSchema: {
|
|
4626
|
-
since:
|
|
4627
|
-
dispatched:
|
|
4628
|
-
settled:
|
|
4629
|
-
inputMode:
|
|
4630
|
-
result:
|
|
4631
|
-
session:
|
|
5490
|
+
since: z16.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
|
|
5491
|
+
dispatched: z16.boolean(),
|
|
5492
|
+
settled: z16.boolean().nullable(),
|
|
5493
|
+
inputMode: z16.string(),
|
|
5494
|
+
result: z16.unknown().optional(),
|
|
5495
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4632
5496
|
},
|
|
4633
5497
|
handler: async (deps, args) => {
|
|
4634
|
-
const session = deps.sessions.resolve(
|
|
5498
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4635
5499
|
const paused = pausedShortCircuit(session);
|
|
4636
5500
|
if (paused !== void 0)
|
|
4637
5501
|
return paused;
|
|
4638
5502
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4639
5503
|
const since = session.elapsed();
|
|
4640
5504
|
session.markActCursor(since);
|
|
4641
|
-
const ref =
|
|
4642
|
-
const action =
|
|
5505
|
+
const ref = asString(args["ref"]) ?? "";
|
|
5506
|
+
const action = asString(args["action"]) ?? "";
|
|
4643
5507
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
4644
5508
|
if (real.result !== void 0) {
|
|
4645
5509
|
if (deps.recordings.active().length > 0) {
|
|
@@ -4665,7 +5529,7 @@ var TOOLS = [
|
|
|
4665
5529
|
if (deps.recordings.active().length > 0) {
|
|
4666
5530
|
deps.recordings.capture(compileActStep(args, result.result));
|
|
4667
5531
|
}
|
|
4668
|
-
const r =
|
|
5532
|
+
const r = asRecord(result.result);
|
|
4669
5533
|
return withControl(session, {
|
|
4670
5534
|
since,
|
|
4671
5535
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -4684,17 +5548,17 @@ var TOOLS = [
|
|
|
4684
5548
|
name: IrisTool.ACT_SEQUENCE,
|
|
4685
5549
|
description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
|
|
4686
5550
|
inputSchema: {
|
|
4687
|
-
steps:
|
|
5551
|
+
steps: z16.array(z16.record(z16.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call; put confirmDangerous:true in a destructive step args object."),
|
|
4688
5552
|
...sessionIdShape6
|
|
4689
5553
|
},
|
|
4690
5554
|
outputSchema: {
|
|
4691
|
-
since:
|
|
4692
|
-
dispatched:
|
|
4693
|
-
result:
|
|
4694
|
-
session:
|
|
5555
|
+
since: z16.number(),
|
|
5556
|
+
dispatched: z16.boolean(),
|
|
5557
|
+
result: z16.unknown().optional(),
|
|
5558
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4695
5559
|
},
|
|
4696
5560
|
handler: async (deps, args) => {
|
|
4697
|
-
const session = deps.sessions.resolve(
|
|
5561
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4698
5562
|
const paused = pausedShortCircuit(session);
|
|
4699
5563
|
if (paused !== void 0)
|
|
4700
5564
|
return paused;
|
|
@@ -4706,7 +5570,7 @@ var TOOLS = [
|
|
|
4706
5570
|
if (deps.recordings.active().length > 0) {
|
|
4707
5571
|
deps.recordings.capture(compileSequenceStep(args, result.result));
|
|
4708
5572
|
}
|
|
4709
|
-
const r =
|
|
5573
|
+
const r = asRecord(result.result);
|
|
4710
5574
|
return withControl(session, {
|
|
4711
5575
|
since,
|
|
4712
5576
|
dispatched: r["count"] !== void 0,
|
|
@@ -4717,34 +5581,34 @@ var TOOLS = [
|
|
|
4717
5581
|
},
|
|
4718
5582
|
{
|
|
4719
5583
|
name: IrisTool.ACT_AND_WAIT,
|
|
4720
|
-
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
5584
|
+
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Omit `until` to wait for the page to settle (network + DOM idle) \u2014 use this instead of a fixed sleep. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
4721
5585
|
inputSchema: {
|
|
4722
|
-
ref:
|
|
4723
|
-
action:
|
|
4724
|
-
args:
|
|
4725
|
-
until: PredicateSchema.describe(
|
|
4726
|
-
timeout_ms:
|
|
4727
|
-
refuseWhenThrottled:
|
|
5586
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query."),
|
|
5587
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5588
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { confirmDangerous: true } for a potentially destructive control."),
|
|
5589
|
+
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as iris_assert). OMIT to wait for the page to SETTLE \u2014 network + DOM idle \u2014 the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
5590
|
+
timeout_ms: z16.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
|
|
5591
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
|
|
4728
5592
|
...sessionIdShape6
|
|
4729
5593
|
},
|
|
4730
5594
|
outputSchema: {
|
|
4731
|
-
effect:
|
|
4732
|
-
verdict:
|
|
4733
|
-
pass:
|
|
4734
|
-
evidence:
|
|
4735
|
-
failureReason:
|
|
5595
|
+
effect: z16.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
|
|
5596
|
+
verdict: z16.object({
|
|
5597
|
+
pass: z16.boolean(),
|
|
5598
|
+
evidence: z16.unknown().optional(),
|
|
5599
|
+
failureReason: z16.string().optional()
|
|
4736
5600
|
}),
|
|
4737
|
-
trace:
|
|
4738
|
-
session:
|
|
5601
|
+
trace: z16.unknown().describe("Reaction report (same shape as iris_observe summary)."),
|
|
5602
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4739
5603
|
},
|
|
4740
5604
|
handler: async (deps, args) => {
|
|
4741
|
-
const session = deps.sessions.resolve(
|
|
5605
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4742
5606
|
const paused = pausedShortCircuit(session);
|
|
4743
5607
|
if (paused !== void 0)
|
|
4744
5608
|
return paused;
|
|
4745
5609
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4746
|
-
const until = PredicateSchema.parse(args["until"]);
|
|
4747
|
-
const timeout =
|
|
5610
|
+
const until = args["until"] !== void 0 ? PredicateSchema.parse(args["until"]) : { kind: "settled" };
|
|
5611
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 4e3;
|
|
4748
5612
|
const since = session.elapsed();
|
|
4749
5613
|
session.markActCursor(since);
|
|
4750
5614
|
const actResult = await session.command(IrisCommand.ACT, {
|
|
@@ -4768,40 +5632,41 @@ var TOOLS = [
|
|
|
4768
5632
|
name: IrisTool.OBSERVE,
|
|
4769
5633
|
description: "Return the timeline of everything the app did in a window (DOM/network/route/console/animation/signal), with a summary. Use after an action. Pass `max_events` to cap the timeline to the most recent N (older events are dropped and counted in cost.droppedOldest). Every result carries a `cost:{events,bytes}` hint so you can self-budget your next call.",
|
|
4770
5634
|
inputSchema: {
|
|
4771
|
-
window_ms:
|
|
4772
|
-
since:
|
|
4773
|
-
filters:
|
|
4774
|
-
max_events:
|
|
5635
|
+
window_ms: z16.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
|
|
5636
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
|
|
5637
|
+
filters: z16.array(z16.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
|
|
5638
|
+
max_events: z16.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
|
|
4775
5639
|
...sessionIdShape6
|
|
4776
5640
|
},
|
|
4777
5641
|
outputSchema: {
|
|
4778
|
-
events:
|
|
4779
|
-
summary:
|
|
4780
|
-
total:
|
|
4781
|
-
network:
|
|
4782
|
-
domAdded:
|
|
4783
|
-
domRemoved:
|
|
4784
|
-
domChanged:
|
|
4785
|
-
routeChanges:
|
|
4786
|
-
consoleErrors:
|
|
4787
|
-
animations:
|
|
4788
|
-
signals:
|
|
5642
|
+
events: z16.array(z16.unknown()),
|
|
5643
|
+
summary: z16.object({
|
|
5644
|
+
total: z16.number(),
|
|
5645
|
+
network: z16.number(),
|
|
5646
|
+
domAdded: z16.number(),
|
|
5647
|
+
domRemoved: z16.number(),
|
|
5648
|
+
domChanged: z16.number(),
|
|
5649
|
+
routeChanges: z16.number(),
|
|
5650
|
+
consoleErrors: z16.number(),
|
|
5651
|
+
animations: z16.number(),
|
|
5652
|
+
signals: z16.number()
|
|
4789
5653
|
}),
|
|
4790
|
-
cost:
|
|
4791
|
-
events:
|
|
4792
|
-
bytes:
|
|
4793
|
-
droppedOldest:
|
|
5654
|
+
cost: z16.object({
|
|
5655
|
+
events: z16.number(),
|
|
5656
|
+
bytes: z16.number(),
|
|
5657
|
+
droppedOldest: z16.number().optional(),
|
|
5658
|
+
recommendation: z16.string().optional().describe("Present when the timeline is large \u2014 scope your next call (filters/max_events).")
|
|
4794
5659
|
}),
|
|
4795
|
-
session:
|
|
5660
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4796
5661
|
},
|
|
4797
5662
|
handler: (deps, args) => {
|
|
4798
|
-
const session = deps.sessions.resolve(
|
|
4799
|
-
const since =
|
|
4800
|
-
const windowMs =
|
|
5663
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5664
|
+
const since = asNumber(args["since"]);
|
|
5665
|
+
const windowMs = asNumber(args["window_ms"]) ?? 2e3;
|
|
4801
5666
|
const events = since !== void 0 ? session.eventsSince(since) : session.eventsInWindow(windowMs);
|
|
4802
5667
|
const filters = Array.isArray(args["filters"]) ? args["filters"] : void 0;
|
|
4803
5668
|
const filtered = filters === void 0 ? events : events.filter((e) => filters.includes(e.type));
|
|
4804
|
-
const { events: budgeted, droppedOldest } = applyEventBudget(filtered,
|
|
5669
|
+
const { events: budgeted, droppedOldest } = applyEventBudget(filtered, asNumber(args["max_events"]));
|
|
4805
5670
|
const report = buildReactionReport(budgeted, windowMs);
|
|
4806
5671
|
return Promise.resolve(withControl(session, {
|
|
4807
5672
|
...report,
|
|
@@ -4814,99 +5679,113 @@ var TOOLS = [
|
|
|
4814
5679
|
name: IrisTool.WAIT_FOR,
|
|
4815
5680
|
description: "Block until a predicate is satisfied (or already true in the recent buffer), else time out. Returns matching evidence or a near-miss diagnosis. By default it only counts events since your last act, so a signal buffered BEFORE the action can never fake a pass; pass `since` (an observe/act cursor) to widen or narrow that window explicitly.",
|
|
4816
5681
|
inputSchema: {
|
|
4817
|
-
predicate: PredicateSchema.describe(
|
|
4818
|
-
timeout_ms:
|
|
4819
|
-
since:
|
|
5682
|
+
predicate: PredicateSchema.describe('Predicate to wait for: { signal }, { net }, { element }, { kind: "settled", quietMs } (deterministic network + DOM idle \u2014 prefer this over a fixed sleep), or a combination via allOf/anyOf.'),
|
|
5683
|
+
timeout_ms: z16.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
|
|
5684
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
|
|
4820
5685
|
...sessionIdShape6
|
|
4821
5686
|
},
|
|
4822
5687
|
outputSchema: {
|
|
4823
|
-
pass:
|
|
4824
|
-
evidence:
|
|
4825
|
-
failureReason:
|
|
4826
|
-
session:
|
|
5688
|
+
pass: z16.boolean(),
|
|
5689
|
+
evidence: z16.unknown().optional(),
|
|
5690
|
+
failureReason: z16.string().optional(),
|
|
5691
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4827
5692
|
},
|
|
4828
5693
|
handler: async (deps, args) => {
|
|
4829
|
-
const session = deps.sessions.resolve(
|
|
5694
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4830
5695
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4831
|
-
const since =
|
|
4832
|
-
const verdict = await waitForPredicate(session, predicate,
|
|
5696
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
5697
|
+
const verdict = await waitForPredicate(session, predicate, asNumber(args["timeout_ms"]) ?? 4e3, since);
|
|
4833
5698
|
return withControl(session, { ...verdict, ...healthEnvelope(session) });
|
|
4834
5699
|
}
|
|
4835
5700
|
},
|
|
4836
5701
|
{
|
|
4837
5702
|
name: IrisTool.ASSERT,
|
|
4838
|
-
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
5703
|
+
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence \u2014 a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
4839
5704
|
inputSchema: {
|
|
4840
5705
|
predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
|
|
4841
|
-
timeout_ms:
|
|
4842
|
-
since:
|
|
5706
|
+
timeout_ms: z16.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
|
|
5707
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
|
|
4843
5708
|
...sessionIdShape6
|
|
4844
5709
|
},
|
|
4845
5710
|
outputSchema: {
|
|
4846
|
-
pass:
|
|
4847
|
-
evidence:
|
|
4848
|
-
failureReason:
|
|
4849
|
-
|
|
5711
|
+
pass: z16.boolean(),
|
|
5712
|
+
evidence: z16.unknown().optional(),
|
|
5713
|
+
failureReason: z16.string().optional(),
|
|
5714
|
+
advice: z16.string().optional().describe("Present on a PASSING presence-only assertion \u2014 nudges toward a consequence."),
|
|
5715
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4850
5716
|
},
|
|
4851
5717
|
handler: async (deps, args) => {
|
|
4852
|
-
const session = deps.sessions.resolve(
|
|
5718
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4853
5719
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4854
|
-
const timeout =
|
|
4855
|
-
const since =
|
|
5720
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 0;
|
|
5721
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
4856
5722
|
const verdict = timeout > 0 ? await waitForPredicate(session, predicate, timeout, since) : await evaluatePredicate(session, predicate, since);
|
|
4857
|
-
|
|
5723
|
+
const advice = verdict.pass && isPresenceOnlyAssertion(predicate) ? { advice: PRESENCE_ONLY_ADVICE } : {};
|
|
5724
|
+
return withControl(session, { ...verdict, ...advice, ...healthEnvelope(session) });
|
|
4858
5725
|
}
|
|
4859
5726
|
},
|
|
4860
5727
|
{
|
|
4861
5728
|
name: IrisTool.NETWORK,
|
|
4862
5729
|
description: 'Filtered list of network calls. Fast path for "did POST /x return 200?". A zero-match filter returns a `hint` { totalInWindow, present[] } of the calls that DID fire, so a miss is diagnosable.',
|
|
4863
5730
|
inputSchema: {
|
|
4864
|
-
since:
|
|
4865
|
-
method:
|
|
4866
|
-
urlContains:
|
|
4867
|
-
status:
|
|
5731
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
|
|
5732
|
+
method: z16.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
|
|
5733
|
+
urlContains: z16.string().optional().describe("Substring that the request URL must contain."),
|
|
5734
|
+
status: z16.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
|
|
5735
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) \u2014 cuts tokens on a wide window."),
|
|
4868
5736
|
...sessionIdShape6
|
|
4869
5737
|
},
|
|
4870
5738
|
outputSchema: {
|
|
4871
|
-
calls:
|
|
4872
|
-
|
|
5739
|
+
calls: z16.array(z16.unknown()),
|
|
5740
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5741
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5742
|
+
hint: z16.object({ totalInWindow: z16.number(), present: z16.array(z16.string()) }).optional(),
|
|
5743
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4873
5744
|
},
|
|
4874
5745
|
handler: (deps, args) => {
|
|
4875
|
-
const session = deps.sessions.resolve(
|
|
4876
|
-
const since =
|
|
4877
|
-
const method =
|
|
4878
|
-
const urlContains =
|
|
4879
|
-
const status =
|
|
5746
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5747
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5748
|
+
const method = asString(args["method"]);
|
|
5749
|
+
const urlContains = asString(args["urlContains"]);
|
|
5750
|
+
const status = asNumber(args["status"]);
|
|
5751
|
+
const limit = asNumber(args["limit"]);
|
|
4880
5752
|
const allNet = session.eventsSince(since).filter((e) => e.type === EventType.NET_REQUEST);
|
|
4881
|
-
const
|
|
4882
|
-
if (
|
|
4883
|
-
return Promise.resolve({ calls, hint: netEmptyHint(allNet) });
|
|
5753
|
+
const matched = allNet.filter((e) => matchNet(e, method, urlContains, status));
|
|
5754
|
+
if (matched.length === 0 && allNet.length > 0) {
|
|
5755
|
+
return Promise.resolve(withSizeCost({ calls: matched, hint: netEmptyHint(allNet) }));
|
|
4884
5756
|
}
|
|
4885
|
-
|
|
5757
|
+
const { events: calls, droppedOldest } = applyEventBudget(matched, limit);
|
|
5758
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { calls, total: matched.length, droppedOldest } : { calls }));
|
|
4886
5759
|
}
|
|
4887
5760
|
},
|
|
4888
5761
|
{
|
|
4889
5762
|
name: IrisTool.CONSOLE,
|
|
4890
5763
|
description: 'Console/error log. Fast path for "were there any errors during this flow?". When a level filter matches nothing, returns a `hint` { totalInWindow, byLevel } so 0 errors is distinguishable from a silent page.',
|
|
4891
5764
|
inputSchema: {
|
|
4892
|
-
level:
|
|
4893
|
-
since:
|
|
5765
|
+
level: z16.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
|
|
5766
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
|
|
5767
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching entries (older are dropped and counted in droppedOldest) \u2014 cuts tokens when a page spams the console."),
|
|
4894
5768
|
...sessionIdShape6
|
|
4895
5769
|
},
|
|
4896
5770
|
outputSchema: {
|
|
4897
|
-
logs:
|
|
4898
|
-
|
|
5771
|
+
logs: z16.array(z16.unknown()),
|
|
5772
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5773
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5774
|
+
hint: z16.object({ totalInWindow: z16.number(), byLevel: z16.record(z16.number()) }).optional(),
|
|
5775
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4899
5776
|
},
|
|
4900
5777
|
handler: (deps, args) => {
|
|
4901
|
-
const session = deps.sessions.resolve(
|
|
4902
|
-
const since =
|
|
4903
|
-
const level =
|
|
5778
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5779
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5780
|
+
const level = asString(args["level"]);
|
|
5781
|
+
const limit = asNumber(args["limit"]);
|
|
4904
5782
|
const allConsole = session.eventsSince(since).filter(isConsoleEvent);
|
|
4905
|
-
const
|
|
4906
|
-
if (
|
|
4907
|
-
return Promise.resolve({ logs, hint: consoleEmptyHint(allConsole) });
|
|
5783
|
+
const matched = allConsole.filter((e) => matchConsole(e, level));
|
|
5784
|
+
if (matched.length === 0 && allConsole.length > 0) {
|
|
5785
|
+
return Promise.resolve(withSizeCost({ logs: matched, hint: consoleEmptyHint(allConsole) }));
|
|
4908
5786
|
}
|
|
4909
|
-
|
|
5787
|
+
const { events: logs, droppedOldest } = applyEventBudget(matched, limit);
|
|
5788
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { logs, total: matched.length, droppedOldest } : { logs }));
|
|
4910
5789
|
}
|
|
4911
5790
|
},
|
|
4912
5791
|
{
|
|
@@ -4914,24 +5793,24 @@ var TOOLS = [
|
|
|
4914
5793
|
description: "Currently running + recently completed animations with targets/timing.",
|
|
4915
5794
|
inputSchema: { ...sessionIdShape6 },
|
|
4916
5795
|
outputSchema: {
|
|
4917
|
-
animations:
|
|
5796
|
+
animations: z16.array(z16.unknown())
|
|
4918
5797
|
},
|
|
4919
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5798
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.ANIMATIONS, {})
|
|
4920
5799
|
},
|
|
4921
5800
|
{
|
|
4922
5801
|
name: IrisTool.BASELINE_SAVE,
|
|
4923
5802
|
description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
|
|
4924
5803
|
inputSchema: {
|
|
4925
|
-
name:
|
|
5804
|
+
name: z16.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
|
|
4926
5805
|
...sessionIdShape6
|
|
4927
5806
|
},
|
|
4928
5807
|
outputSchema: {
|
|
4929
|
-
baseline:
|
|
4930
|
-
lineCount:
|
|
5808
|
+
baseline: z16.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
|
|
5809
|
+
lineCount: z16.number()
|
|
4931
5810
|
},
|
|
4932
5811
|
handler: async (deps, args) => {
|
|
4933
|
-
const name =
|
|
4934
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5812
|
+
const name = asString(args["name"]) ?? "default";
|
|
5813
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4935
5814
|
deps.baselines.save({ name, lines, route });
|
|
4936
5815
|
return { baseline: name, lineCount: lines.length };
|
|
4937
5816
|
}
|
|
@@ -4941,7 +5820,7 @@ var TOOLS = [
|
|
|
4941
5820
|
description: "List saved baseline names.",
|
|
4942
5821
|
inputSchema: {},
|
|
4943
5822
|
outputSchema: {
|
|
4944
|
-
baselines:
|
|
5823
|
+
baselines: z16.array(z16.string())
|
|
4945
5824
|
},
|
|
4946
5825
|
handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
|
|
4947
5826
|
},
|
|
@@ -4949,23 +5828,23 @@ var TOOLS = [
|
|
|
4949
5828
|
name: IrisTool.DIFF,
|
|
4950
5829
|
description: 'Diff current semantic state vs a saved baseline: REMOVED/ADDED elements + console-error count. Call iris_baseline_list to list saved baselines, iris_baseline_save to create one. Pass `baseline` (name from iris_baseline_list). Answers "did anything silently go missing/break?".',
|
|
4951
5830
|
inputSchema: {
|
|
4952
|
-
baseline:
|
|
5831
|
+
baseline: z16.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
|
|
4953
5832
|
...sessionIdShape6
|
|
4954
5833
|
},
|
|
4955
5834
|
outputSchema: {
|
|
4956
|
-
baseline:
|
|
4957
|
-
removed:
|
|
4958
|
-
added:
|
|
4959
|
-
consoleErrors:
|
|
4960
|
-
routeChanged:
|
|
5835
|
+
baseline: z16.string(),
|
|
5836
|
+
removed: z16.array(z16.string()),
|
|
5837
|
+
added: z16.array(z16.string()),
|
|
5838
|
+
consoleErrors: z16.number(),
|
|
5839
|
+
routeChanged: z16.boolean()
|
|
4961
5840
|
},
|
|
4962
5841
|
handler: async (deps, args) => {
|
|
4963
|
-
const name =
|
|
5842
|
+
const name = asString(args["baseline"]) ?? "default";
|
|
4964
5843
|
const base = deps.baselines.get(name);
|
|
4965
5844
|
if (base === void 0)
|
|
4966
5845
|
throw new Error(`no baseline named '${name}'`);
|
|
4967
|
-
const session = deps.sessions.resolve(
|
|
4968
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5846
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5847
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4969
5848
|
const { removed, added } = diffLines(base.lines, lines);
|
|
4970
5849
|
const consoleErrors = session.eventsSince(0).filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
4971
5850
|
return { baseline: name, removed, added, consoleErrors, routeChanged: base.route !== route };
|
|
@@ -4975,16 +5854,16 @@ var TOOLS = [
|
|
|
4975
5854
|
name: IrisTool.RECORD_START,
|
|
4976
5855
|
description: "Start recording the event timeline under a name (for replay / a flow report).",
|
|
4977
5856
|
inputSchema: {
|
|
4978
|
-
recordingName:
|
|
5857
|
+
recordingName: z16.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
|
|
4979
5858
|
...sessionIdShape6
|
|
4980
5859
|
},
|
|
4981
5860
|
outputSchema: {
|
|
4982
|
-
recordingName:
|
|
4983
|
-
since:
|
|
5861
|
+
recordingName: z16.string(),
|
|
5862
|
+
since: z16.number()
|
|
4984
5863
|
},
|
|
4985
5864
|
handler: (deps, args) => {
|
|
4986
|
-
const session = deps.sessions.resolve(
|
|
4987
|
-
const name =
|
|
5865
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5866
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4988
5867
|
const cursor = session.elapsed();
|
|
4989
5868
|
deps.recordings.start(name, cursor);
|
|
4990
5869
|
return Promise.resolve({ recordingName: name, since: cursor });
|
|
@@ -4994,17 +5873,17 @@ var TOOLS = [
|
|
|
4994
5873
|
name: IrisTool.RECORD_STOP,
|
|
4995
5874
|
description: "Stop the recording identified by `recordingName` and return both the reaction report for the span and a compiled, replayable { program: { version, steps:[{tool,args,stable}] } } of the agent acts captured during it.",
|
|
4996
5875
|
inputSchema: {
|
|
4997
|
-
recordingName:
|
|
5876
|
+
recordingName: z16.string().describe("Identifier of an active recording started with iris_record_start."),
|
|
4998
5877
|
...sessionIdShape6
|
|
4999
5878
|
},
|
|
5000
5879
|
outputSchema: {
|
|
5001
|
-
recordingName:
|
|
5002
|
-
program:
|
|
5003
|
-
warning:
|
|
5880
|
+
recordingName: z16.string(),
|
|
5881
|
+
program: z16.unknown(),
|
|
5882
|
+
warning: z16.string().optional()
|
|
5004
5883
|
},
|
|
5005
5884
|
handler: (deps, args) => {
|
|
5006
|
-
const session = deps.sessions.resolve(
|
|
5007
|
-
const name =
|
|
5885
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5886
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5008
5887
|
const rec = deps.recordings.stop(name);
|
|
5009
5888
|
if (rec === void 0)
|
|
5010
5889
|
throw new Error(`no active recording named '${name}'`);
|
|
@@ -5030,29 +5909,30 @@ var TOOLS = [
|
|
|
5030
5909
|
},
|
|
5031
5910
|
{
|
|
5032
5911
|
name: IrisTool.REPLAY,
|
|
5033
|
-
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
5912
|
+
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Destructive controls require confirmDangerous:true on every replay; confirmation is never persisted. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
5034
5913
|
inputSchema: {
|
|
5035
|
-
recordingName:
|
|
5914
|
+
recordingName: z16.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
|
|
5915
|
+
confirmDangerous: z16.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
5036
5916
|
...sessionIdShape6
|
|
5037
5917
|
},
|
|
5038
5918
|
outputSchema: {
|
|
5039
|
-
recordingName:
|
|
5040
|
-
ok:
|
|
5041
|
-
steps:
|
|
5042
|
-
tool:
|
|
5043
|
-
ok:
|
|
5044
|
-
error:
|
|
5045
|
-
note:
|
|
5919
|
+
recordingName: z16.string(),
|
|
5920
|
+
ok: z16.boolean(),
|
|
5921
|
+
steps: z16.array(z16.object({
|
|
5922
|
+
tool: z16.string(),
|
|
5923
|
+
ok: z16.boolean(),
|
|
5924
|
+
error: z16.string().optional(),
|
|
5925
|
+
note: z16.string().optional()
|
|
5046
5926
|
}))
|
|
5047
5927
|
},
|
|
5048
5928
|
handler: async (deps, args) => {
|
|
5049
|
-
const name =
|
|
5929
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5050
5930
|
const program = deps.recordings.getCompiled(name);
|
|
5051
5931
|
if (program === void 0)
|
|
5052
5932
|
throw new Error(`no compiled recording named '${name}'`);
|
|
5053
|
-
const session = deps.sessions.resolve(
|
|
5933
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5054
5934
|
const since = session.elapsed();
|
|
5055
|
-
const steps = await replayProgram(session, program);
|
|
5935
|
+
const steps = await replayProgram(session, program, args["confirmDangerous"] === true);
|
|
5056
5936
|
return { recordingName: name, since, steps, ok: steps.every((s) => s.ok) };
|
|
5057
5937
|
}
|
|
5058
5938
|
},
|
|
@@ -5060,13 +5940,13 @@ var TOOLS = [
|
|
|
5060
5940
|
name: IrisTool.NARRATE,
|
|
5061
5941
|
description: "Narrate your intent on the page (presenter HUD) so the human watching sees what you are about to do and why. Use a short sentence before a meaningful action.",
|
|
5062
5942
|
inputSchema: {
|
|
5063
|
-
text:
|
|
5064
|
-
level:
|
|
5943
|
+
text: z16.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
|
|
5944
|
+
level: z16.string().optional().describe("Display severity: info | warn | error. Default: info."),
|
|
5065
5945
|
...sessionIdShape6
|
|
5066
5946
|
},
|
|
5067
|
-
outputSchema: { ok:
|
|
5947
|
+
outputSchema: { ok: z16.boolean() },
|
|
5068
5948
|
handler: async (deps, args) => {
|
|
5069
|
-
const result = await commandOrThrow3(deps,
|
|
5949
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.NARRATE, {
|
|
5070
5950
|
text: args["text"],
|
|
5071
5951
|
level: args["level"]
|
|
5072
5952
|
});
|
|
@@ -5077,16 +5957,16 @@ var TOOLS = [
|
|
|
5077
5957
|
name: IrisTool.CLOCK,
|
|
5078
5958
|
description: "Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically.",
|
|
5079
5959
|
inputSchema: {
|
|
5080
|
-
freeze:
|
|
5081
|
-
advanceMs:
|
|
5082
|
-
reset:
|
|
5960
|
+
freeze: z16.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
|
|
5961
|
+
advanceMs: z16.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
|
|
5962
|
+
reset: z16.boolean().optional().describe("Restore the real clock."),
|
|
5083
5963
|
...sessionIdShape6
|
|
5084
5964
|
},
|
|
5085
5965
|
outputSchema: {
|
|
5086
|
-
ok:
|
|
5087
|
-
elapsed:
|
|
5966
|
+
ok: z16.boolean().optional(),
|
|
5967
|
+
elapsed: z16.number().optional()
|
|
5088
5968
|
},
|
|
5089
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5969
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.CLOCK, {
|
|
5090
5970
|
freeze: args["freeze"],
|
|
5091
5971
|
advanceMs: args["advanceMs"],
|
|
5092
5972
|
reset: args["reset"]
|
|
@@ -5096,27 +5976,27 @@ var TOOLS = [
|
|
|
5096
5976
|
name: IrisTool.STATE,
|
|
5097
5977
|
description: "Read live framework state without the app pre-broadcasting it. PREFERRED/RELIABLE: `store` reads a registered store (e.g. 'workspace'); omit `store` to read all stores. To avoid paying for a huge store, scope the read: `path` extracts a dot-path sub-tree (e.g. 'captionCache.v3', with numeric array indices), and `depth` collapses anything deeper than N levels to a size marker. A wrong `path` returns { found:false, availableKeys } so it is diagnosable. `ref` attempts a best-effort read of the nearest React component's hook state and is BOUNDED \u2014 on failure it returns component: { ok: false, reason: 'component-state-unavailable' }. Without path/depth: returns { stores, storeNames, component? }.",
|
|
5098
5978
|
inputSchema: {
|
|
5099
|
-
ref:
|
|
5100
|
-
store:
|
|
5101
|
-
path:
|
|
5102
|
-
depth:
|
|
5979
|
+
ref: z16.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
|
|
5980
|
+
store: z16.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
|
|
5981
|
+
path: z16.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
5982
|
+
depth: z16.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
|
|
5103
5983
|
...sessionIdShape6
|
|
5104
5984
|
},
|
|
5105
5985
|
outputSchema: {
|
|
5106
|
-
stores:
|
|
5107
|
-
storeNames:
|
|
5108
|
-
found:
|
|
5109
|
-
value:
|
|
5110
|
-
component:
|
|
5986
|
+
stores: z16.record(z16.unknown()).optional(),
|
|
5987
|
+
storeNames: z16.array(z16.string()).optional(),
|
|
5988
|
+
found: z16.boolean().optional(),
|
|
5989
|
+
value: z16.unknown().optional(),
|
|
5990
|
+
component: z16.object({ ok: z16.boolean(), reason: z16.string().optional(), state: z16.unknown().optional() }).optional()
|
|
5111
5991
|
},
|
|
5112
5992
|
handler: async (deps, args) => {
|
|
5113
|
-
const store =
|
|
5114
|
-
const result = await commandOrThrow3(deps,
|
|
5993
|
+
const store = asString(args["store"]);
|
|
5994
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.STATE_READ, {
|
|
5115
5995
|
ref: args["ref"],
|
|
5116
5996
|
store
|
|
5117
5997
|
});
|
|
5118
|
-
const path =
|
|
5119
|
-
const depth =
|
|
5998
|
+
const path = asString(args["path"]);
|
|
5999
|
+
const depth = asNumber(args["depth"]);
|
|
5120
6000
|
if (path === void 0 && depth === void 0)
|
|
5121
6001
|
return result;
|
|
5122
6002
|
const root = result;
|
|
@@ -5136,16 +6016,16 @@ var TOOLS = [
|
|
|
5136
6016
|
name: IrisTool.EXPLORE,
|
|
5137
6017
|
description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
|
|
5138
6018
|
inputSchema: {
|
|
5139
|
-
scope:
|
|
6019
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
|
|
5140
6020
|
...sessionIdShape6
|
|
5141
6021
|
},
|
|
5142
6022
|
outputSchema: {
|
|
5143
|
-
interactive:
|
|
5144
|
-
consoleErrors:
|
|
5145
|
-
hint:
|
|
6023
|
+
interactive: z16.array(z16.unknown()),
|
|
6024
|
+
consoleErrors: z16.number(),
|
|
6025
|
+
hint: z16.string()
|
|
5146
6026
|
},
|
|
5147
6027
|
handler: async (deps, args) => {
|
|
5148
|
-
const session = deps.sessions.resolve(
|
|
6028
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5149
6029
|
const result = await session.command(IrisCommand.SNAPSHOT, {
|
|
5150
6030
|
mode: SnapshotMode.INTERACTIVE,
|
|
5151
6031
|
scope: args["scope"]
|
|
@@ -5163,6 +6043,7 @@ var TOOLS = [
|
|
|
5163
6043
|
},
|
|
5164
6044
|
// iris_capabilities (live | fromDisk) + iris_contract_save. See contract-tools.ts.
|
|
5165
6045
|
...CONTRACT_TOOLS,
|
|
6046
|
+
...DOMAIN_TOOLS,
|
|
5166
6047
|
// iris_flow_save / iris_flow_list / iris_flow_load. See flow-tools.ts.
|
|
5167
6048
|
...FLOW_TOOLS,
|
|
5168
6049
|
// iris_project (read history + diff-vs-last) / iris_run_record. See project-tools.ts.
|
|
@@ -5316,7 +6197,7 @@ async function runTool(tool, deps, args) {
|
|
|
5316
6197
|
return result;
|
|
5317
6198
|
if (!isPlainObject(result) || "session" in result)
|
|
5318
6199
|
return result;
|
|
5319
|
-
const session = deps.sessions.resolve(
|
|
6200
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5320
6201
|
const envelope = { ...healthEnvelope(session) };
|
|
5321
6202
|
const lease = session.takeSessionLease();
|
|
5322
6203
|
if (lease !== void 0)
|
|
@@ -5486,7 +6367,17 @@ function isRunning(port) {
|
|
|
5486
6367
|
// ../server/dist/index.js
|
|
5487
6368
|
async function start(options = {}) {
|
|
5488
6369
|
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
5489
|
-
const
|
|
6370
|
+
const envToken = process.env["IRIS_TOKEN"];
|
|
6371
|
+
const envOrigins = process.env["IRIS_ALLOWED_ORIGINS"];
|
|
6372
|
+
const host = options.host ?? process.env["IRIS_HOST"];
|
|
6373
|
+
const token = options.token ?? (envToken !== void 0 && envToken.length > 0 ? envToken : void 0);
|
|
6374
|
+
const allowedOrigins = options.allowedOrigins ?? envOrigins?.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
6375
|
+
const bridge = new Bridge({
|
|
6376
|
+
port,
|
|
6377
|
+
...host === void 0 ? {} : { host },
|
|
6378
|
+
...token === void 0 ? {} : { token },
|
|
6379
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins }
|
|
6380
|
+
});
|
|
5490
6381
|
const reaper = new SessionReaper(bridge.sessions);
|
|
5491
6382
|
reaper.start();
|
|
5492
6383
|
const baselines = new BaselineStore();
|
|
@@ -5622,6 +6513,7 @@ export {
|
|
|
5622
6513
|
CORE_TOOL_NAMES,
|
|
5623
6514
|
CdpRealInputProvider,
|
|
5624
6515
|
DriveError,
|
|
6516
|
+
FlowAssertionGrade,
|
|
5625
6517
|
FlowStore,
|
|
5626
6518
|
IrisTool,
|
|
5627
6519
|
LaunchedRealInputProvider,
|
|
@@ -5640,15 +6532,19 @@ export {
|
|
|
5640
6532
|
TOOL_PROFILE_ENV,
|
|
5641
6533
|
UNKNOWN_TOOL_ERROR,
|
|
5642
6534
|
VisualStore,
|
|
6535
|
+
assertSuccess,
|
|
5643
6536
|
baselinePath,
|
|
5644
6537
|
boxCenter,
|
|
6538
|
+
buildDomainModel,
|
|
5645
6539
|
buildReactionReport,
|
|
5646
6540
|
buildSessionRecommendation,
|
|
6541
|
+
classifyFlowAssertions,
|
|
5647
6542
|
crawl,
|
|
5648
6543
|
createNodeFileSystem,
|
|
5649
6544
|
createToolInvoker,
|
|
5650
6545
|
diffLines,
|
|
5651
6546
|
diffPng,
|
|
6547
|
+
dynamicTestids,
|
|
5652
6548
|
ensureIrisDir,
|
|
5653
6549
|
evaluatePredicate,
|
|
5654
6550
|
filterTools,
|
|
@@ -5671,6 +6567,8 @@ export {
|
|
|
5671
6567
|
scrollToFind,
|
|
5672
6568
|
start,
|
|
5673
6569
|
startDaemon,
|
|
6570
|
+
successLabel,
|
|
6571
|
+
successToPredicate,
|
|
5674
6572
|
waitForPredicate,
|
|
5675
6573
|
writeContract,
|
|
5676
6574
|
writePid
|