@qatonic_innovations/qaios 0.3.2 → 0.4.1

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/index.js CHANGED
@@ -3,13 +3,14 @@ import { useApp, useInput, Box, Text } from 'ink';
3
3
  import { useState } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
5
  import { realpathSync, readFileSync, existsSync, rmSync, mkdirSync, writeFileSync, statSync, mkdtempSync, copyFileSync, readdirSync } from 'fs';
6
- import path12 from 'path';
6
+ import path15 from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { Command, InvalidArgumentError } from 'commander';
9
- import { createHash } from 'crypto';
10
- import { createRequire } from 'module';
11
9
  import { z, ZodError } from 'zod';
12
10
  import { monotonicFactory } from 'ulid';
11
+ import { gzipSync, gunzipSync } from 'zlib';
12
+ import { createHash } from 'crypto';
13
+ import { createRequire } from 'module';
13
14
  import Anthropic from '@anthropic-ai/sdk';
14
15
  import OpenAI from 'openai';
15
16
  import { zodToJsonSchema } from 'zod-to-json-schema';
@@ -384,6 +385,13 @@ var AuditEvent = z.enum([
384
385
  "execution.started",
385
386
  "execution.completed",
386
387
  "execution.failed",
388
+ // xray (network capture) — hash-chained like everything else.
389
+ "xray.capture",
390
+ // a net_run + its requests were persisted
391
+ "xray.diff",
392
+ // a run was diffed against a baseline
393
+ "xray.baseline.set",
394
+ // a baseline was pinned/cleared
387
395
  "audit.verified",
388
396
  "system.config_changed"
389
397
  ]);
@@ -587,6 +595,10 @@ var ResultClassification = z.object({
587
595
  "environmental",
588
596
  "locator_broken",
589
597
  "expectation_outdated",
598
+ // The UI behaved but the BACKEND diverged from its baseline contract — a
599
+ // 4xx/5xx, status change, or response-shape change on a call the test made.
600
+ // Sourced from xray's network diff (v0.4). Routes to create_defect.
601
+ "backend_divergence",
590
602
  "unknown"
591
603
  ]),
592
604
  confidence: z.number().min(0).max(1),
@@ -878,8 +890,11 @@ var QaiosConfig = z.object({
878
890
  }).default({}),
879
891
  // Lower-bounded: a 0/negative cap would silently disable enforcement.
880
892
  maxRetriesPerSkill: z.number().int().nonnegative().default(2),
881
- maxLlmCallsPerWorkflow: z.number().int().positive().default(15),
882
- costAlertThresholdUsdCents: z.number().int().positive().default(50)
893
+ // Raised from 15/50¢ after real-app e2e — a real OpenAPI spec or complex
894
+ // site legitimately costs ~$0.70 to generate from, so the old cap aborted
895
+ // valid single-feature generation. Still a runaway-loop guard.
896
+ maxLlmCallsPerWorkflow: z.number().int().positive().default(20),
897
+ costAlertThresholdUsdCents: z.number().int().positive().default(100)
883
898
  }).default({}),
884
899
  testing: z.object({
885
900
  framework: z.literal("playwright").default("playwright"),
@@ -925,6 +940,31 @@ var QaiosConfig = z.object({
925
940
  telemetry: z.object({
926
941
  enabled: z.boolean().default(false),
927
942
  endpoint: z.string().default("https://telemetry.qaios.dev/v1")
943
+ }).default({}),
944
+ // Network capture + API-divergence (`qaios run --xray`, `qaios explore`).
945
+ // Safe to `.default({})`: every field has a defaulted scalar/array (no
946
+ // optional probe like `app`), so `qaios init` serializes a complete block,
947
+ // not a stub. `enabled: false` keeps capture opt-in for v0.4 — the `--xray`
948
+ // flag turns it on per-run regardless. See QAIOS xray spec §9.
949
+ xray: z.object({
950
+ enabled: z.boolean().default(false),
951
+ level: z.enum(["off", "headers", "summary", "full"]).default("summary"),
952
+ // Quiet window after an action with no new request before a step's
953
+ // network window closes (capped at 15s by the causality engine).
954
+ quiescenceMs: z.number().int().positive().default(500),
955
+ // Response bodies above this are hashed/shape-summarized but not stored.
956
+ // Spec: 16 at summary, 256 at full — the resolver bumps it when level=full.
957
+ bodyMaxKb: z.number().int().positive().default(16),
958
+ // Extends the built-in volatile-query-param denylist (t, ts, nonce, …).
959
+ volatileParams: z.array(z.string()).default([]),
960
+ // JSONPaths redacted (→ "<redacted>") BEFORE hashing/storage, so secrets
961
+ // never touch disk. The header denylist (authorization, cookie, …) is
962
+ // always applied regardless.
963
+ redact: z.array(z.string()).default([]),
964
+ // Glob hosts whose traffic is dropped at capture time (analytics noise).
965
+ ignoreHosts: z.array(z.string()).default([]),
966
+ // Raw rows retained per (test, browser) beyond baselines; older pruned.
967
+ retainRuns: z.number().int().positive().default(10)
928
968
  }).default({})
929
969
  });
930
970
  var RunStatus = z.enum(["running", "passed", "failed", "mixed", "errored", "flaky"]);
@@ -964,6 +1004,101 @@ z.object({
964
1004
  // serialized to value_json
965
1005
  updatedAt: z.string()
966
1006
  });
1007
+ var XrayTier = z.enum(["A", "B"]);
1008
+ var XrayCaptureLevel = z.enum(["off", "headers", "summary", "full"]);
1009
+ var XrayAttribution = z.enum([
1010
+ "initiator",
1011
+ // Tier-A initiator stack tied into a step window
1012
+ "temporal",
1013
+ // started inside exactly one open step window
1014
+ "temporal_ambiguous",
1015
+ // inside multiple overlapping windows → most-recent
1016
+ "background"
1017
+ // no step window (polling/analytics/heartbeats)
1018
+ ]);
1019
+ var XrayTargetType = z.enum(["page", "iframe", "service_worker"]);
1020
+ var XrayBodyStatus = z.enum(["stored", "evicted", "truncated", "streaming", "skipped"]);
1021
+ var XrayRequestEvent = z.object({
1022
+ method: z.string(),
1023
+ url: z.string(),
1024
+ // HAR `_initiator` is coarse (type + optional url); no deep stack. Kept for
1025
+ // forward-compat with an opt-in initiator fixture; null for HAR capture.
1026
+ initiator: z.object({
1027
+ type: z.string(),
1028
+ // script|parser|preload|other|...
1029
+ url: z.string().nullable().default(null)
1030
+ }).nullable().default(null),
1031
+ status: z.number().int().nullable().default(null),
1032
+ errorText: z.string().nullable().default(null),
1033
+ // populated for failed entries
1034
+ mime: z.string().nullable().default(null),
1035
+ fromServiceWorker: z.boolean().default(false),
1036
+ fromDiskCache: z.boolean().default(false),
1037
+ // Raw bodies as captured (subject to level/MIME/size gates). The runtime
1038
+ // canonicalizes + redacts + hashes these; they never round-trip raw.
1039
+ reqBody: z.string().nullable().default(null),
1040
+ respBody: z.string().nullable().default(null),
1041
+ bodyStatus: XrayBodyStatus.default("skipped"),
1042
+ redirects: z.array(z.object({ url: z.string(), status: z.number().int() })).default([]),
1043
+ timings: z.record(z.string(), z.number()).default({}),
1044
+ // dns/connect/ttfb/total ms
1045
+ wallStart: z.number()
1046
+ // epoch ms — drives temporal attribution + display
1047
+ });
1048
+ z.object({
1049
+ stepId: z.string(),
1050
+ title: z.string(),
1051
+ category: z.string(),
1052
+ // 'pw:api' | 'test.step' | ...
1053
+ startWall: z.number(),
1054
+ // epoch ms
1055
+ endWall: z.number()
1056
+ // epoch ms
1057
+ });
1058
+ var NetRunRow = z.object({
1059
+ runId: z.string(),
1060
+ workflowId: z.string(),
1061
+ testId: z.string(),
1062
+ browser: z.string(),
1063
+ tier: XrayTier,
1064
+ captureLevel: XrayCaptureLevel,
1065
+ startedAt: z.string(),
1066
+ isGreen: z.boolean().default(false),
1067
+ isBaseline: z.boolean().default(false)
1068
+ });
1069
+ var NetRequestRow = z.object({
1070
+ id: z.string(),
1071
+ runId: z.string(),
1072
+ stepId: z.string().nullable(),
1073
+ attribution: XrayAttribution,
1074
+ targetType: XrayTargetType,
1075
+ method: z.string(),
1076
+ urlRaw: z.string(),
1077
+ urlTemplate: z.string(),
1078
+ gqlOperation: z.string().nullable(),
1079
+ identityKey: z.string(),
1080
+ status: z.number().int().nullable(),
1081
+ errorText: z.string().nullable(),
1082
+ mime: z.string().nullable(),
1083
+ reqHash: z.string().nullable(),
1084
+ respHash: z.string().nullable(),
1085
+ shapeHash: z.string().nullable(),
1086
+ reqBodyRef: z.string().nullable(),
1087
+ respBodyRef: z.string().nullable(),
1088
+ bodyStatus: XrayBodyStatus.nullable(),
1089
+ redirectsJson: z.string().nullable(),
1090
+ timingsJson: z.string().nullable(),
1091
+ initiatorJson: z.string().nullable(),
1092
+ wallStart: z.string()
1093
+ });
1094
+ var NetVolatilityRow = z.object({
1095
+ testId: z.string(),
1096
+ identityKey: z.string(),
1097
+ jsonPath: z.string(),
1098
+ // '' => presence-level
1099
+ kind: z.enum(["value", "presence"]),
1100
+ learnedAt: z.string()
1101
+ });
967
1102
  var ulidImpl = monotonicFactory();
968
1103
  function ulid() {
969
1104
  return ulidImpl();
@@ -998,6 +1133,12 @@ var LlmError = class extends QaiosError {
998
1133
  this.name = "LlmError";
999
1134
  }
1000
1135
  };
1136
+ var XrayError = class extends QaiosError {
1137
+ constructor(opts) {
1138
+ super(opts);
1139
+ this.name = "XrayError";
1140
+ }
1141
+ };
1001
1142
  function isQaiosShaped(err) {
1002
1143
  if (!err || typeof err !== "object") return false;
1003
1144
  const e = err;
@@ -1054,6 +1195,1376 @@ function formatQaiosError(err, opts = {}) {
1054
1195
  }
1055
1196
  return lines.join("\n") + "\n";
1056
1197
  }
1198
+ var __defProp2 = Object.defineProperty;
1199
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
1200
+ var __esm2 = (fn, res) => function __init() {
1201
+ return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
1202
+ };
1203
+ var __export2 = (target, all) => {
1204
+ for (var name in all)
1205
+ __defProp2(target, name, { get: all[name], enumerable: true });
1206
+ };
1207
+ function rowToNetRun(row) {
1208
+ return NetRunRow.parse({
1209
+ runId: row.run_id,
1210
+ workflowId: row.workflow_id,
1211
+ testId: row.test_id,
1212
+ browser: row.browser,
1213
+ tier: row.tier,
1214
+ captureLevel: row.capture_level,
1215
+ startedAt: row.started_at,
1216
+ isGreen: row.is_green === 1,
1217
+ isBaseline: row.is_baseline === 1
1218
+ });
1219
+ }
1220
+ var NetRunsRepository;
1221
+ var init_net_runs = __esm2({
1222
+ "src/storage/repositories/net-runs.ts"() {
1223
+ NetRunsRepository = class {
1224
+ constructor(db) {
1225
+ this.db = db;
1226
+ }
1227
+ db;
1228
+ create(input) {
1229
+ const r = NetRunRow.parse(input);
1230
+ this.db.prepare(
1231
+ `INSERT INTO net_runs
1232
+ (run_id, workflow_id, test_id, browser, tier, capture_level,
1233
+ started_at, is_green, is_baseline)
1234
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1235
+ ).run(
1236
+ r.runId,
1237
+ r.workflowId,
1238
+ r.testId,
1239
+ r.browser,
1240
+ r.tier,
1241
+ r.captureLevel,
1242
+ r.startedAt,
1243
+ r.isGreen ? 1 : 0,
1244
+ r.isBaseline ? 1 : 0
1245
+ );
1246
+ return r;
1247
+ }
1248
+ findById(runId) {
1249
+ const row = this.db.prepare("SELECT * FROM net_runs WHERE run_id = ?").get(runId);
1250
+ return row ? rowToNetRun(row) : null;
1251
+ }
1252
+ listByWorkflow(workflowId) {
1253
+ const rows = this.db.prepare("SELECT * FROM net_runs WHERE workflow_id = ? ORDER BY started_at DESC").all(workflowId);
1254
+ return rows.map(rowToNetRun);
1255
+ }
1256
+ /**
1257
+ * Runs for a test, newest first. Used by baseline selection (last green per
1258
+ * test+browser) and volatility learning (compare consecutive green runs).
1259
+ */
1260
+ listByTest(testId, browser) {
1261
+ const sql = browser === void 0 ? "SELECT * FROM net_runs WHERE test_id = ? ORDER BY started_at DESC" : "SELECT * FROM net_runs WHERE test_id = ? AND browser = ? ORDER BY started_at DESC";
1262
+ const rows = browser === void 0 ? this.db.prepare(sql).all(testId) : this.db.prepare(sql).all(testId, browser);
1263
+ return rows.map(rowToNetRun);
1264
+ }
1265
+ /** Flip green/baseline flags as the run is finalized / a baseline is pinned. */
1266
+ update(runId, patch) {
1267
+ const fields = [];
1268
+ const params = [];
1269
+ if (patch.isGreen !== void 0) {
1270
+ fields.push("is_green = ?");
1271
+ params.push(patch.isGreen ? 1 : 0);
1272
+ }
1273
+ if (patch.isBaseline !== void 0) {
1274
+ fields.push("is_baseline = ?");
1275
+ params.push(patch.isBaseline ? 1 : 0);
1276
+ }
1277
+ if (fields.length === 0) return this.findById(runId);
1278
+ params.push(runId);
1279
+ this.db.prepare(`UPDATE net_runs SET ${fields.join(", ")} WHERE run_id = ?`).run(...params);
1280
+ return this.findById(runId);
1281
+ }
1282
+ /** Clear the baseline pin for every run of a (test, browser). */
1283
+ clearBaseline(testId, browser) {
1284
+ this.db.prepare("UPDATE net_runs SET is_baseline = 0 WHERE test_id = ? AND browser = ?").run(testId, browser);
1285
+ }
1286
+ delete(runId) {
1287
+ this.db.prepare("DELETE FROM net_runs WHERE run_id = ?").run(runId);
1288
+ }
1289
+ };
1290
+ }
1291
+ });
1292
+ function rowToNetRequest(row) {
1293
+ return NetRequestRow.parse({
1294
+ id: row.id,
1295
+ runId: row.run_id,
1296
+ stepId: row.step_id,
1297
+ attribution: row.attribution,
1298
+ targetType: row.target_type,
1299
+ method: row.method,
1300
+ urlRaw: row.url_raw,
1301
+ urlTemplate: row.url_template,
1302
+ gqlOperation: row.gql_operation,
1303
+ identityKey: row.identity_key,
1304
+ status: row.status,
1305
+ errorText: row.error_text,
1306
+ mime: row.mime,
1307
+ reqHash: row.req_hash,
1308
+ respHash: row.resp_hash,
1309
+ shapeHash: row.shape_hash,
1310
+ reqBodyRef: row.req_body_ref,
1311
+ respBodyRef: row.resp_body_ref,
1312
+ bodyStatus: row.body_status,
1313
+ redirectsJson: row.redirects_json,
1314
+ timingsJson: row.timings_json,
1315
+ initiatorJson: row.initiator_json,
1316
+ wallStart: row.wall_start
1317
+ });
1318
+ }
1319
+ var NetRequestsRepository;
1320
+ var init_net_requests = __esm2({
1321
+ "src/storage/repositories/net-requests.ts"() {
1322
+ NetRequestsRepository = class {
1323
+ constructor(db) {
1324
+ this.db = db;
1325
+ }
1326
+ db;
1327
+ create(input) {
1328
+ const r = NetRequestRow.parse(input);
1329
+ this.db.prepare(
1330
+ `INSERT INTO net_requests
1331
+ (id, run_id, step_id, attribution, target_type, method, url_raw,
1332
+ url_template, gql_operation, identity_key, status, error_text, mime,
1333
+ req_hash, resp_hash, shape_hash, req_body_ref, resp_body_ref,
1334
+ body_status, redirects_json, timings_json, initiator_json, wall_start)
1335
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1336
+ ).run(
1337
+ r.id,
1338
+ r.runId,
1339
+ r.stepId,
1340
+ r.attribution,
1341
+ r.targetType,
1342
+ r.method,
1343
+ r.urlRaw,
1344
+ r.urlTemplate,
1345
+ r.gqlOperation,
1346
+ r.identityKey,
1347
+ r.status,
1348
+ r.errorText,
1349
+ r.mime,
1350
+ r.reqHash,
1351
+ r.respHash,
1352
+ r.shapeHash,
1353
+ r.reqBodyRef,
1354
+ r.respBodyRef,
1355
+ r.bodyStatus,
1356
+ r.redirectsJson,
1357
+ r.timingsJson,
1358
+ r.initiatorJson,
1359
+ r.wallStart
1360
+ );
1361
+ return r;
1362
+ }
1363
+ /** Bulk insert in one transaction — a run can produce hundreds of requests. */
1364
+ createMany(rows) {
1365
+ const insert = this.db.transaction((items) => {
1366
+ for (const item of items) this.create(item);
1367
+ });
1368
+ insert(rows);
1369
+ }
1370
+ listByRun(runId) {
1371
+ const rows = this.db.prepare("SELECT * FROM net_requests WHERE run_id = ? ORDER BY wall_start ASC").all(runId);
1372
+ return rows.map(rowToNetRequest);
1373
+ }
1374
+ listByRunStep(runId, stepId) {
1375
+ const sql = stepId === null ? "SELECT * FROM net_requests WHERE run_id = ? AND step_id IS NULL ORDER BY wall_start ASC" : "SELECT * FROM net_requests WHERE run_id = ? AND step_id = ? ORDER BY wall_start ASC";
1376
+ const rows = stepId === null ? this.db.prepare(sql).all(runId) : this.db.prepare(sql).all(runId, stepId);
1377
+ return rows.map(rowToNetRequest);
1378
+ }
1379
+ deleteByRun(runId) {
1380
+ this.db.prepare("DELETE FROM net_requests WHERE run_id = ?").run(runId);
1381
+ }
1382
+ };
1383
+ }
1384
+ });
1385
+ var NetBodiesRepository;
1386
+ var init_net_bodies = __esm2({
1387
+ "src/storage/repositories/net-bodies.ts"() {
1388
+ NetBodiesRepository = class {
1389
+ constructor(db) {
1390
+ this.db = db;
1391
+ }
1392
+ db;
1393
+ /**
1394
+ * Store a canonical body and return its hash (the ref stored on the request
1395
+ * row). Idempotent: an already-present hash is left untouched (`INSERT OR
1396
+ * IGNORE`), so two runs producing the same body share one BLOB. The caller
1397
+ * passes the ALREADY-canonicalized + redacted bytes — secrets must never
1398
+ * reach here.
1399
+ */
1400
+ putBody(canonical) {
1401
+ const hash = createHash("sha256").update(canonical).digest("hex");
1402
+ const compressed = gzipSync(canonical);
1403
+ this.db.prepare("INSERT OR IGNORE INTO net_bodies (hash, compressed, size_raw) VALUES (?, ?, ?)").run(hash, compressed, canonical.byteLength);
1404
+ return hash;
1405
+ }
1406
+ /** Retrieve + decompress a body by hash, or null if absent (e.g. GC'd). */
1407
+ getBody(hash) {
1408
+ const row = this.db.prepare("SELECT * FROM net_bodies WHERE hash = ?").get(hash);
1409
+ if (row === void 0) return null;
1410
+ return gunzipSync(Buffer.from(row.compressed));
1411
+ }
1412
+ has(hash) {
1413
+ const row = this.db.prepare("SELECT 1 AS present FROM net_bodies WHERE hash = ?").get(hash);
1414
+ return row !== void 0;
1415
+ }
1416
+ /**
1417
+ * Garbage-collect bodies no longer referenced by any net_requests row. Called
1418
+ * after pruning old runs (retainRuns). Refcounting is implicit: a body is live
1419
+ * iff some request still points at it via req_body_ref / resp_body_ref.
1420
+ * Returns the number of orphaned bodies deleted.
1421
+ */
1422
+ gcOrphans() {
1423
+ const result = this.db.prepare(
1424
+ `DELETE FROM net_bodies
1425
+ WHERE hash NOT IN (
1426
+ SELECT req_body_ref FROM net_requests WHERE req_body_ref IS NOT NULL
1427
+ UNION
1428
+ SELECT resp_body_ref FROM net_requests WHERE resp_body_ref IS NOT NULL
1429
+ )`
1430
+ ).run();
1431
+ return Number(result.changes);
1432
+ }
1433
+ };
1434
+ }
1435
+ });
1436
+ function rowToVolatility(row) {
1437
+ return NetVolatilityRow.parse({
1438
+ testId: row.test_id,
1439
+ identityKey: row.identity_key,
1440
+ jsonPath: row.json_path,
1441
+ kind: row.kind,
1442
+ learnedAt: row.learned_at
1443
+ });
1444
+ }
1445
+ var NetVolatilityRepository;
1446
+ var init_net_volatility = __esm2({
1447
+ "src/storage/repositories/net-volatility.ts"() {
1448
+ NetVolatilityRepository = class {
1449
+ constructor(db) {
1450
+ this.db = db;
1451
+ }
1452
+ db;
1453
+ /** Upsert a volatility entry (idempotent on the composite PK). */
1454
+ mark(entry) {
1455
+ const e = NetVolatilityRow.parse(entry);
1456
+ this.db.prepare(
1457
+ `INSERT INTO net_volatility (test_id, identity_key, json_path, kind, learned_at)
1458
+ VALUES (?, ?, ?, ?, ?)
1459
+ ON CONFLICT(test_id, identity_key, json_path)
1460
+ DO UPDATE SET kind = excluded.kind, learned_at = excluded.learned_at`
1461
+ ).run(e.testId, e.identityKey, e.jsonPath, e.kind, e.learnedAt);
1462
+ }
1463
+ listByTest(testId) {
1464
+ const rows = this.db.prepare("SELECT * FROM net_volatility WHERE test_id = ?").all(testId);
1465
+ return rows.map(rowToVolatility);
1466
+ }
1467
+ reset(testId) {
1468
+ const result = testId === void 0 ? this.db.prepare("DELETE FROM net_volatility").run() : this.db.prepare("DELETE FROM net_volatility WHERE test_id = ?").run(testId);
1469
+ return Number(result.changes);
1470
+ }
1471
+ };
1472
+ }
1473
+ });
1474
+ function stepReporterPath() {
1475
+ const candidates = [
1476
+ path15.resolve(here, "scripts", "xray-step-reporter.mjs"),
1477
+ // dist/xray/scripts
1478
+ path15.resolve(here, "xray", "scripts", "xray-step-reporter.mjs"),
1479
+ path15.resolve(here, "..", "xray", "scripts", "xray-step-reporter.mjs")
1480
+ ];
1481
+ for (const c of candidates) if (existsSync(c)) return c;
1482
+ return candidates[0];
1483
+ }
1484
+ function resolveUserConfig(cwd) {
1485
+ for (const name of USER_CONFIG_NAMES) {
1486
+ const p = path15.join(cwd, name);
1487
+ if (existsSync(p)) return p;
1488
+ }
1489
+ return null;
1490
+ }
1491
+ function wrapperWithUserConfig(spec) {
1492
+ return `// AUTO-GENERATED by QAIOS xray \u2014 temporary; deleted after the run. Extends your
1493
+ // playwright config with recordHar so QAIOS can capture network traffic. Your
1494
+ // config is never modified. This file sits beside your config so all relative
1495
+ // paths in it (webServer, globalSetup, testDir, \u2026) resolve normally.
1496
+ import base from ${JSON.stringify(spec)};
1497
+
1498
+ const harPath = process.env.QAIOS_XRAY_HAR_PATH;
1499
+ const level = process.env.QAIOS_XRAY_LEVEL ?? 'summary';
1500
+ const harContent = level === 'headers' ? 'omit' : 'embed';
1501
+
1502
+ const baseConfig = base && base.default ? base.default : base;
1503
+
1504
+ export default {
1505
+ ...baseConfig,
1506
+ use: {
1507
+ ...(baseConfig && baseConfig.use ? baseConfig.use : {}),
1508
+ contextOptions: {
1509
+ ...(baseConfig && baseConfig.use && baseConfig.use.contextOptions
1510
+ ? baseConfig.use.contextOptions
1511
+ : {}),
1512
+ recordHar: { path: harPath, content: harContent, mode: 'full' },
1513
+ },
1514
+ },
1515
+ };
1516
+ `;
1517
+ }
1518
+ function wrapperStandalone() {
1519
+ return `// AUTO-GENERATED by QAIOS xray \u2014 do not edit. No user playwright config was
1520
+ // found, so this is a minimal config with recordHar enabled.
1521
+ import path from 'node:path';
1522
+
1523
+ const harPath = process.env.QAIOS_XRAY_HAR_PATH;
1524
+ const level = process.env.QAIOS_XRAY_LEVEL ?? 'summary';
1525
+ const cwd = process.env.QAIOS_XRAY_CWD ?? process.cwd();
1526
+ const testDirRel = process.env.QAIOS_XRAY_TESTDIR ?? 'tests';
1527
+ const harContent = level === 'headers' ? 'omit' : 'embed';
1528
+
1529
+ export default {
1530
+ testDir: path.isAbsolute(testDirRel) ? testDirRel : path.resolve(cwd, testDirRel),
1531
+ use: {
1532
+ contextOptions: { recordHar: { path: harPath, content: harContent, mode: 'full' } },
1533
+ },
1534
+ };
1535
+ `;
1536
+ }
1537
+ function materializeWrapperConfig(opts) {
1538
+ mkdirSync(opts.xrayDir, { recursive: true });
1539
+ const userConfig = resolveUserConfig(opts.cwd);
1540
+ let source;
1541
+ let ext;
1542
+ if (userConfig !== null) {
1543
+ const userExt = path15.extname(userConfig);
1544
+ ext = userExt === ".cjs" ? ".cjs" : userExt;
1545
+ const baseName = path15.basename(userConfig).replace(/\.[^.]+$/, "");
1546
+ const spec = userExt === ".mjs" || userExt === ".cjs" ? `./${baseName}${userExt}` : `./${baseName}`;
1547
+ source = wrapperWithUserConfig(spec);
1548
+ } else {
1549
+ ext = ".mjs";
1550
+ source = wrapperStandalone();
1551
+ }
1552
+ const configPath = path15.join(opts.cwd, `${WRAPPER_BASENAME}${ext}`);
1553
+ try {
1554
+ writeFileSync(configPath, source, "utf-8");
1555
+ } catch (err) {
1556
+ throw new XrayError({
1557
+ code: "qaios.xray.config_resolve_failed",
1558
+ message: "Failed to write the xray wrapper Playwright config.",
1559
+ detail: `Could not write ${configPath}.`,
1560
+ cause: err
1561
+ });
1562
+ }
1563
+ const stepsPath = path15.join(opts.xrayDir, `${path15.basename(opts.harPath, ".har")}.steps.ndjson`);
1564
+ return {
1565
+ configPath,
1566
+ cleanupPath: configPath,
1567
+ stepReporterPath: stepReporterPath(),
1568
+ stepsPath,
1569
+ env: {
1570
+ QAIOS_XRAY_USER_CONFIG: userConfig ?? "",
1571
+ QAIOS_XRAY_HAR_PATH: opts.harPath,
1572
+ QAIOS_XRAY_LEVEL: opts.level,
1573
+ QAIOS_XRAY_CWD: opts.cwd,
1574
+ QAIOS_XRAY_TESTDIR: opts.testDir ?? "tests",
1575
+ QAIOS_XRAY_STEPS_PATH: stepsPath
1576
+ }
1577
+ };
1578
+ }
1579
+ var here;
1580
+ var USER_CONFIG_NAMES;
1581
+ var WRAPPER_BASENAME;
1582
+ var init_wrapper_config = __esm2({
1583
+ "src/xray/wrapper-config.ts"() {
1584
+ here = path15.dirname(fileURLToPath(import.meta.url));
1585
+ USER_CONFIG_NAMES = [
1586
+ "playwright.config.ts",
1587
+ "playwright.config.js",
1588
+ "playwright.config.mjs",
1589
+ "playwright.config.cjs"
1590
+ ];
1591
+ WRAPPER_BASENAME = ".qaios-xray.pw.config";
1592
+ }
1593
+ });
1594
+ function mimeOf(entry) {
1595
+ const ct = entry.response?.content?.mimeType;
1596
+ if (!ct) return null;
1597
+ return ct.split(";")[0].trim() || null;
1598
+ }
1599
+ function bodyOf(entry) {
1600
+ const content = entry.response?.content;
1601
+ if (!content || typeof content.text !== "string" || content.text.length === 0) {
1602
+ return { text: null, status: "skipped" };
1603
+ }
1604
+ if (content.encoding === "base64") {
1605
+ try {
1606
+ return { text: Buffer.from(content.text, "base64").toString("utf-8"), status: "stored" };
1607
+ } catch {
1608
+ return { text: null, status: "skipped" };
1609
+ }
1610
+ }
1611
+ return { text: content.text, status: "stored" };
1612
+ }
1613
+ function timingsOf(entry) {
1614
+ const t = entry.timings ?? {};
1615
+ const out = {};
1616
+ if (typeof t.dns === "number" && t.dns >= 0) out.dns = t.dns;
1617
+ if (typeof t.connect === "number" && t.connect >= 0) out.connect = t.connect;
1618
+ if (typeof t.wait === "number" && t.wait >= 0) out.ttfb = t.wait;
1619
+ if (typeof entry.time === "number" && entry.time >= 0) out.total = entry.time;
1620
+ return out;
1621
+ }
1622
+ function parseHarFile(harPath) {
1623
+ if (!existsSync(harPath)) {
1624
+ return [];
1625
+ }
1626
+ let har;
1627
+ try {
1628
+ har = JSON.parse(readFileSync(harPath, "utf-8"));
1629
+ } catch (err) {
1630
+ throw new XrayError({
1631
+ code: "qaios.xray.har_parse_failed",
1632
+ message: "Failed to parse the Playwright HAR file produced by xray capture.",
1633
+ detail: `HAR at ${harPath} was not valid JSON.`,
1634
+ cause: err
1635
+ });
1636
+ }
1637
+ const entries = har.log?.entries ?? [];
1638
+ const events = [];
1639
+ for (const entry of entries) {
1640
+ const url = entry.request?.url;
1641
+ if (typeof url !== "string" || !/^https?:/i.test(url)) continue;
1642
+ const wallStart = typeof entry.startedDateTime === "string" ? Date.parse(entry.startedDateTime) : NaN;
1643
+ const mime = mimeOf(entry);
1644
+ const isTextual = mime !== null && /^(application\/json|application\/graphql|text\/)/i.test(mime);
1645
+ const body = isTextual ? bodyOf(entry) : { text: null, status: "skipped" };
1646
+ const failure = entry.response?._failureText ?? entry._failureText ?? null;
1647
+ const event = XrayRequestEvent.parse({
1648
+ method: entry.request?.method ?? "GET",
1649
+ url,
1650
+ initiator: null,
1651
+ // HAR carries no usable initiator stack
1652
+ status: typeof entry.response?.status === "number" ? entry.response.status : null,
1653
+ errorText: failure,
1654
+ mime,
1655
+ fromServiceWorker: false,
1656
+ fromDiskCache: Boolean(entry.cache && Object.keys(entry.cache).length > 0),
1657
+ reqBody: typeof entry.request?.postData?.text === "string" ? entry.request.postData.text : null,
1658
+ respBody: body.text,
1659
+ bodyStatus: body.status,
1660
+ redirects: typeof entry.response?.redirectURL === "string" && entry.response.redirectURL.length > 0 ? [{ url: entry.response.redirectURL, status: entry.response?.status ?? 0 }] : [],
1661
+ timings: timingsOf(entry),
1662
+ wallStart: Number.isFinite(wallStart) ? wallStart : 0
1663
+ });
1664
+ events.push(event);
1665
+ }
1666
+ events.sort((a, b) => a.wallStart - b.wallStart);
1667
+ return events;
1668
+ }
1669
+ var init_har_parser = __esm2({
1670
+ "src/xray/har-parser.ts"() {
1671
+ }
1672
+ });
1673
+ function testIdFor(file, title) {
1674
+ return `${file.replace(/\\/g, "/")}::${title}`;
1675
+ }
1676
+ function flattenSteps(steps, out) {
1677
+ if (!steps) return;
1678
+ for (const s of steps) {
1679
+ if (s.category === "pw:api" && typeof s.startTime === "string") {
1680
+ const start = Date.parse(s.startTime);
1681
+ if (Number.isFinite(start)) {
1682
+ const dur = typeof s.duration === "number" && s.duration >= 0 ? s.duration : 0;
1683
+ out.push({
1684
+ stepId: ulid(),
1685
+ title: s.title ?? "(step)",
1686
+ category: s.category,
1687
+ startWall: start,
1688
+ endWall: start + dur
1689
+ });
1690
+ }
1691
+ }
1692
+ flattenSteps(s.steps, out);
1693
+ }
1694
+ }
1695
+ function walkSuite(suite, acc) {
1696
+ for (const spec of suite.specs ?? []) {
1697
+ const file = spec.file ?? suite.file ?? "";
1698
+ const rel = file ? path15.basename(file) : "";
1699
+ const title = spec.title ?? "(test)";
1700
+ const test = spec.tests?.[spec.tests.length - 1];
1701
+ const finalResult = test?.results?.[test.results.length - 1];
1702
+ const windows = [];
1703
+ flattenSteps(finalResult?.steps, windows);
1704
+ windows.sort((a, b) => a.startWall - b.startWall);
1705
+ acc.push({
1706
+ testId: testIdFor(rel, title),
1707
+ testTitle: title,
1708
+ file: rel,
1709
+ passed: finalResult?.status === "passed",
1710
+ windows
1711
+ });
1712
+ }
1713
+ for (const child of suite.suites ?? []) walkSuite(child, acc);
1714
+ }
1715
+ function parseStepWindows(reportPath) {
1716
+ if (!existsSync(reportPath)) return [];
1717
+ let report;
1718
+ try {
1719
+ report = JSON.parse(readFileSync(reportPath, "utf-8"));
1720
+ } catch {
1721
+ return [];
1722
+ }
1723
+ const acc = [];
1724
+ for (const suite of report.suites ?? []) walkSuite(suite, acc);
1725
+ return acc;
1726
+ }
1727
+ function parseStepSidecar(stepsPath) {
1728
+ const byTest = /* @__PURE__ */ new Map();
1729
+ if (!existsSync(stepsPath)) return byTest;
1730
+ let raw;
1731
+ try {
1732
+ raw = readFileSync(stepsPath, "utf-8");
1733
+ } catch {
1734
+ return byTest;
1735
+ }
1736
+ for (const line of raw.split("\n")) {
1737
+ const trimmed = line.trim();
1738
+ if (trimmed.length === 0) continue;
1739
+ let parsed;
1740
+ try {
1741
+ parsed = JSON.parse(trimmed);
1742
+ } catch {
1743
+ continue;
1744
+ }
1745
+ const list = byTest.get(parsed.testId) ?? [];
1746
+ list.push({
1747
+ stepId: ulid(),
1748
+ title: parsed.title,
1749
+ category: parsed.category,
1750
+ startWall: parsed.startWall,
1751
+ endWall: parsed.endWall
1752
+ });
1753
+ byTest.set(parsed.testId, list);
1754
+ }
1755
+ for (const list of byTest.values()) list.sort((a, b) => a.startWall - b.startWall);
1756
+ return byTest;
1757
+ }
1758
+ function parseStepWindowsWithSidecar(reportPath, stepsPath) {
1759
+ const fromReport = parseStepWindows(reportPath);
1760
+ if (stepsPath === void 0) return fromReport;
1761
+ const sidecar = parseStepSidecar(stepsPath);
1762
+ if (sidecar.size === 0) return fromReport;
1763
+ const merged = fromReport.map((t) => {
1764
+ const windows = sidecar.get(t.testId);
1765
+ return windows !== void 0 ? { ...t, windows } : t;
1766
+ });
1767
+ const seen = new Set(merged.map((t) => t.testId));
1768
+ for (const [testId, windows] of sidecar) {
1769
+ if (!seen.has(testId)) {
1770
+ const title = testId.split("::").slice(1).join("::") || testId;
1771
+ merged.push({
1772
+ testId,
1773
+ testTitle: title,
1774
+ file: testId.split("::")[0] ?? "",
1775
+ passed: false,
1776
+ windows
1777
+ });
1778
+ }
1779
+ }
1780
+ return merged;
1781
+ }
1782
+ var init_step_windows = __esm2({
1783
+ "src/xray/step-windows.ts"() {
1784
+ }
1785
+ });
1786
+ function attributeRequests(requests, windows, opts = {}) {
1787
+ const quiescence = opts.quiescenceMs ?? DEFAULT_QUIESCENCE_MS;
1788
+ const maxWindow = opts.maxWindowMs ?? DEFAULT_MAX_WINDOW_MS;
1789
+ const effective = windows.map((w) => {
1790
+ const rawEnd = w.endWall + quiescence;
1791
+ const cappedEnd = Math.min(rawEnd, w.startWall + maxWindow);
1792
+ return { stepId: w.stepId, start: w.startWall, end: Math.max(cappedEnd, w.startWall) };
1793
+ });
1794
+ return requests.map((event) => {
1795
+ const t = event.wallStart;
1796
+ const containing = effective.filter((w) => t >= w.start && t <= w.end);
1797
+ if (containing.length === 0) {
1798
+ return { event, stepId: null, attribution: "background" };
1799
+ }
1800
+ if (containing.length === 1) {
1801
+ return { event, stepId: containing[0].stepId, attribution: "temporal" };
1802
+ }
1803
+ const winner = containing.reduce((a, b) => b.start > a.start ? b : a);
1804
+ return { event, stepId: winner.stepId, attribution: "temporal_ambiguous" };
1805
+ });
1806
+ }
1807
+ var DEFAULT_QUIESCENCE_MS;
1808
+ var DEFAULT_MAX_WINDOW_MS;
1809
+ var init_causality_engine = __esm2({
1810
+ "src/xray/causality-engine.ts"() {
1811
+ DEFAULT_QUIESCENCE_MS = 500;
1812
+ DEFAULT_MAX_WINDOW_MS = 15e3;
1813
+ }
1814
+ });
1815
+ function isVolatileParamName(name, extra) {
1816
+ const lower = name.toLowerCase();
1817
+ if (DEFAULT_VOLATILE_PARAMS.includes(lower)) return true;
1818
+ if (/^(session|token|sig)/.test(lower)) return true;
1819
+ return extra.some((p) => p.toLowerCase() === lower);
1820
+ }
1821
+ function templateSegment(seg) {
1822
+ if (seg.length === 0) return seg;
1823
+ if (NUMERIC_RE.test(seg)) return "{id}";
1824
+ if (UUID_RE.test(seg) || ULID_RE.test(seg) || OBJECTID_RE.test(seg) || LONGHEX_RE.test(seg)) {
1825
+ return "{id}";
1826
+ }
1827
+ if (BASE64URL_RE.test(seg)) return "{token}";
1828
+ return seg;
1829
+ }
1830
+ function templateUrl(rawUrl, volatileParams = []) {
1831
+ let u;
1832
+ try {
1833
+ u = new URL(rawUrl);
1834
+ } catch {
1835
+ return { host: "", urlTemplate: rawUrl };
1836
+ }
1837
+ const segs = u.pathname.split("/").map(templateSegment);
1838
+ const pathTemplate = segs.join("/");
1839
+ const params = [...u.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b));
1840
+ const queryParts = params.map(
1841
+ ([k, v]) => isVolatileParamName(k, volatileParams) ? k : `${k}=${templateSegment(v)}`
1842
+ );
1843
+ const query = queryParts.length > 0 ? `?${queryParts.join("&")}` : "";
1844
+ return { host: u.host, urlTemplate: `${pathTemplate}${query}` };
1845
+ }
1846
+ function identityKey(method, host, urlTemplate, gqlOperation) {
1847
+ const base = `${method.toUpperCase()}|${host}|${urlTemplate}`;
1848
+ return gqlOperation ? `${base}|${gqlOperation}` : base;
1849
+ }
1850
+ var UUID_RE;
1851
+ var ULID_RE;
1852
+ var OBJECTID_RE;
1853
+ var LONGHEX_RE;
1854
+ var NUMERIC_RE;
1855
+ var BASE64URL_RE;
1856
+ var DEFAULT_VOLATILE_PARAMS;
1857
+ var init_identity = __esm2({
1858
+ "src/xray/identity.ts"() {
1859
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1860
+ ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
1861
+ OBJECTID_RE = /^[0-9a-f]{24}$/i;
1862
+ LONGHEX_RE = /^[0-9a-f]{16,}$/i;
1863
+ NUMERIC_RE = /^\d+$/;
1864
+ BASE64URL_RE = /^[A-Za-z0-9_-]{22,}$/;
1865
+ DEFAULT_VOLATILE_PARAMS = [
1866
+ "t",
1867
+ "ts",
1868
+ "timestamp",
1869
+ "_",
1870
+ "cb",
1871
+ "nonce",
1872
+ "state",
1873
+ "code"
1874
+ ];
1875
+ }
1876
+ });
1877
+ function placeholderFor(value) {
1878
+ if (ISO_DATE_RE.test(value)) return "<iso-date>";
1879
+ if (JWT_RE.test(value) && value.length > 30) return "<jwt>";
1880
+ if (UUID_RE2.test(value)) return "<uuid>";
1881
+ if (ULID_RE2.test(value)) return "<ulid>";
1882
+ if (EPOCH_MS_RE.test(value)) return "<epoch-ms>";
1883
+ if (EPOCH_S_RE.test(value)) return "<epoch-s>";
1884
+ return null;
1885
+ }
1886
+ function sha256Hex3(s) {
1887
+ return createHash("sha256").update(s).digest("hex");
1888
+ }
1889
+ function parseRedactionRules(jsonPaths) {
1890
+ const rules = [];
1891
+ for (const raw of jsonPaths) {
1892
+ const p = raw.trim();
1893
+ if (p.startsWith("$..")) {
1894
+ rules.push({ recursiveField: p.slice(3), path: null });
1895
+ } else if (p.startsWith("$.")) {
1896
+ const segs = p.slice(2).split(".").map((s) => s.replace(/\[[^\]]*\]/g, "")).filter((s) => s.length > 0);
1897
+ rules.push({ recursiveField: null, path: segs });
1898
+ }
1899
+ }
1900
+ return rules;
1901
+ }
1902
+ function redactJson(value, rules) {
1903
+ if (rules.length === 0) return value;
1904
+ const recursiveFields = new Set(
1905
+ rules.map((r) => r.recursiveField).filter((f) => f !== null)
1906
+ );
1907
+ const anchored = rules.map((r) => r.path).filter((p) => p !== null);
1908
+ function walk(node, trail) {
1909
+ if (Array.isArray(node)) return node.map((v) => walk(v, trail));
1910
+ if (node !== null && typeof node === "object") {
1911
+ const out = {};
1912
+ for (const [k, v] of Object.entries(node)) {
1913
+ const here22 = [...trail, k];
1914
+ if (recursiveFields.has(k)) {
1915
+ out[k] = REDACTED;
1916
+ } else if (anchored.some((p) => pathEquals(p, here22))) {
1917
+ out[k] = REDACTED;
1918
+ } else {
1919
+ out[k] = walk(v, here22);
1920
+ }
1921
+ }
1922
+ return out;
1923
+ }
1924
+ return node;
1925
+ }
1926
+ return walk(value, []);
1927
+ }
1928
+ function pathEquals(a, b) {
1929
+ return a.length === b.length && a.every((s, i) => s === b[i]);
1930
+ }
1931
+ function canonicalizeValue(value) {
1932
+ if (Array.isArray(value)) return value.map(canonicalizeValue);
1933
+ if (value !== null && typeof value === "object") {
1934
+ const out = {};
1935
+ for (const key of Object.keys(value).sort()) {
1936
+ out[key] = canonicalizeValue(value[key]);
1937
+ }
1938
+ return out;
1939
+ }
1940
+ if (typeof value === "string") {
1941
+ const ph = placeholderFor(value);
1942
+ return ph ?? value;
1943
+ }
1944
+ return value;
1945
+ }
1946
+ function shapeOf(value) {
1947
+ if (Array.isArray(value)) {
1948
+ return value.length > 0 ? ["array", shapeOf(value[0])] : ["array"];
1949
+ }
1950
+ if (value !== null && typeof value === "object") {
1951
+ const out = {};
1952
+ for (const key of Object.keys(value).sort()) {
1953
+ out[key] = shapeOf(value[key]);
1954
+ }
1955
+ return out;
1956
+ }
1957
+ if (value === null) return "null";
1958
+ return typeof value;
1959
+ }
1960
+ function canonicalizeBody(body, mime, redactionRules) {
1961
+ const looksJson = mime !== null && /json|graphql/i.test(mime) || /^\s*[{[]/.test(body);
1962
+ if (looksJson) {
1963
+ try {
1964
+ const parsed = JSON.parse(body);
1965
+ const redacted = redactJson(parsed, redactionRules);
1966
+ const canonical = canonicalizeValue(redacted);
1967
+ const canonicalStr = JSON.stringify(canonical);
1968
+ return {
1969
+ hash: sha256Hex3(canonicalStr),
1970
+ shapeHash: sha256Hex3(JSON.stringify(shapeOf(redacted))),
1971
+ canonical: Buffer.from(canonicalStr, "utf-8"),
1972
+ isJson: true
1973
+ };
1974
+ } catch {
1975
+ }
1976
+ }
1977
+ return {
1978
+ hash: sha256Hex3(body),
1979
+ shapeHash: sha256Hex3("text"),
1980
+ canonical: Buffer.from(body, "utf-8"),
1981
+ isJson: false
1982
+ };
1983
+ }
1984
+ function detectGraphql(url, reqBody) {
1985
+ const looksGraphqlUrl = /\/graphql\b/i.test(url);
1986
+ if (reqBody === null) return null;
1987
+ let parsed;
1988
+ try {
1989
+ parsed = JSON.parse(reqBody);
1990
+ } catch {
1991
+ return null;
1992
+ }
1993
+ if (parsed === null || typeof parsed !== "object") return null;
1994
+ const obj = parsed;
1995
+ if (typeof obj.query !== "string") return null;
1996
+ if (!looksGraphqlUrl && !/\b(query|mutation|subscription)\b/.test(obj.query)) return null;
1997
+ const operationName = typeof obj.operationName === "string" && obj.operationName.length > 0 ? obj.operationName : extractOpName(obj.query) ?? "anonymous";
1998
+ const queryHash = sha256Hex3(obj.query.replace(/\s+/g, " ").trim()).slice(0, 16);
1999
+ return { operationName, queryHash };
2000
+ }
2001
+ function extractOpName(query) {
2002
+ const m = query.match(/\b(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/);
2003
+ return m ? m[1] : null;
2004
+ }
2005
+ function normalizeRequest(input) {
2006
+ const { host, urlTemplate } = templateUrl(input.url, input.volatileParams);
2007
+ const gql = detectGraphql(input.url, input.reqBody);
2008
+ const idKey = gql ? identityKey(input.method, host, urlTemplate, `${gql.operationName}:${gql.queryHash}`) : identityKey(input.method, host, urlTemplate, null);
2009
+ const req = input.reqBody !== null && input.reqBody.length > 0 ? canonicalizeBody(input.reqBody, input.mime, input.redactionRules) : null;
2010
+ const resp = input.respBody !== null && input.respBody.length > 0 ? canonicalizeBody(input.respBody, input.mime, input.redactionRules) : null;
2011
+ return {
2012
+ host,
2013
+ urlTemplate,
2014
+ identityKey: idKey,
2015
+ gqlOperation: gql ? gql.operationName : null,
2016
+ reqHash: req ? req.hash : null,
2017
+ // shapeHash describes the response shape (what SCHEMA_CHANGED compares).
2018
+ respHash: resp ? resp.hash : null,
2019
+ shapeHash: resp ? resp.shapeHash : null,
2020
+ reqCanonical: req ? req.canonical : null,
2021
+ respCanonical: resp ? resp.canonical : null
2022
+ };
2023
+ }
2024
+ var ISO_DATE_RE;
2025
+ var EPOCH_MS_RE;
2026
+ var EPOCH_S_RE;
2027
+ var UUID_RE2;
2028
+ var ULID_RE2;
2029
+ var JWT_RE;
2030
+ var REDACTED;
2031
+ var DEFAULT_HEADER_DENYLIST;
2032
+ var init_normalizer = __esm2({
2033
+ "src/xray/normalizer.ts"() {
2034
+ init_identity();
2035
+ ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
2036
+ EPOCH_MS_RE = /^\d{13}$/;
2037
+ EPOCH_S_RE = /^\d{10}$/;
2038
+ UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2039
+ ULID_RE2 = /^[0-9A-HJKMNP-TV-Z]{26}$/;
2040
+ JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
2041
+ REDACTED = "<redacted>";
2042
+ DEFAULT_HEADER_DENYLIST = [
2043
+ "authorization",
2044
+ "cookie",
2045
+ "set-cookie",
2046
+ "x-api-key",
2047
+ "proxy-authorization"
2048
+ ];
2049
+ }
2050
+ });
2051
+ function buildRunGraph(requests) {
2052
+ const graph = /* @__PURE__ */ new Map();
2053
+ for (const r of requests) {
2054
+ const key = r.identityKey;
2055
+ const existing = graph.get(key);
2056
+ if (existing === void 0) {
2057
+ graph.set(key, {
2058
+ identityKey: r.identityKey,
2059
+ stepId: r.stepId,
2060
+ attribution: r.attribution,
2061
+ method: r.method,
2062
+ status: r.status,
2063
+ errorText: r.errorText,
2064
+ shapeHash: r.shapeHash,
2065
+ respHash: r.respHash,
2066
+ count: 1
2067
+ });
2068
+ } else {
2069
+ existing.count += 1;
2070
+ if (isErrorStatus(r.status) && !isErrorStatus(existing.status)) {
2071
+ existing.status = r.status;
2072
+ existing.errorText = r.errorText;
2073
+ }
2074
+ }
2075
+ }
2076
+ return graph;
2077
+ }
2078
+ function isErrorStatus(status) {
2079
+ return status === null || status >= 400 || status < 0;
2080
+ }
2081
+ var BaselineSelector;
2082
+ var init_baseline = __esm2({
2083
+ "src/xray/baseline.ts"() {
2084
+ init_net_runs();
2085
+ init_net_requests();
2086
+ BaselineSelector = class {
2087
+ netRuns;
2088
+ netRequests;
2089
+ constructor(db) {
2090
+ this.netRuns = new NetRunsRepository(db);
2091
+ this.netRequests = new NetRequestsRepository(db);
2092
+ }
2093
+ /**
2094
+ * The baseline run for a (test, browser): the pinned baseline if one exists,
2095
+ * else the most recent green run. Returns null if there's no green run yet.
2096
+ * `excludeRunId` skips a candidate run (so a run never baselines against itself).
2097
+ */
2098
+ select(testId, browser, excludeRunId) {
2099
+ const runs = this.netRuns.listByTest(testId, browser);
2100
+ const pinned = runs.find((r) => r.isBaseline && r.runId !== excludeRunId);
2101
+ if (pinned !== void 0) return pinned;
2102
+ return runs.find((r) => r.isGreen && r.runId !== excludeRunId) ?? null;
2103
+ }
2104
+ /** Build the causal graph for a run by id (or null if the run is unknown). */
2105
+ graphFor(runId) {
2106
+ if (this.netRuns.findById(runId) === null) return null;
2107
+ return buildRunGraph(this.netRequests.listByRun(runId));
2108
+ }
2109
+ /** Pin a run as the baseline for its (test, browser), clearing any prior pin. */
2110
+ pin(runId) {
2111
+ const run = this.netRuns.findById(runId);
2112
+ if (run === null) return null;
2113
+ this.netRuns.clearBaseline(run.testId, run.browser);
2114
+ return this.netRuns.update(runId, { isBaseline: true });
2115
+ }
2116
+ /** Clear the baseline pin for a (test, browser). */
2117
+ clear(testId, browser) {
2118
+ this.netRuns.clearBaseline(testId, browser);
2119
+ }
2120
+ };
2121
+ }
2122
+ });
2123
+ function learnVolatility(testId, graphA, graphB, learnedAt) {
2124
+ const out = [];
2125
+ for (const [key, a] of graphA) {
2126
+ const b = graphB.get(key);
2127
+ if (b === void 0) continue;
2128
+ const sameShape = a.shapeHash !== null && b.shapeHash !== null && a.shapeHash === b.shapeHash;
2129
+ const valueDiffers = a.respHash !== null && b.respHash !== null && a.respHash !== b.respHash;
2130
+ if (sameShape && valueDiffers) {
2131
+ out.push(
2132
+ NetVolatilityRow.parse({
2133
+ testId,
2134
+ identityKey: key,
2135
+ jsonPath: "",
2136
+ kind: "value",
2137
+ learnedAt
2138
+ })
2139
+ );
2140
+ }
2141
+ }
2142
+ const onlyInOne = (from, other) => {
2143
+ for (const key of from.keys()) {
2144
+ if (!other.has(key)) {
2145
+ out.push(
2146
+ NetVolatilityRow.parse({
2147
+ testId,
2148
+ identityKey: key,
2149
+ jsonPath: "",
2150
+ kind: "presence",
2151
+ learnedAt
2152
+ })
2153
+ );
2154
+ }
2155
+ }
2156
+ };
2157
+ onlyInOne(graphA, graphB);
2158
+ onlyInOne(graphB, graphA);
2159
+ const seen = /* @__PURE__ */ new Set();
2160
+ return out.filter((r) => {
2161
+ const k = `${r.identityKey}|${r.kind}`;
2162
+ if (seen.has(k)) return false;
2163
+ seen.add(k);
2164
+ return true;
2165
+ });
2166
+ }
2167
+ var VolatilityLearner;
2168
+ var init_volatility = __esm2({
2169
+ "src/xray/volatility.ts"() {
2170
+ init_net_runs();
2171
+ init_net_requests();
2172
+ init_net_volatility();
2173
+ init_baseline();
2174
+ VolatilityLearner = class {
2175
+ netRuns;
2176
+ netRequests;
2177
+ volatility;
2178
+ constructor(db) {
2179
+ this.netRuns = new NetRunsRepository(db);
2180
+ this.netRequests = new NetRequestsRepository(db);
2181
+ this.volatility = new NetVolatilityRepository(db);
2182
+ }
2183
+ /**
2184
+ * Learn from the two most recent green runs of (test, browser). No-op (returns
2185
+ * 0) until at least two green runs exist. Returns the number of rows learned.
2186
+ */
2187
+ learnForTest(testId, browser, learnedAt) {
2188
+ const green = this.netRuns.listByTest(testId, browser).filter((r) => r.isGreen);
2189
+ if (green.length < 2) return 0;
2190
+ const [newest, prev] = green;
2191
+ const graphA = buildRunGraph(this.netRequests.listByRun(newest.runId));
2192
+ const graphB = buildRunGraph(this.netRequests.listByRun(prev.runId));
2193
+ const rows = learnVolatility(testId, graphA, graphB, learnedAt);
2194
+ for (const row of rows) this.volatility.mark(row);
2195
+ return rows.length;
2196
+ }
2197
+ };
2198
+ }
2199
+ });
2200
+ function windowEnvelope(windows) {
2201
+ if (windows.length === 0) return null;
2202
+ let start = Infinity;
2203
+ let end = -Infinity;
2204
+ for (const w of windows) {
2205
+ if (w.startWall < start) start = w.startWall;
2206
+ if (w.endWall > end) end = w.endWall;
2207
+ }
2208
+ return { start: start - 1e3, end: end + 15e3 };
2209
+ }
2210
+ var CaptureSession;
2211
+ var init_capture_session = __esm2({
2212
+ "src/xray/capture-session.ts"() {
2213
+ init_net_runs();
2214
+ init_net_requests();
2215
+ init_net_bodies();
2216
+ init_har_parser();
2217
+ init_step_windows();
2218
+ init_causality_engine();
2219
+ init_normalizer();
2220
+ init_volatility();
2221
+ CaptureSession = class {
2222
+ constructor(deps) {
2223
+ this.deps = deps;
2224
+ this.netRuns = new NetRunsRepository(deps.db);
2225
+ this.netRequests = new NetRequestsRepository(deps.db);
2226
+ this.netBodies = new NetBodiesRepository(deps.db);
2227
+ this.volatilityLearner = new VolatilityLearner(deps.db);
2228
+ }
2229
+ deps;
2230
+ netRuns;
2231
+ netRequests;
2232
+ netBodies;
2233
+ volatilityLearner;
2234
+ /**
2235
+ * Ingest a finished run's HAR + report. Because recordHar writes ONE HAR for
2236
+ * the whole subprocess (all tests share it), requests are bucketed to tests by
2237
+ * their step windows: a request that falls in a test's window-span belongs to
2238
+ * that test; requests outside every test's span are attributed to that test's
2239
+ * `background` only if they still fall in its outer [firstStep, lastStep]
2240
+ * envelope, else dropped from per-test runs (true cross-test noise).
2241
+ */
2242
+ ingest(opts) {
2243
+ const startedAt = opts.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2244
+ const events = parseHarFile(opts.harPath);
2245
+ const perTest = parseStepWindowsWithSidecar(opts.reportPath, opts.stepsPath);
2246
+ const tier = opts.browser === "chromium" ? "A" : "B";
2247
+ const volatileParams = opts.volatileParams ?? [];
2248
+ const redactionRules = parseRedactionRules(opts.redact ?? []);
2249
+ const runIds = [];
2250
+ let requestCount = 0;
2251
+ const greenTests = /* @__PURE__ */ new Set();
2252
+ for (const test of perTest) {
2253
+ const envelope = windowEnvelope(test.windows);
2254
+ const testEvents = envelope === null ? perTest.length === 1 ? events : [] : events.filter((e) => e.wallStart >= envelope.start && e.wallStart <= envelope.end);
2255
+ if (testEvents.length === 0 && perTest.length > 1) continue;
2256
+ const attributed = attributeRequests(
2257
+ testEvents,
2258
+ test.windows,
2259
+ opts.quiescenceMs !== void 0 ? { quiescenceMs: opts.quiescenceMs } : {}
2260
+ );
2261
+ const runId = ulid();
2262
+ const isGreen = test.passed;
2263
+ this.netRuns.create(
2264
+ NetRunRow.parse({
2265
+ runId,
2266
+ workflowId: opts.workflowId,
2267
+ testId: test.testId,
2268
+ browser: opts.browser,
2269
+ tier,
2270
+ captureLevel: opts.level,
2271
+ startedAt,
2272
+ isGreen,
2273
+ isBaseline: false
2274
+ })
2275
+ );
2276
+ const rows = attributed.map(
2277
+ (a) => this.toRequestRow(runId, a, volatileParams, redactionRules)
2278
+ );
2279
+ if (rows.length > 0) this.netRequests.createMany(rows);
2280
+ runIds.push(runId);
2281
+ requestCount += rows.length;
2282
+ if (isGreen) greenTests.add(test.testId);
2283
+ }
2284
+ if (perTest.length === 0 && events.length > 0) {
2285
+ const runId = ulid();
2286
+ const testId = testIdFor(path15.basename(opts.harPath), "(unattributed)");
2287
+ this.netRuns.create(
2288
+ NetRunRow.parse({
2289
+ runId,
2290
+ workflowId: opts.workflowId,
2291
+ testId,
2292
+ browser: opts.browser,
2293
+ tier,
2294
+ captureLevel: opts.level,
2295
+ startedAt,
2296
+ isGreen: false,
2297
+ isBaseline: false
2298
+ })
2299
+ );
2300
+ const rows = events.map(
2301
+ (e) => this.toRequestRow(
2302
+ runId,
2303
+ { event: e, stepId: null, attribution: "background" },
2304
+ volatileParams,
2305
+ redactionRules
2306
+ )
2307
+ );
2308
+ if (rows.length > 0) this.netRequests.createMany(rows);
2309
+ runIds.push(runId);
2310
+ requestCount += rows.length;
2311
+ }
2312
+ for (const testId of greenTests) {
2313
+ try {
2314
+ this.volatilityLearner.learnForTest(testId, opts.browser, startedAt);
2315
+ } catch {
2316
+ }
2317
+ }
2318
+ this.deps.auditLogger.append({
2319
+ workflowId: opts.workflowId,
2320
+ phase: "EXECUTION",
2321
+ skillId: null,
2322
+ event: "xray.capture",
2323
+ payload: {
2324
+ runIds,
2325
+ requestCount,
2326
+ browser: opts.browser,
2327
+ tier,
2328
+ level: opts.level
2329
+ },
2330
+ actor: "system"
2331
+ });
2332
+ return { runIds, requestCount };
2333
+ }
2334
+ /** Build a persisted net_requests row from an attributed event. */
2335
+ toRequestRow(runId, a, volatileParams, redactionRules) {
2336
+ const e = a.event;
2337
+ const norm = normalizeRequest({
2338
+ method: e.method,
2339
+ url: e.url,
2340
+ mime: e.mime,
2341
+ reqBody: e.reqBody,
2342
+ respBody: e.bodyStatus === "stored" ? e.respBody : null,
2343
+ volatileParams,
2344
+ redactionRules
2345
+ });
2346
+ let reqBodyRef = null;
2347
+ let respBodyRef = null;
2348
+ if (norm.reqCanonical !== null) reqBodyRef = this.netBodies.putBody(norm.reqCanonical);
2349
+ if (norm.respCanonical !== null) respBodyRef = this.netBodies.putBody(norm.respCanonical);
2350
+ return NetRequestRow.parse({
2351
+ id: ulid(),
2352
+ runId,
2353
+ stepId: a.stepId,
2354
+ attribution: a.attribution,
2355
+ targetType: "page",
2356
+ method: e.method,
2357
+ urlRaw: e.url,
2358
+ urlTemplate: norm.urlTemplate,
2359
+ gqlOperation: norm.gqlOperation,
2360
+ identityKey: norm.identityKey,
2361
+ status: e.status,
2362
+ errorText: e.errorText,
2363
+ mime: e.mime,
2364
+ reqHash: norm.reqHash,
2365
+ respHash: norm.respHash,
2366
+ shapeHash: norm.shapeHash,
2367
+ reqBodyRef,
2368
+ respBodyRef,
2369
+ bodyStatus: e.bodyStatus,
2370
+ redirectsJson: e.redirects.length > 0 ? JSON.stringify(e.redirects) : null,
2371
+ timingsJson: Object.keys(e.timings).length > 0 ? JSON.stringify(e.timings) : null,
2372
+ initiatorJson: e.initiator ? JSON.stringify(e.initiator) : null,
2373
+ wallStart: new Date(e.wallStart).toISOString()
2374
+ });
2375
+ }
2376
+ };
2377
+ }
2378
+ });
2379
+ function statusClass(status) {
2380
+ if (status === null || status <= 0) return "unknown";
2381
+ if (status >= 500) return "5xx";
2382
+ if (status >= 400) return "4xx";
2383
+ if (status >= 300) return "3xx";
2384
+ if (status >= 200) return "2xx";
2385
+ return "other";
2386
+ }
2387
+ function isErrorStatus2(status) {
2388
+ return status !== null && status >= 400;
2389
+ }
2390
+ function isUnknownStatus(status, errorText) {
2391
+ return errorText === null && (status === null || status <= 0);
2392
+ }
2393
+ function diffGraphs(run, baseline, opts = {}) {
2394
+ const presenceUnstable = new Set(
2395
+ (opts.volatility ?? []).filter((v) => v.kind === "presence").map((v) => v.identityKey)
2396
+ );
2397
+ const valueVolatile = new Set(
2398
+ (opts.volatility ?? []).filter((v) => v.kind === "value").map((v) => v.identityKey)
2399
+ );
2400
+ const backgroundFailSignal = opts.backgroundIsFailSignal === true;
2401
+ const divergences = [];
2402
+ const emit2 = (diffClass, e, detail) => {
2403
+ const isFail = FAIL_SIGNAL_CLASSES.has(diffClass) && (backgroundFailSignal || e.attribution !== "background");
2404
+ divergences.push({
2405
+ diffClass,
2406
+ identityKey: e.identityKey,
2407
+ stepId: e.stepId,
2408
+ attribution: e.attribution,
2409
+ detail,
2410
+ failSignal: isFail
2411
+ });
2412
+ };
2413
+ for (const [key, entry] of run) {
2414
+ const base = baseline.get(key);
2415
+ if (isUnknownStatus(entry.status, entry.errorText)) continue;
2416
+ if (isErrorStatus2(entry.status) || entry.errorText !== null) {
2417
+ const ui = opts.uiPassed === true ? " (UI assertions passed)" : "";
2418
+ emit2(
2419
+ "ERROR_RESPONSE",
2420
+ entry,
2421
+ `${describe(entry)} returned ${entry.errorText ?? entry.status ?? "error"}${ui}`
2422
+ );
2423
+ continue;
2424
+ }
2425
+ if (base !== void 0) {
2426
+ const runClass = statusClass(entry.status);
2427
+ const baseClass = statusClass(base.status);
2428
+ if (runClass !== "unknown" && baseClass !== "unknown" && runClass !== baseClass) {
2429
+ emit2(
2430
+ "STATUS_CHANGED",
2431
+ entry,
2432
+ `${describe(entry)} status ${base.status ?? "\u2014"} \u2192 ${entry.status ?? "\u2014"}`
2433
+ );
2434
+ } else if (entry.shapeHash !== null && base.shapeHash !== null && entry.shapeHash !== base.shapeHash) {
2435
+ emit2("SCHEMA_CHANGED", entry, `${describe(entry)} response shape changed`);
2436
+ } else if (entry.respHash !== null && base.respHash !== null && entry.respHash !== base.respHash && !valueVolatile.has(key)) {
2437
+ emit2("VALUE_CHANGED", entry, `${describe(entry)} response value changed`);
2438
+ }
2439
+ } else if (!presenceUnstable.has(key)) {
2440
+ emit2("NEW_CALL", entry, `${describe(entry)} is new vs baseline`);
2441
+ }
2442
+ }
2443
+ for (const [key, base] of baseline) {
2444
+ if (!run.has(key) && !presenceUnstable.has(key)) {
2445
+ emit2("MISSING_CALL", base, `${describe(base)} was expected but did not occur`);
2446
+ }
2447
+ }
2448
+ divergences.sort(
2449
+ (a, b) => SEVERITY[b.diffClass] - SEVERITY[a.diffClass] || a.identityKey.localeCompare(b.identityKey)
2450
+ );
2451
+ return {
2452
+ divergences,
2453
+ hasFailSignal: divergences.some((d) => d.failSignal)
2454
+ };
2455
+ }
2456
+ function describe(e) {
2457
+ const parts = e.identityKey.split("|");
2458
+ return parts.length >= 3 ? `${parts[0]} ${parts[1]}${parts[2]}` : e.identityKey;
2459
+ }
2460
+ function summarizeForClassifier(result, maxLines = 20) {
2461
+ if (result.divergences.length === 0) return "No backend network divergences detected.";
2462
+ const lines = result.divergences.slice(0, maxLines).map((d) => `- [${d.diffClass}] ${d.detail} {attribution: ${d.attribution}}`);
2463
+ const omitted = result.divergences.length - lines.length;
2464
+ if (omitted > 0) lines.push(`- \u2026and ${omitted} more (lower severity).`);
2465
+ return lines.join("\n");
2466
+ }
2467
+ var FAIL_SIGNAL_CLASSES;
2468
+ var SEVERITY;
2469
+ var init_diff_engine = __esm2({
2470
+ "src/xray/diff-engine.ts"() {
2471
+ FAIL_SIGNAL_CLASSES = /* @__PURE__ */ new Set([
2472
+ "ERROR_RESPONSE",
2473
+ "STATUS_CHANGED"
2474
+ ]);
2475
+ SEVERITY = {
2476
+ ERROR_RESPONSE: 6,
2477
+ STATUS_CHANGED: 5,
2478
+ MISSING_CALL: 4,
2479
+ NEW_CALL: 3,
2480
+ SCHEMA_CHANGED: 2,
2481
+ VALUE_CHANGED: 1
2482
+ };
2483
+ }
2484
+ });
2485
+ function networkDiffForTest(db, workflowId, testFile, testName, uiPassed) {
2486
+ const netRuns = new NetRunsRepository(db);
2487
+ const netRequests = new NetRequestsRepository(db);
2488
+ const volatility = new NetVolatilityRepository(db);
2489
+ const baselines = new BaselineSelector(db);
2490
+ const base = testFile.split(/[\\/]/).pop() ?? testFile;
2491
+ const title = testName.includes(" > ") ? testName.slice(testName.lastIndexOf(" > ") + 3) : testName;
2492
+ const candidates = [
2493
+ testIdFor(base, title),
2494
+ testIdFor(testFile, testName),
2495
+ // exact, in case conventions already align
2496
+ testIdFor(base, testName)
2497
+ ];
2498
+ const runsForWorkflow = netRuns.listByWorkflow(workflowId);
2499
+ const runRow = runsForWorkflow.find((r) => candidates.includes(r.testId)) ?? // Last resort: match on the bare title alone (suffix).
2500
+ runsForWorkflow.find((r) => r.testId.endsWith(`::${title}`));
2501
+ if (runRow === void 0) return null;
2502
+ const baseline = baselines.select(runRow.testId, runRow.browser, runRow.runId);
2503
+ if (baseline === null) return null;
2504
+ const runGraph = buildRunGraph(netRequests.listByRun(runRow.runId));
2505
+ const baseGraph = buildRunGraph(netRequests.listByRun(baseline.runId));
2506
+ const result = diffGraphs(runGraph, baseGraph, {
2507
+ volatility: volatility.listByTest(runRow.testId),
2508
+ uiPassed
2509
+ });
2510
+ if (result.divergences.length === 0) return null;
2511
+ return {
2512
+ summary: summarizeForClassifier(result),
2513
+ hasFailSignal: result.hasFailSignal
2514
+ };
2515
+ }
2516
+ var init_classify_bridge = __esm2({
2517
+ "src/xray/classify-bridge.ts"() {
2518
+ init_net_runs();
2519
+ init_net_requests();
2520
+ init_net_volatility();
2521
+ init_baseline();
2522
+ init_diff_engine();
2523
+ init_step_windows();
2524
+ }
2525
+ });
2526
+ var xray_exports = {};
2527
+ __export2(xray_exports, {
2528
+ BaselineSelector: () => BaselineSelector,
2529
+ CaptureSession: () => CaptureSession,
2530
+ DEFAULT_HEADER_DENYLIST: () => DEFAULT_HEADER_DENYLIST,
2531
+ DEFAULT_VOLATILE_PARAMS: () => DEFAULT_VOLATILE_PARAMS,
2532
+ FAIL_SIGNAL_CLASSES: () => FAIL_SIGNAL_CLASSES,
2533
+ VolatilityLearner: () => VolatilityLearner,
2534
+ attributeRequests: () => attributeRequests,
2535
+ buildRunGraph: () => buildRunGraph,
2536
+ canonicalizeBody: () => canonicalizeBody,
2537
+ detectGraphql: () => detectGraphql,
2538
+ diffGraphs: () => diffGraphs,
2539
+ identityKey: () => identityKey,
2540
+ learnVolatility: () => learnVolatility,
2541
+ materializeWrapperConfig: () => materializeWrapperConfig,
2542
+ networkDiffForTest: () => networkDiffForTest,
2543
+ normalizeRequest: () => normalizeRequest,
2544
+ parseHarFile: () => parseHarFile,
2545
+ parseRedactionRules: () => parseRedactionRules,
2546
+ parseStepWindows: () => parseStepWindows,
2547
+ redactJson: () => redactJson,
2548
+ resolveUserConfig: () => resolveUserConfig,
2549
+ summarizeForClassifier: () => summarizeForClassifier,
2550
+ templateUrl: () => templateUrl,
2551
+ testIdFor: () => testIdFor
2552
+ });
2553
+ var init_xray = __esm2({
2554
+ "src/xray/index.ts"() {
2555
+ init_wrapper_config();
2556
+ init_har_parser();
2557
+ init_step_windows();
2558
+ init_causality_engine();
2559
+ init_identity();
2560
+ init_capture_session();
2561
+ init_normalizer();
2562
+ init_baseline();
2563
+ init_diff_engine();
2564
+ init_volatility();
2565
+ init_classify_bridge();
2566
+ }
2567
+ });
1057
2568
  var StorageError = class extends Error {
1058
2569
  code;
1059
2570
  cause;
@@ -1086,7 +2597,7 @@ function runMigrations(db, migrationsDir) {
1086
2597
  const applied = [];
1087
2598
  const skipped = [];
1088
2599
  for (const filename of files) {
1089
- const fullPath = path12.join(migrationsDir, filename);
2600
+ const fullPath = path15.join(migrationsDir, filename);
1090
2601
  const sql = readFileSync(fullPath, "utf-8");
1091
2602
  const checksum = sha256Hex(sql);
1092
2603
  const knownChecksum = appliedMap.get(filename);
@@ -1228,8 +2739,8 @@ var SqliteDb = class {
1228
2739
  this.db.close();
1229
2740
  }
1230
2741
  };
1231
- var __dirname$1 = path12.dirname(fileURLToPath(import.meta.url));
1232
- var DEFAULT_MIGRATIONS_DIR = path12.resolve(__dirname$1, "migrations");
2742
+ var __dirname$1 = path15.dirname(fileURLToPath(import.meta.url));
2743
+ var DEFAULT_MIGRATIONS_DIR = path15.resolve(__dirname$1, "migrations");
1233
2744
  var Storage = class _Storage {
1234
2745
  db;
1235
2746
  path;
@@ -1923,6 +3434,10 @@ var VisualDiffsRepository = class {
1923
3434
  return this.findById(id);
1924
3435
  }
1925
3436
  };
3437
+ init_net_runs();
3438
+ init_net_requests();
3439
+ init_net_bodies();
3440
+ init_net_volatility();
1926
3441
  function canonicalize(value) {
1927
3442
  return JSON.stringify(sortedDeep(value));
1928
3443
  }
@@ -2681,7 +4196,7 @@ function tempForTier(tier) {
2681
4196
  return 0.1;
2682
4197
  }
2683
4198
  }
2684
- var DEFAULT_LLM_TIMEOUT_MS = 12e4;
4199
+ var DEFAULT_LLM_TIMEOUT_MS = 24e4;
2685
4200
  function llmTimeoutMs() {
2686
4201
  const raw = process.env["QAIOS_LLM_TIMEOUT_MS"];
2687
4202
  if (raw === void 0) return DEFAULT_LLM_TIMEOUT_MS;
@@ -2965,14 +4480,14 @@ function formatZodError(err) {
2965
4480
  }
2966
4481
  var PROJECT_MEMORY_FILENAME = "QAIOS.md";
2967
4482
  function loadProjectMemory(projectRoot) {
2968
- const fullPath = path12.join(projectRoot, PROJECT_MEMORY_FILENAME);
4483
+ const fullPath = path15.join(projectRoot, PROJECT_MEMORY_FILENAME);
2969
4484
  if (!existsSync(fullPath)) return null;
2970
4485
  const text = readFileSync(fullPath, "utf-8");
2971
4486
  return parseProjectMemory(text);
2972
4487
  }
2973
4488
  var DEFAULT_COST_CAP = {
2974
- maxCalls: 15,
2975
- maxUsdCents: 50
4489
+ maxCalls: 20,
4490
+ maxUsdCents: 100
2976
4491
  };
2977
4492
  var CostTracker = class {
2978
4493
  constructor(db) {
@@ -3109,8 +4624,8 @@ function expectedCostCents(skillId) {
3109
4624
  }
3110
4625
  var whereCache = /* @__PURE__ */ new Map();
3111
4626
  function resolveBinaryWindows(name) {
3112
- if (path12.isAbsolute(name)) return name;
3113
- if (name.includes(path12.sep) || name.includes("/")) return path12.resolve(name);
4627
+ if (path15.isAbsolute(name)) return name;
4628
+ if (name.includes(path15.sep) || name.includes("/")) return path15.resolve(name);
3114
4629
  if (whereCache.has(name)) return whereCache.get(name) ?? null;
3115
4630
  const r = spawnSync("where", [name], { encoding: "utf8", shell: false });
3116
4631
  if (r.status !== 0) {
@@ -3291,9 +4806,9 @@ var PlaywrightSubprocessAdapter = class {
3291
4806
  const runId = opts.runId ?? ulid();
3292
4807
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3293
4808
  const startTimeMs = Date.now();
3294
- const artifactsDir = opts.artifactsDir ?? path12.join(opts.cwd, ".qaios", "runs", runId);
4809
+ const artifactsDir = opts.artifactsDir ?? path15.join(opts.cwd, ".qaios", "runs", runId);
3295
4810
  mkdirSync(artifactsDir, { recursive: true });
3296
- const reportPath = path12.join(artifactsDir, "results.json");
4811
+ const reportPath = path15.join(artifactsDir, "results.json");
3297
4812
  const runningRow = {
3298
4813
  id: runId,
3299
4814
  workflowId: opts.workflowId,
@@ -3318,20 +4833,59 @@ var PlaywrightSubprocessAdapter = class {
3318
4833
  );
3319
4834
  }
3320
4835
  const [bin, ...baseArgs] = command;
4836
+ const xrayDir = path15.join(opts.cwd, ".qaios", "xray");
4837
+ const harPath = path15.join(xrayDir, `${runId}.har`);
4838
+ let xrayConfigArgs = [];
4839
+ let xrayEnv = {};
4840
+ let xrayActive = false;
4841
+ let xrayCleanupPath = null;
4842
+ let xrayStepsPath = null;
4843
+ let reporterArg = "--reporter=json";
4844
+ if (opts.xray !== void 0) {
4845
+ try {
4846
+ const { materializeWrapperConfig: materializeWrapperConfig2 } = await Promise.resolve().then(() => (init_xray(), xray_exports));
4847
+ const wrapper = materializeWrapperConfig2({
4848
+ cwd: opts.cwd,
4849
+ xrayDir,
4850
+ harPath,
4851
+ level: opts.xray.level,
4852
+ ...opts.xray.testDir !== void 0 ? { testDir: opts.xray.testDir } : {}
4853
+ });
4854
+ xrayConfigArgs = [`--config=${wrapper.configPath}`];
4855
+ xrayEnv = wrapper.env;
4856
+ xrayCleanupPath = wrapper.cleanupPath;
4857
+ xrayStepsPath = wrapper.stepsPath;
4858
+ reporterArg = `--reporter=json,${wrapper.stepReporterPath}`;
4859
+ xrayActive = true;
4860
+ } catch (err) {
4861
+ process.stderr.write(
4862
+ `[qaios xray] capture disabled for this run: ${err.message}
4863
+ `
4864
+ );
4865
+ }
4866
+ }
3321
4867
  const args = [
3322
4868
  ...baseArgs,
3323
- "--reporter=json",
4869
+ ...xrayConfigArgs,
4870
+ reporterArg,
3324
4871
  ...opts.pattern !== void 0 ? [opts.pattern] : [],
3325
4872
  ...opts.extraArgs ?? []
3326
4873
  ];
3327
4874
  const env = {
3328
4875
  ...process.env,
4876
+ ...xrayEnv,
3329
4877
  PLAYWRIGHT_JSON_OUTPUT_NAME: reportPath
3330
4878
  };
3331
4879
  const subprocessResult = await this.spawnPlaywright(bin, args, opts.cwd, env, {
3332
4880
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
3333
4881
  ...opts.cancelSignal !== void 0 ? { cancelSignal: opts.cancelSignal } : {}
3334
4882
  });
4883
+ if (xrayCleanupPath !== null) {
4884
+ try {
4885
+ rmSync(xrayCleanupPath, { force: true });
4886
+ } catch {
4887
+ }
4888
+ }
3335
4889
  let report;
3336
4890
  let parseFailedReason = null;
3337
4891
  try {
@@ -3387,6 +4941,32 @@ var PlaywrightSubprocessAdapter = class {
3387
4941
  },
3388
4942
  actor: "system"
3389
4943
  });
4944
+ if (xrayActive && opts.xray !== void 0) {
4945
+ try {
4946
+ const { CaptureSession: CaptureSession2 } = await Promise.resolve().then(() => (init_xray(), xray_exports));
4947
+ const session = new CaptureSession2({
4948
+ db: this.deps.storage.db,
4949
+ auditLogger: this.deps.auditLogger
4950
+ });
4951
+ session.ingest({
4952
+ workflowId: opts.workflowId,
4953
+ harPath,
4954
+ reportPath,
4955
+ browser: "chromium",
4956
+ level: opts.xray.level,
4957
+ startedAt,
4958
+ ...xrayStepsPath !== null ? { stepsPath: xrayStepsPath } : {},
4959
+ ...opts.xray.quiescenceMs !== void 0 ? { quiescenceMs: opts.xray.quiescenceMs } : {},
4960
+ ...opts.xray.volatileParams !== void 0 ? { volatileParams: opts.xray.volatileParams } : {},
4961
+ ...opts.xray.redact !== void 0 ? { redact: opts.xray.redact } : {}
4962
+ });
4963
+ } catch (err) {
4964
+ process.stderr.write(
4965
+ `[qaios xray] capture ingest failed (run still recorded): ${err.message}
4966
+ `
4967
+ );
4968
+ }
4969
+ }
3390
4970
  const finalRow = this.runsRepo.findById(runId);
3391
4971
  if (!finalRow) {
3392
4972
  throw new PlaywrightAdapterError(
@@ -3495,16 +5075,16 @@ function truncateHtmlSafe(html, max = PAGE_CAPTURE_HTML_MAX_CHARS) {
3495
5075
  return slice + "\n<!-- truncated -->";
3496
5076
  }
3497
5077
  function defaultCaptureScriptPath() {
3498
- const here = path12.dirname(fileURLToPath(import.meta.url));
5078
+ const here22 = path15.dirname(fileURLToPath(import.meta.url));
3499
5079
  const candidates = [
3500
5080
  // tsup flattens src/healing/page-capture.ts to dist/index.js, so
3501
5081
  // `here` is dist/. The mirror lives at dist/healing/scripts/.
3502
- path12.resolve(here, "healing", "scripts", "capture-page.mjs"),
5082
+ path15.resolve(here22, "healing", "scripts", "capture-page.mjs"),
3503
5083
  // Co-located when bundled to dist/healing/.
3504
- path12.resolve(here, "scripts", "capture-page.mjs"),
5084
+ path15.resolve(here22, "scripts", "capture-page.mjs"),
3505
5085
  // Source layout under vitest.
3506
- path12.resolve(here, "scripts", "capture-page.mjs"),
3507
- path12.resolve(here, "..", "healing", "scripts", "capture-page.mjs")
5086
+ path15.resolve(here22, "scripts", "capture-page.mjs"),
5087
+ path15.resolve(here22, "..", "healing", "scripts", "capture-page.mjs")
3508
5088
  ];
3509
5089
  for (const c of candidates) {
3510
5090
  if (existsSync(c)) return c;
@@ -3512,8 +5092,8 @@ function defaultCaptureScriptPath() {
3512
5092
  return candidates[0];
3513
5093
  }
3514
5094
  async function capturePageSnapshot(opts) {
3515
- const tmp = mkdtempSync(path12.join(tmpdir(), "qaios-capture-"));
3516
- const out = path12.join(tmp, "snapshot.json");
5095
+ const tmp = mkdtempSync(path15.join(tmpdir(), "qaios-capture-"));
5096
+ const out = path15.join(tmp, "snapshot.json");
3517
5097
  mkdirSync(tmp, { recursive: true });
3518
5098
  const timeoutMs = opts.timeoutMs ?? 3e4;
3519
5099
  const command = opts.command ?? ["node", defaultCaptureScriptPath()];
@@ -3699,14 +5279,14 @@ function applyLocatorFix(opts) {
3699
5279
  };
3700
5280
  }
3701
5281
  const cwd = opts.cwd ?? process.cwd();
3702
- const absPath = path12.isAbsolute(opts.testFile) ? opts.testFile : path12.resolve(cwd, opts.testFile);
5282
+ const absPath = path15.isAbsolute(opts.testFile) ? opts.testFile : path15.resolve(cwd, opts.testFile);
3703
5283
  const oldContents = readFileSync(absPath, "utf-8");
3704
5284
  const occurrences = countOccurrences(oldContents, opts.fix.oldLocator);
3705
5285
  if (occurrences === 0) {
3706
5286
  return {
3707
5287
  applied: false,
3708
5288
  reason: "old_locator_not_found",
3709
- detail: `old locator not present in ${path12.relative(cwd, absPath)}: ${opts.fix.oldLocator}`,
5289
+ detail: `old locator not present in ${path15.relative(cwd, absPath)}: ${opts.fix.oldLocator}`,
3710
5290
  oldContents,
3711
5291
  newContents: null
3712
5292
  };
@@ -3715,7 +5295,7 @@ function applyLocatorFix(opts) {
3715
5295
  return {
3716
5296
  applied: false,
3717
5297
  reason: "old_locator_not_unique",
3718
- detail: `old locator appears ${occurrences} times in ${path12.relative(cwd, absPath)}; refusing to patch ambiguously.`,
5298
+ detail: `old locator appears ${occurrences} times in ${path15.relative(cwd, absPath)}; refusing to patch ambiguously.`,
3719
5299
  oldContents,
3720
5300
  newContents: null
3721
5301
  };
@@ -3753,7 +5333,7 @@ function applyLocatorFix(opts) {
3753
5333
  event: "artifact.updated",
3754
5334
  payload: {
3755
5335
  kind: "locator_fix",
3756
- testFile: path12.relative(cwd, absPath),
5336
+ testFile: path15.relative(cwd, absPath),
3757
5337
  oldLocator: opts.fix.oldLocator,
3758
5338
  newLocator: opts.fix.newLocator,
3759
5339
  fixKind: opts.fix.fixKind,
@@ -3780,12 +5360,12 @@ function extractLocatorFromError(errorMessage) {
3780
5360
  return null;
3781
5361
  }
3782
5362
  function resolveTestFile(testFile, cwd, configuredTestDir) {
3783
- if (path12.isAbsolute(testFile)) return testFile;
5363
+ if (path15.isAbsolute(testFile)) return testFile;
3784
5364
  const candidateDirs = ["", configuredTestDir ?? "tests", "tests", "test", "e2e"];
3785
5365
  for (const dir of candidateDirs) {
3786
5366
  if (dir === void 0) continue;
3787
- if (existsSync(path12.join(cwd, dir, testFile))) {
3788
- return path12.join(dir, testFile);
5367
+ if (existsSync(path15.join(cwd, dir, testFile))) {
5368
+ return path15.join(dir, testFile);
3789
5369
  }
3790
5370
  }
3791
5371
  return testFile;
@@ -3812,19 +5392,19 @@ async function fileDefect(opts) {
3812
5392
  const { defect, target } = opts;
3813
5393
  const allowedTools = opts.allowedTools ?? ["github:create_issue", "jira:create_issue"];
3814
5394
  if (target.integration === "stdout") {
3815
- const write = opts.stdoutWrite ?? ((s) => process.stdout.write(s));
3816
- write(`
5395
+ const write2 = opts.stdoutWrite ?? ((s) => process.stdout.write(s));
5396
+ write2(`
3817
5397
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 DEFECT (stdout target) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3818
5398
  `);
3819
- write(`${defect.title}
5399
+ write2(`${defect.title}
3820
5400
 
3821
5401
  `);
3822
- write(`${defect.body}
5402
+ write2(`${defect.body}
3823
5403
  `);
3824
- if (defect.labels.length > 0) write(`
5404
+ if (defect.labels.length > 0) write2(`
3825
5405
  Labels: ${defect.labels.join(", ")}
3826
5406
  `);
3827
- write(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5407
+ write2(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3828
5408
  `);
3829
5409
  audit(opts, "defect.filed", {
3830
5410
  target: "stdout",
@@ -4313,13 +5893,13 @@ function buildGatePayload(gateType, prior) {
4313
5893
  }
4314
5894
  if (gateType === "writing_review") {
4315
5895
  const writeKey2 = Object.keys(prior).find((k) => k.startsWith("write."));
4316
- const write = writeKey2 !== void 0 ? prior[writeKey2] : null;
4317
- const files = Array.isArray(write?.["files"]) ? write["files"] : [];
5896
+ const write2 = writeKey2 !== void 0 ? prior[writeKey2] : null;
5897
+ const files = Array.isArray(write2?.["files"]) ? write2["files"] : [];
4318
5898
  return {
4319
5899
  writeSkill: writeKey2 ?? null,
4320
5900
  fileCount: files.length,
4321
- qaiosMdSuggestionCount: Array.isArray(write?.["qaiosMdSuggestions"]) ? write["qaiosMdSuggestions"].length : 0,
4322
- reasoning: write?.["reasoning"] ?? null
5901
+ qaiosMdSuggestionCount: Array.isArray(write2?.["qaiosMdSuggestions"]) ? write2["qaiosMdSuggestions"].length : 0,
5902
+ reasoning: write2?.["reasoning"] ?? null
4323
5903
  };
4324
5904
  }
4325
5905
  return {};
@@ -4504,6 +6084,11 @@ var readyToRun = async (deps) => {
4504
6084
  }
4505
6085
  return { nextState: "completed" };
4506
6086
  };
6087
+ function isXrayArg(v) {
6088
+ if (v === null || typeof v !== "object") return false;
6089
+ const level = v.level;
6090
+ return level === "headers" || level === "summary" || level === "full" || level === "off";
6091
+ }
4507
6092
  var running = async (deps) => {
4508
6093
  const cwd = typeof deps.args["cwd"] === "string" ? deps.args["cwd"] : process.cwd();
4509
6094
  const adapterOpts = {
@@ -4521,7 +6106,14 @@ var running = async (deps) => {
4521
6106
  )
4522
6107
  } : {},
4523
6108
  ...typeof deps.args["executionTimeoutMs"] === "number" ? { timeoutMs: deps.args["executionTimeoutMs"] } : {},
4524
- ...deps.skillCtx.cancelSignal !== void 0 ? { cancelSignal: deps.skillCtx.cancelSignal } : {}
6109
+ ...deps.skillCtx.cancelSignal !== void 0 ? { cancelSignal: deps.skillCtx.cancelSignal } : {},
6110
+ ...isXrayArg(deps.args["xray"]) ? {
6111
+ xray: {
6112
+ ...deps.args["xray"],
6113
+ // Anchor capture's testDir to the same dir healing/writing use.
6114
+ ...typeof deps.args["testDir"] === "string" ? { testDir: deps.args["testDir"] } : {}
6115
+ }
6116
+ } : {}
4525
6117
  };
4526
6118
  const adapterResult = await deps.executionAdapter.run(adapterOpts);
4527
6119
  return {
@@ -4543,14 +6135,39 @@ var running = async (deps) => {
4543
6135
  }
4544
6136
  };
4545
6137
  };
4546
- var classifying = async (deps) => {
4547
- if (deps.args["noClassify"] === true) {
4548
- return { nextState: "completed" };
6138
+ async function emitPassWithDivergence(deps, testResults, networkDiffFor) {
6139
+ if (deps.storage === void 0) return;
6140
+ const passed = testResults.filter((t) => t.status === "passed");
6141
+ const flagged = [];
6142
+ for (const tr of passed) {
6143
+ const diff = networkDiffFor(deps.storage.db, deps.workflow.id, tr.testFile, tr.testName, true);
6144
+ if (diff !== null && diff.hasFailSignal) {
6145
+ flagged.push({ test: tr.testName, summary: diff.summary });
6146
+ }
4549
6147
  }
6148
+ if (flagged.length === 0) return;
6149
+ deps.auditLogger.append({
6150
+ workflowId: deps.workflow.id,
6151
+ phase: "EXECUTION",
6152
+ skillId: null,
6153
+ event: "xray.diff",
6154
+ payload: { passWithDivergence: true, count: flagged.length, flagged },
6155
+ actor: "system"
6156
+ });
6157
+ }
6158
+ var classifying = async (deps) => {
4550
6159
  const exec = deps.prior["execute.playwright"];
4551
6160
  if (!exec || !Array.isArray(exec.testResults)) {
4552
6161
  return { nextState: "completed" };
4553
6162
  }
6163
+ const xrayRan = isXrayArg(deps.args["xray"]) && deps.storage !== void 0;
6164
+ const networkDiffFor = xrayRan ? (await Promise.resolve().then(() => (init_xray(), xray_exports))).networkDiffForTest : null;
6165
+ if (networkDiffFor !== null && deps.storage !== void 0) {
6166
+ await emitPassWithDivergence(deps, exec.testResults, networkDiffFor);
6167
+ }
6168
+ if (deps.args["noClassify"] === true) {
6169
+ return { nextState: "completed" };
6170
+ }
4554
6171
  const failed2 = exec.testResults.filter((t) => t.status === "failed");
4555
6172
  if (failed2.length === 0) {
4556
6173
  return { nextState: "completed" };
@@ -4568,6 +6185,7 @@ var classifying = async (deps) => {
4568
6185
  excludeRunId: tr.runId,
4569
6186
  limit: 10
4570
6187
  });
6188
+ const netDiff = networkDiffFor !== null && deps.storage !== void 0 ? networkDiffFor(deps.storage.db, deps.workflow.id, tr.testFile, tr.testName, false) : null;
4571
6189
  const classifyInput = {
4572
6190
  testResult: {
4573
6191
  file: tr.testFile,
@@ -4577,7 +6195,8 @@ var classifying = async (deps) => {
4577
6195
  attachments: [],
4578
6196
  historicalRuns,
4579
6197
  ...errorBlock
4580
- }
6198
+ },
6199
+ ...netDiff !== null ? { networkDiff: netDiff } : {}
4581
6200
  };
4582
6201
  const result = await deps.skillRunner.run(classifySkill, deps.skillCtx, classifyInput);
4583
6202
  deps.testResultsRepo.setClassification(tr.id, result.output);
@@ -5321,6 +6940,8 @@ var Orchestrator = class {
5321
6940
  executionAdapter: this.executionAdapter,
5322
6941
  testResultsRepo: this.testResultsRepo,
5323
6942
  gatesRepo: this.gatesRepo,
6943
+ // Storage handle for the classifying state's xray diff lookup (v0.4).
6944
+ storage: this.deps.storage,
5324
6945
  // F3 pre-flight: hand the cost tracker + cap to runSkill so
5325
6946
  // it can abort before launching a skill that would tip the
5326
6947
  // cap. costCap=null (opt-out) is propagated as-is.
@@ -5474,11 +7095,11 @@ var AxeRunError = class extends Error {
5474
7095
  }
5475
7096
  };
5476
7097
  function defaultAxeScriptPath() {
5477
- const here = path12.dirname(fileURLToPath(import.meta.url));
7098
+ const here22 = path15.dirname(fileURLToPath(import.meta.url));
5478
7099
  const candidates = [
5479
- path12.resolve(here, "a11y", "scripts", "axe-runner.mjs"),
5480
- path12.resolve(here, "scripts", "axe-runner.mjs"),
5481
- path12.resolve(here, "..", "a11y", "scripts", "axe-runner.mjs")
7100
+ path15.resolve(here22, "a11y", "scripts", "axe-runner.mjs"),
7101
+ path15.resolve(here22, "scripts", "axe-runner.mjs"),
7102
+ path15.resolve(here22, "..", "a11y", "scripts", "axe-runner.mjs")
5482
7103
  ];
5483
7104
  for (const c of candidates) {
5484
7105
  if (existsSync(c)) return c;
@@ -5486,8 +7107,8 @@ function defaultAxeScriptPath() {
5486
7107
  return candidates[0];
5487
7108
  }
5488
7109
  async function runAxe(opts) {
5489
- const tmp = mkdtempSync(path12.join(tmpdir(), "qaios-axe-"));
5490
- const out = path12.join(tmp, "findings.json");
7110
+ const tmp = mkdtempSync(path15.join(tmpdir(), "qaios-axe-"));
7111
+ const out = path15.join(tmp, "findings.json");
5491
7112
  mkdirSync(tmp, { recursive: true });
5492
7113
  const timeoutMs = opts.timeoutMs ?? 45e3;
5493
7114
  const command = opts.command ?? ["node", defaultAxeScriptPath()];
@@ -5612,14 +7233,14 @@ var SnapshotCaptureError = class extends Error {
5612
7233
  }
5613
7234
  };
5614
7235
  function defaultCaptureScriptPath2() {
5615
- const here = path12.dirname(fileURLToPath(import.meta.url));
7236
+ const here22 = path15.dirname(fileURLToPath(import.meta.url));
5616
7237
  const candidates = [
5617
7238
  // Bundled (tsup flattens src/visual/snapshot-capture.ts → dist/index.js).
5618
- path12.resolve(here, "visual", "scripts", "capture-screenshot.mjs"),
7239
+ path15.resolve(here22, "visual", "scripts", "capture-screenshot.mjs"),
5619
7240
  // Co-located fallback.
5620
- path12.resolve(here, "scripts", "capture-screenshot.mjs"),
7241
+ path15.resolve(here22, "scripts", "capture-screenshot.mjs"),
5621
7242
  // Source layout under vitest.
5622
- path12.resolve(here, "..", "visual", "scripts", "capture-screenshot.mjs")
7243
+ path15.resolve(here22, "..", "visual", "scripts", "capture-screenshot.mjs")
5623
7244
  ];
5624
7245
  for (const c of candidates) {
5625
7246
  if (existsSync(c)) return c;
@@ -5629,7 +7250,7 @@ function defaultCaptureScriptPath2() {
5629
7250
  function safeName(name) {
5630
7251
  return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "snapshot";
5631
7252
  }
5632
- function sha256Hex3(buffer) {
7253
+ function sha256Hex4(buffer) {
5633
7254
  return createHash("sha256").update(buffer).digest("hex");
5634
7255
  }
5635
7256
  async function captureVisualBaselines(opts) {
@@ -5644,7 +7265,7 @@ async function captureVisualBaselines(opts) {
5644
7265
  );
5645
7266
  }
5646
7267
  const [bin, ...args] = command;
5647
- const baselinesDir = opts.baselinesDir ?? path12.join(opts.cwd, ".qaios", "baselines");
7268
+ const baselinesDir = opts.baselinesDir ?? path15.join(opts.cwd, ".qaios", "baselines");
5648
7269
  const repo = new VisualBaselinesRepository(opts.storage.db);
5649
7270
  const viewportByName = new Map(opts.viewports.map((v) => [v.name, v]));
5650
7271
  const baselines = [];
@@ -5667,8 +7288,8 @@ async function captureVisualBaselines(opts) {
5667
7288
  const relImagePath = [".qaios", "baselines", dim.name, `${safeName(snapshot.name)}.png`].join(
5668
7289
  "/"
5669
7290
  );
5670
- const absImagePath = path12.join(baselinesDir, dim.name, `${safeName(snapshot.name)}.png`);
5671
- mkdirSync(path12.dirname(absImagePath), { recursive: true });
7291
+ const absImagePath = path15.join(baselinesDir, dim.name, `${safeName(snapshot.name)}.png`);
7292
+ mkdirSync(path15.dirname(absImagePath), { recursive: true });
5672
7293
  const env = {
5673
7294
  ...process.env,
5674
7295
  QAIOS_SCREENSHOT_URL: joinUrl(opts.baseUrl, snapshot.route),
@@ -5705,7 +7326,7 @@ async function captureVisualBaselines(opts) {
5705
7326
  );
5706
7327
  }
5707
7328
  const bytes = readFileSync(absImagePath);
5708
- const sha = sha256Hex3(bytes);
7329
+ const sha = sha256Hex4(bytes);
5709
7330
  const baselineId = ulid();
5710
7331
  const approvedAt = (/* @__PURE__ */ new Date()).toISOString();
5711
7332
  const baselineInput = {
@@ -5936,7 +7557,7 @@ function diffImages(baselinePath, currentPath, diffOutPath, opts = {}) {
5936
7557
  diffMask[i] = 1;
5937
7558
  }
5938
7559
  }
5939
- mkdirSync(path12.dirname(diffOutPath), { recursive: true });
7560
+ mkdirSync(path15.dirname(diffOutPath), { recursive: true });
5940
7561
  writeFileSync(diffOutPath, PNG.sync.write(diffPng));
5941
7562
  const totalPixels = width * height;
5942
7563
  const percentageChanged = Number((pixelsChanged / totalPixels * 100).toFixed(4));
@@ -6002,18 +7623,18 @@ var SnapshotCheckError = class extends Error {
6002
7623
  }
6003
7624
  };
6004
7625
  function defaultCaptureScriptPath3() {
6005
- const here = path12.dirname(fileURLToPath(import.meta.url));
7626
+ const here22 = path15.dirname(fileURLToPath(import.meta.url));
6006
7627
  const candidates = [
6007
- path12.resolve(here, "visual", "scripts", "capture-screenshot.mjs"),
6008
- path12.resolve(here, "scripts", "capture-screenshot.mjs"),
6009
- path12.resolve(here, "..", "visual", "scripts", "capture-screenshot.mjs")
7628
+ path15.resolve(here22, "visual", "scripts", "capture-screenshot.mjs"),
7629
+ path15.resolve(here22, "scripts", "capture-screenshot.mjs"),
7630
+ path15.resolve(here22, "..", "visual", "scripts", "capture-screenshot.mjs")
6010
7631
  ];
6011
7632
  for (const c of candidates) {
6012
7633
  if (existsSync(c)) return c;
6013
7634
  }
6014
7635
  return candidates[0];
6015
7636
  }
6016
- function sha256Hex4(buf) {
7637
+ function sha256Hex5(buf) {
6017
7638
  return createHash("sha256").update(buf).digest("hex");
6018
7639
  }
6019
7640
  function spawnCapture3(bin, args, cwd, env, opts) {
@@ -6083,8 +7704,8 @@ async function runVisualCheck(opts) {
6083
7704
  }
6084
7705
  const [bin, ...args] = command;
6085
7706
  const thresholdPct = opts.thresholdPct ?? 0.1;
6086
- const runsDir = opts.runsDir ?? path12.join(opts.cwd, ".qaios", "runs", opts.workflowId);
6087
- const diffsDir = opts.diffsDir ?? path12.join(opts.cwd, ".qaios", "diffs", opts.workflowId);
7707
+ const runsDir = opts.runsDir ?? path15.join(opts.cwd, ".qaios", "runs", opts.workflowId);
7708
+ const diffsDir = opts.diffsDir ?? path15.join(opts.cwd, ".qaios", "diffs", opts.workflowId);
6088
7709
  new VisualBaselinesRepository(opts.storage.db);
6089
7710
  const diffRepo = new VisualDiffsRepository(opts.storage.db);
6090
7711
  const skillRunner = opts.skillRunner ?? new SkillRunner();
@@ -6109,7 +7730,7 @@ async function runVisualCheck(opts) {
6109
7730
  `baseline ${baseline.id} references unknown viewport "${baseline.viewport}"`
6110
7731
  );
6111
7732
  }
6112
- const baselineAbsPath = path12.isAbsolute(baseline.imagePath) ? baseline.imagePath : path12.join(opts.cwd, baseline.imagePath);
7733
+ const baselineAbsPath = path15.isAbsolute(baseline.imagePath) ? baseline.imagePath : path15.join(opts.cwd, baseline.imagePath);
6113
7734
  if (!existsSync(baselineAbsPath)) {
6114
7735
  perBaseline.push({
6115
7736
  baselineId: baseline.id,
@@ -6125,9 +7746,9 @@ async function runVisualCheck(opts) {
6125
7746
  summary.pendingReview++;
6126
7747
  continue;
6127
7748
  }
6128
- const safe = path12.basename(baseline.imagePath);
6129
- const currentAbsPath = path12.join(runsDir, baseline.viewport, safe);
6130
- mkdirSync(path12.dirname(currentAbsPath), { recursive: true });
7749
+ const safe = path15.basename(baseline.imagePath);
7750
+ const currentAbsPath = path15.join(runsDir, baseline.viewport, safe);
7751
+ mkdirSync(path15.dirname(currentAbsPath), { recursive: true });
6131
7752
  const env = {
6132
7753
  ...process.env,
6133
7754
  QAIOS_SCREENSHOT_URL: joinUrl(opts.baseUrl, baseline.route),
@@ -6156,11 +7777,11 @@ async function runVisualCheck(opts) {
6156
7777
  }
6157
7778
  const baselineBytes = readFileSync(baselineAbsPath);
6158
7779
  const currentBytes = readFileSync(currentAbsPath);
6159
- const baselineSha = sha256Hex4(baselineBytes);
6160
- const currentSha = sha256Hex4(currentBytes);
6161
- const currentImagePath = path12.relative(opts.cwd, currentAbsPath).split(path12.sep).join("/");
6162
- const diffAbsPath = path12.join(diffsDir, baseline.viewport, safe);
6163
- const diffImagePath = path12.relative(opts.cwd, diffAbsPath).split(path12.sep).join("/");
7780
+ const baselineSha = sha256Hex5(baselineBytes);
7781
+ const currentSha = sha256Hex5(currentBytes);
7782
+ const currentImagePath = path15.relative(opts.cwd, currentAbsPath).split(path15.sep).join("/");
7783
+ const diffAbsPath = path15.join(diffsDir, baseline.viewport, safe);
7784
+ const diffImagePath = path15.relative(opts.cwd, diffAbsPath).split(path15.sep).join("/");
6164
7785
  if (baselineSha === currentSha) {
6165
7786
  perBaseline.push({
6166
7787
  baselineId: baseline.id,
@@ -6176,7 +7797,7 @@ async function runVisualCheck(opts) {
6176
7797
  summary.noDiff++;
6177
7798
  continue;
6178
7799
  }
6179
- mkdirSync(path12.dirname(diffAbsPath), { recursive: true });
7800
+ mkdirSync(path15.dirname(diffAbsPath), { recursive: true });
6180
7801
  const diff = diffImages(baselineAbsPath, currentAbsPath, diffAbsPath);
6181
7802
  if (diff.sizeMismatch) {
6182
7803
  const diffId2 = ulid();
@@ -6361,7 +7982,7 @@ var OpenApiParseError = class extends Error {
6361
7982
  };
6362
7983
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6363
7984
  function parseOpenApi(specPath) {
6364
- const abs = path12.resolve(specPath);
7985
+ const abs = path15.resolve(specPath);
6365
7986
  if (!existsSync(abs)) {
6366
7987
  throw new OpenApiParseError("qaios.openapi.spec_not_found", `OpenAPI spec not found at ${abs}`);
6367
7988
  }
@@ -6858,6 +8479,10 @@ var ExitCode = {
6858
8479
  // rate limit, timeout, key invalid
6859
8480
  INTERNAL: 5,
6860
8481
  // QAIOS bug — should be reported
8482
+ // The UI tests passed, but xray detected a backend divergence (a 4xx/5xx or
8483
+ // status change on a call the test made). Only returned under `--xray-strict`;
8484
+ // without it, such a run is exit 0 with a warning. (v0.4)
8485
+ PASS_WITH_NETWORK_DIVERGENCE: 6,
6861
8486
  CANCELLED: 130
6862
8487
  // user pressed Ctrl+C (SIGINT)
6863
8488
  };
@@ -6867,7 +8492,7 @@ function detectProject(projectRoot) {
6867
8492
  const playwrightVersion = readPlaywrightVersion(packageJson);
6868
8493
  const hasPlaywright = playwrightVersion !== null;
6869
8494
  const testDir = detectTestDir(projectRoot);
6870
- const projectName = packageJson?.name ?? path12.basename(path12.resolve(projectRoot));
8495
+ const projectName = packageJson?.name ?? path15.basename(path15.resolve(projectRoot));
6871
8496
  const isEmpty = directoryIsEmpty(projectRoot);
6872
8497
  const looksLikeProject = packageJson !== null;
6873
8498
  return {
@@ -6882,7 +8507,7 @@ function detectProject(projectRoot) {
6882
8507
  };
6883
8508
  }
6884
8509
  function readPackageJson(projectRoot) {
6885
- const p = path12.join(projectRoot, "package.json");
8510
+ const p = path15.join(projectRoot, "package.json");
6886
8511
  if (!existsSync(p)) return null;
6887
8512
  try {
6888
8513
  return JSON.parse(readFileSync(p, "utf-8"));
@@ -6891,10 +8516,10 @@ function readPackageJson(projectRoot) {
6891
8516
  }
6892
8517
  }
6893
8518
  function detectPackageManager(projectRoot) {
6894
- if (existsSync(path12.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
6895
- if (existsSync(path12.join(projectRoot, "bun.lockb"))) return "bun";
6896
- if (existsSync(path12.join(projectRoot, "yarn.lock"))) return "yarn";
6897
- if (existsSync(path12.join(projectRoot, "package-lock.json"))) return "npm";
8519
+ if (existsSync(path15.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
8520
+ if (existsSync(path15.join(projectRoot, "bun.lockb"))) return "bun";
8521
+ if (existsSync(path15.join(projectRoot, "yarn.lock"))) return "yarn";
8522
+ if (existsSync(path15.join(projectRoot, "package-lock.json"))) return "npm";
6898
8523
  return "unknown";
6899
8524
  }
6900
8525
  function readPlaywrightVersion(pkg) {
@@ -6903,7 +8528,7 @@ function readPlaywrightVersion(pkg) {
6903
8528
  }
6904
8529
  function detectTestDir(projectRoot) {
6905
8530
  for (const candidate of ["tests", "e2e", "playwright"]) {
6906
- const full = path12.join(projectRoot, candidate);
8531
+ const full = path15.join(projectRoot, candidate);
6907
8532
  if (existsSync(full)) {
6908
8533
  try {
6909
8534
  if (statSync(full).isDirectory()) return candidate;
@@ -6929,12 +8554,12 @@ function runDoctorEnv() {
6929
8554
  return { exitCode: ExitCode.SUCCESS, envSnapshot: snapshotEnv() };
6930
8555
  }
6931
8556
  function runDoctor(opts = {}) {
6932
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
8557
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
6933
8558
  const checks = [];
6934
8559
  checks.push(checkNode());
6935
8560
  const apiKeyCheck = checkProviderApiKey(cwd);
6936
8561
  checks.push(apiKeyCheck);
6937
- const qaiosDir = path12.join(cwd, ".qaios");
8562
+ const qaiosDir = path15.join(cwd, ".qaios");
6938
8563
  const qaiosExists = existsSync(qaiosDir) && safeIsDir(qaiosDir);
6939
8564
  checks.push({
6940
8565
  name: ".qaios/ directory",
@@ -6942,7 +8567,7 @@ function runDoctor(opts = {}) {
6942
8567
  detail: qaiosExists ? `present at ${qaiosDir}` : `missing at ${qaiosDir}. Run \`qaios init\` first.`
6943
8568
  });
6944
8569
  if (qaiosExists) {
6945
- const dbPath = path12.join(qaiosDir, "workflows.db");
8570
+ const dbPath = path15.join(qaiosDir, "workflows.db");
6946
8571
  if (!existsSync(dbPath)) {
6947
8572
  checks.push({
6948
8573
  name: "SQLite DB",
@@ -6971,7 +8596,7 @@ function runDoctor(opts = {}) {
6971
8596
  function checkConfig(qaiosDir, qaiosExists) {
6972
8597
  const name = "config.yaml";
6973
8598
  if (!qaiosExists) return { name, status: "skip", detail: "skipped (no .qaios/)" };
6974
- const configPath = path12.join(qaiosDir, "config.yaml");
8599
+ const configPath = path15.join(qaiosDir, "config.yaml");
6975
8600
  if (!existsSync(configPath)) {
6976
8601
  return { name, status: "skip", detail: "not present (defaults will be used)" };
6977
8602
  }
@@ -7002,7 +8627,7 @@ function checkNode() {
7002
8627
  };
7003
8628
  }
7004
8629
  function resolveProviderFromConfig(cwd) {
7005
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
8630
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
7006
8631
  if (!existsSync(candidate)) return { provider: "anthropic" };
7007
8632
  try {
7008
8633
  const raw = parse(readFileSync(candidate, "utf-8"));
@@ -7103,7 +8728,7 @@ function checkPlaywright(cwd) {
7103
8728
  }
7104
8729
  function checkAxeCore(cwd) {
7105
8730
  try {
7106
- const pkgPath = path12.join(cwd, "package.json");
8731
+ const pkgPath = path15.join(cwd, "package.json");
7107
8732
  if (!existsSync(pkgPath)) {
7108
8733
  return { name: "axe-core (a11y)", status: "skip", detail: "no package.json" };
7109
8734
  }
@@ -7391,8 +9016,8 @@ function scopeFooterApi(scope) {
7391
9016
  }
7392
9017
  var STATUS_OR_RESPONSE_PATTERN = /\b[1-5]\d\d\b|response\./i;
7393
9018
  var MIN_SCENARIOS_PER_ENDPOINT = 3;
7394
- function endpointStepPattern(path18, method) {
7395
- const escaped = path18.split("/").map(
9019
+ function endpointStepPattern(path20, method) {
9020
+ const escaped = path20.split("/").map(
7396
9021
  (seg) => /^\{.*\}$/.test(seg) ? "[^/\\s?#]+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
7397
9022
  ).join("/");
7398
9023
  return new RegExp(`\\b${method}\\b[\\s\\S]*?${escaped}(?=$|[\\s?#])`, "i");
@@ -7702,6 +9327,15 @@ var ClassifyResultInput = z.object({
7702
9327
  designSpec: z.object({
7703
9328
  scenarioId: z.string().optional(),
7704
9329
  oracle: z.string().optional()
9330
+ }).optional(),
9331
+ // xray (v0.4): a compact, deterministic summary of how this test's network
9332
+ // traffic diverged from its baseline contract. Present only when xray ran.
9333
+ // Lets the classifier reach `backend_divergence` when the UI error (or even a
9334
+ // UI pass) is explained by a backend 4xx/5xx / status / shape change.
9335
+ networkDiff: z.object({
9336
+ summary: z.string(),
9337
+ // already truncated ≤~1,500 tokens by the diff engine
9338
+ hasFailSignal: z.boolean()
7705
9339
  }).optional()
7706
9340
  });
7707
9341
  function buildSystemPrompt5(projectMemory) {
@@ -7722,6 +9356,11 @@ Classification rubric:
7722
9356
  - expectation_outdated: app intentionally changed but test wasn't updated. Cues:
7723
9357
  app screenshot looks correct/intentional but assertion checks old text/structure.
7724
9358
  Difficult to detect without app context \u2014 be cautious here.
9359
+ - backend_divergence: the UI may look fine but the BACKEND diverged from its known-good
9360
+ baseline. Cues: the "Network divergence" section below shows an ERROR_RESPONSE (a
9361
+ 4xx/5xx or failed request) or STATUS_CHANGED on a call this test made \u2014 especially
9362
+ when the UI assertions passed. This is the class for "the page rendered but the API
9363
+ broke". Prefer this over real_failure when a network fail-signal explains the result.
7725
9364
  - unknown: cannot classify with confidence. Default if multiple categories plausible.
7726
9365
 
7727
9366
  Suggested action mapping:
@@ -7730,6 +9369,7 @@ Suggested action mapping:
7730
9369
  - environmental \u2192 investigate_env (do not file as defect against the app)
7731
9370
  - locator_broken \u2192 heal_locator
7732
9371
  - expectation_outdated \u2192 update_test (but flag for human review)
9372
+ - backend_divergence \u2192 create_defect (cite the offending identity key + diff class)
7733
9373
  - unknown \u2192 needs_human_review
7734
9374
 
7735
9375
  Project context:
@@ -7744,6 +9384,10 @@ function buildUserPrompt5(input) {
7744
9384
  const attachments = tr.attachments.length === 0 ? "(none)" : tr.attachments.join(", ");
7745
9385
  const historicalJson = JSON.stringify(tr.historicalRuns, null, 2);
7746
9386
  const oracle = input.designSpec?.oracle ?? "(unspecified)";
9387
+ const networkSection = input.networkDiff !== void 0 ? `
9388
+
9389
+ Network divergence (vs baseline contract${input.networkDiff.hasFailSignal ? ", FAIL-SIGNAL present" : ""}):
9390
+ ${input.networkDiff.summary}` : "";
7747
9391
  return `Test that failed:
7748
9392
  File: ${tr.file}
7749
9393
  Test: ${tr.name}
@@ -7761,7 +9405,7 @@ Attachments: ${attachments}
7761
9405
  Historical runs (most recent first):
7762
9406
  ${historicalJson}
7763
9407
 
7764
- Oracle (if available): ${oracle}
9408
+ Oracle (if available): ${oracle}${networkSection}
7765
9409
 
7766
9410
  Classify this failure.`;
7767
9411
  }
@@ -8681,7 +10325,7 @@ function resolveLlmClient(injected, llmConfig) {
8681
10325
 
8682
10326
  // src/commands/a11y.ts
8683
10327
  function loadConfig(cwd) {
8684
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
10328
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
8685
10329
  if (!existsSync(candidate)) return null;
8686
10330
  try {
8687
10331
  return parse(readFileSync(candidate, "utf-8"));
@@ -8691,7 +10335,7 @@ function loadConfig(cwd) {
8691
10335
  }
8692
10336
  var GLYPH = { ok: "\u2713", failed: "\u2717", warn: "\u26A0", issue: "\u2691" };
8693
10337
  async function runA11y(opts) {
8694
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
10338
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
8695
10339
  if (!opts.url || opts.url.trim().length === 0) {
8696
10340
  return {
8697
10341
  exitCode: ExitCode.USER_ERROR,
@@ -8701,7 +10345,7 @@ async function runA11y(opts) {
8701
10345
  }
8702
10346
  };
8703
10347
  }
8704
- const qaiosDir = path12.join(cwd, ".qaios");
10348
+ const qaiosDir = path15.join(cwd, ".qaios");
8705
10349
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
8706
10350
  return {
8707
10351
  exitCode: ExitCode.USER_ERROR,
@@ -8716,7 +10360,7 @@ async function runA11y(opts) {
8716
10360
  const mode = opts.mode ?? config?.mode ?? "LITE";
8717
10361
  const wcagLevel = opts.wcagLevel ?? "AA";
8718
10362
  const ownsStorage = opts.storage === void 0;
8719
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
10363
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
8720
10364
  const auditLogger = new AuditLogger(storage.db);
8721
10365
  const workflowsRepo = new WorkflowsRepository(storage.db);
8722
10366
  const llm = resolveLlmClient(opts.llm, config?.llm);
@@ -8783,11 +10427,11 @@ async function runA11y(opts) {
8783
10427
  ` ${GLYPH.ok} scan complete \u2014 ${violationCount} violation(s), ${incompleteCount} needs-review, ${bestPracticeCount} best-practice`
8784
10428
  );
8785
10429
  writeOut("");
8786
- const specsDir = path12.join(qaiosDir, "specs", workflowId);
10430
+ const specsDir = path15.join(qaiosDir, "specs", workflowId);
8787
10431
  if (findings.length === 0) {
8788
10432
  mkdirSync(specsDir, { recursive: true });
8789
10433
  writeFileSync(
8790
- path12.join(specsDir, "A11yReport.json"),
10434
+ path15.join(specsDir, "A11yReport.json"),
8791
10435
  JSON.stringify({ url: opts.url, wcagLevel, findings: [], issues: [] }, null, 2) + "\n"
8792
10436
  );
8793
10437
  writeOut("No accessibility issues found.");
@@ -8841,7 +10485,7 @@ async function runA11y(opts) {
8841
10485
  const issues = triaged?.issues ?? [];
8842
10486
  mkdirSync(specsDir, { recursive: true });
8843
10487
  writeFileSync(
8844
- path12.join(specsDir, "A11yReport.json"),
10488
+ path15.join(specsDir, "A11yReport.json"),
8845
10489
  JSON.stringify({ url: opts.url, wcagLevel, findings, issues }, null, 2) + "\n"
8846
10490
  );
8847
10491
  writeOut("");
@@ -8986,7 +10630,7 @@ async function fileIssueAsDefect(args) {
8986
10630
  };
8987
10631
  }
8988
10632
  function loadConfig2(cwd) {
8989
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
10633
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
8990
10634
  if (!existsSync(candidate)) return null;
8991
10635
  try {
8992
10636
  return parse(readFileSync(candidate, "utf-8"));
@@ -9004,7 +10648,7 @@ function renderTextLine(line) {
9004
10648
  return line + "\n";
9005
10649
  }
9006
10650
  async function runExplore(opts) {
9007
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
10651
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
9008
10652
  if (!opts.url || opts.url.trim().length === 0) {
9009
10653
  return {
9010
10654
  exitCode: ExitCode.USER_ERROR,
@@ -9014,7 +10658,7 @@ async function runExplore(opts) {
9014
10658
  }
9015
10659
  };
9016
10660
  }
9017
- const qaiosDir = path12.join(cwd, ".qaios");
10661
+ const qaiosDir = path15.join(cwd, ".qaios");
9018
10662
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
9019
10663
  return {
9020
10664
  exitCode: ExitCode.USER_ERROR,
@@ -9040,7 +10684,7 @@ async function runExplore(opts) {
9040
10684
  const memory = loadProjectMemory(cwd);
9041
10685
  const mode = opts.mode ?? config?.mode ?? "LITE";
9042
10686
  const ownsStorage = opts.storage === void 0;
9043
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
10687
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
9044
10688
  const auditLogger = new AuditLogger(storage.db);
9045
10689
  const workflowsRepo = new WorkflowsRepository(storage.db);
9046
10690
  const llm = resolveLlmClient(opts.llm, config?.llm);
@@ -9182,14 +10826,14 @@ async function runExplore(opts) {
9182
10826
  writeOut(` Areas: ${exploreOutput.charter.areasToProbe.join("; ")}`);
9183
10827
  writeOut(` Techniques: ${exploreOutput.charter.techniques.join(", ")}`);
9184
10828
  writeOut("");
9185
- const specsDir = path12.join(qaiosDir, "specs", workflowId);
10829
+ const specsDir = path15.join(qaiosDir, "specs", workflowId);
9186
10830
  mkdirSync(specsDir, { recursive: true });
9187
- const charterPath = path12.join(specsDir, "Charter.json");
10831
+ const charterPath = path15.join(specsDir, "Charter.json");
9188
10832
  writeFileSync(charterPath, JSON.stringify(exploreOutput.charter, null, 2) + "\n");
9189
- const notesPath = path12.join(specsDir, "SessionNotes.md");
10833
+ const notesPath = path15.join(specsDir, "SessionNotes.md");
9190
10834
  writeFileSync(notesPath, renderSessionNotesMarkdown(exploreOutput));
9191
- writeOut(`Persisted ${path12.relative(cwd, charterPath)}`);
9192
- writeOut(`Persisted ${path12.relative(cwd, notesPath)}`);
10835
+ writeOut(`Persisted ${path15.relative(cwd, charterPath)}`);
10836
+ writeOut(`Persisted ${path15.relative(cwd, notesPath)}`);
9193
10837
  writeOut("");
9194
10838
  if (opts.charterOnly === true) {
9195
10839
  finalize2("succeeded");
@@ -9383,7 +11027,7 @@ async function fileFindingAsDefect(args) {
9383
11027
  };
9384
11028
  }
9385
11029
  function loadConfig3(cwd) {
9386
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
11030
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
9387
11031
  if (!existsSync(candidate)) return null;
9388
11032
  try {
9389
11033
  return parse(readFileSync(candidate, "utf-8"));
@@ -9418,8 +11062,8 @@ function pickTarget(testResultsRepo, opts) {
9418
11062
  reason: "must pass a test file path or --last-failure."
9419
11063
  };
9420
11064
  }
9421
- const abs = path12.isAbsolute(opts.target) ? opts.target : path12.resolve(opts.cwd, opts.target);
9422
- const rel = path12.relative(opts.cwd, abs).split(path12.sep).join("/");
11065
+ const abs = path15.isAbsolute(opts.target) ? opts.target : path15.resolve(opts.cwd, opts.target);
11066
+ const rel = path15.relative(opts.cwd, abs).split(path15.sep).join("/");
9423
11067
  if (opts.scenarioId !== void 0 && opts.scenarioId.length > 0) {
9424
11068
  const row2 = testResultsRepo.mostRecentFailedForFileScenario(rel, opts.scenarioId);
9425
11069
  return row2 !== null ? { row: row2, reason: null } : {
@@ -9437,8 +11081,8 @@ function renderTextLine2(line) {
9437
11081
  return line + "\n";
9438
11082
  }
9439
11083
  async function runFix(opts) {
9440
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
9441
- const qaiosDir = path12.join(cwd, ".qaios");
11084
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
11085
+ const qaiosDir = path15.join(cwd, ".qaios");
9442
11086
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
9443
11087
  return {
9444
11088
  exitCode: ExitCode.USER_ERROR,
@@ -9449,7 +11093,7 @@ async function runFix(opts) {
9449
11093
  };
9450
11094
  }
9451
11095
  const ownsStorage = opts.storage === void 0;
9452
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11096
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
9453
11097
  const auditLogger = new AuditLogger(storage.db);
9454
11098
  const testResultsRepo = new TestResultsRepository(storage.db);
9455
11099
  const workflowsRepo = new WorkflowsRepository(storage.db);
@@ -9492,9 +11136,9 @@ async function runFix(opts) {
9492
11136
  ];
9493
11137
  let resolvedTestFile = tr.testFile;
9494
11138
  for (const dir of candidateDirs) {
9495
- const candidate = path12.isAbsolute(tr.testFile) ? tr.testFile : path12.join(cwd, dir, tr.testFile);
11139
+ const candidate = path15.isAbsolute(tr.testFile) ? tr.testFile : path15.join(cwd, dir, tr.testFile);
9496
11140
  if (existsSync(candidate)) {
9497
- resolvedTestFile = path12.isAbsolute(tr.testFile) ? tr.testFile : path12.join(dir, tr.testFile);
11141
+ resolvedTestFile = path15.isAbsolute(tr.testFile) ? tr.testFile : path15.join(dir, tr.testFile);
9498
11142
  break;
9499
11143
  }
9500
11144
  }
@@ -9748,7 +11392,7 @@ async function runFix(opts) {
9748
11392
  } else {
9749
11393
  writeOut(`${GLYPH3.failed} test still failing after patch`);
9750
11394
  if (opts.keepFailedPatch !== true && apply.oldContents !== null) {
9751
- const absPath = path12.isAbsolute(resolvedTestFile) ? resolvedTestFile : path12.resolve(cwd, resolvedTestFile);
11395
+ const absPath = path15.isAbsolute(resolvedTestFile) ? resolvedTestFile : path15.resolve(cwd, resolvedTestFile);
9752
11396
  try {
9753
11397
  writeFileSync(absPath, apply.oldContents, "utf-8");
9754
11398
  rolledBack = true;
@@ -9893,8 +11537,8 @@ function renderVerify(result, genesis, latest) {
9893
11537
  ].join("\n");
9894
11538
  }
9895
11539
  async function runHistory(opts) {
9896
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
9897
- const qaiosDir = path12.join(cwd, ".qaios");
11540
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
11541
+ const qaiosDir = path15.join(cwd, ".qaios");
9898
11542
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
9899
11543
  return {
9900
11544
  exitCode: ExitCode.USER_ERROR,
@@ -9905,7 +11549,7 @@ async function runHistory(opts) {
9905
11549
  };
9906
11550
  }
9907
11551
  const ownsStorage = opts.storage === void 0;
9908
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11552
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
9909
11553
  const auditLogger = new AuditLogger(storage.db);
9910
11554
  const workflowsRepo = new WorkflowsRepository(storage.db);
9911
11555
  const runsRepo = new RunsRepository(storage.db);
@@ -9939,13 +11583,13 @@ async function runHistory(opts) {
9939
11583
  }
9940
11584
  if (opts.exportPath !== void 0) {
9941
11585
  const entries = auditLogger.listEntries();
9942
- const absPath = path12.resolve(cwd, opts.exportPath);
9943
- mkdirSync(path12.dirname(absPath), { recursive: true });
11586
+ const absPath = path15.resolve(cwd, opts.exportPath);
11587
+ mkdirSync(path15.dirname(absPath), { recursive: true });
9944
11588
  const lines = entries.map((e) => JSON.stringify(e)).join("\n");
9945
11589
  writeFileSync(absPath, lines + (entries.length > 0 ? "\n" : ""));
9946
11590
  if (!opts.quiet) {
9947
11591
  process.stdout.write(
9948
- `Exported ${entries.length} audit entries to ${path12.relative(cwd, absPath)}
11592
+ `Exported ${entries.length} audit entries to ${path15.relative(cwd, absPath)}
9949
11593
  `
9950
11594
  );
9951
11595
  }
@@ -10012,8 +11656,8 @@ async function runHistory(opts) {
10012
11656
  if (ownsStorage) storage.close();
10013
11657
  }
10014
11658
  }
10015
- var __dirname$2 = path12.dirname(fileURLToPath(import.meta.url));
10016
- var DEFAULT_TEMPLATES_DIR = path12.resolve(__dirname$2);
11659
+ var __dirname$2 = path15.dirname(fileURLToPath(import.meta.url));
11660
+ var DEFAULT_TEMPLATES_DIR = path15.resolve(__dirname$2);
10017
11661
  function renderTemplate(template, vars) {
10018
11662
  let out = template;
10019
11663
  for (const [key, value] of Object.entries(vars)) {
@@ -10029,7 +11673,7 @@ function renderTemplate(template, vars) {
10029
11673
  return out;
10030
11674
  }
10031
11675
  function renderQaiosMd(vars, templatesDir = DEFAULT_TEMPLATES_DIR) {
10032
- const templatePath = path12.join(templatesDir, "QAIOS.md.template");
11676
+ const templatePath = path15.join(templatesDir, "QAIOS.md.template");
10033
11677
  const template = readFileSync(templatePath, "utf-8");
10034
11678
  return renderTemplate(template, vars);
10035
11679
  }
@@ -10044,11 +11688,12 @@ var QAIOS_GITIGNORE = `# QAIOS managed directory.
10044
11688
  cache/
10045
11689
  runs/
10046
11690
  diffs/
11691
+ xray/
10047
11692
  workflows.db-wal
10048
11693
  workflows.db-shm
10049
11694
  `;
10050
11695
  function runInit(opts = {}) {
10051
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
11696
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
10052
11697
  const detection = detectProject(cwd);
10053
11698
  if (!detection.looksLikeProject && !detection.isEmpty && !opts.force) {
10054
11699
  return {
@@ -10060,7 +11705,7 @@ function runInit(opts = {}) {
10060
11705
  detection
10061
11706
  };
10062
11707
  }
10063
- const qaiosDir = path12.join(cwd, ".qaios");
11708
+ const qaiosDir = path15.join(cwd, ".qaios");
10064
11709
  if (existsSync(qaiosDir) && !opts.force) {
10065
11710
  return {
10066
11711
  exitCode: ExitCode.USER_ERROR,
@@ -10103,7 +11748,7 @@ function runInit(opts = {}) {
10103
11748
  }
10104
11749
  });
10105
11750
  mkdirSync(qaiosDir, { recursive: true });
10106
- const dbPath = path12.join(qaiosDir, "workflows.db");
11751
+ const dbPath = path15.join(qaiosDir, "workflows.db");
10107
11752
  let migrations;
10108
11753
  try {
10109
11754
  const storage = Storage.open(dbPath, { skipMigrations: true });
@@ -10129,14 +11774,14 @@ Original error: ${raw}` : `Failed to initialise workflows.db: ${raw}`;
10129
11774
  };
10130
11775
  }
10131
11776
  const filesWritten = [];
10132
- filesWritten.push(path12.relative(cwd, dbPath));
10133
- const configPath = path12.join(qaiosDir, "config.yaml");
11777
+ filesWritten.push(path15.relative(cwd, dbPath));
11778
+ const configPath = path15.join(qaiosDir, "config.yaml");
10134
11779
  writeFileSync(configPath, stringify(config), "utf-8");
10135
- filesWritten.push(path12.relative(cwd, configPath));
10136
- const gitignorePath = path12.join(qaiosDir, ".gitignore");
11780
+ filesWritten.push(path15.relative(cwd, configPath));
11781
+ const gitignorePath = path15.join(qaiosDir, ".gitignore");
10137
11782
  writeFileSync(gitignorePath, QAIOS_GITIGNORE, "utf-8");
10138
- filesWritten.push(path12.relative(cwd, gitignorePath));
10139
- const qaiosMdPath = path12.join(cwd, "QAIOS.md");
11783
+ filesWritten.push(path15.relative(cwd, gitignorePath));
11784
+ const qaiosMdPath = path15.join(cwd, "QAIOS.md");
10140
11785
  const qaiosMd = renderQaiosMd({
10141
11786
  projectName: detection.projectName,
10142
11787
  stack: stackHint(detection),
@@ -10145,7 +11790,7 @@ Original error: ${raw}` : `Failed to initialise workflows.db: ${raw}`;
10145
11790
  });
10146
11791
  if (!existsSync(qaiosMdPath) || opts.force) {
10147
11792
  writeFileSync(qaiosMdPath, qaiosMd, "utf-8");
10148
- filesWritten.push(path12.relative(cwd, qaiosMdPath));
11793
+ filesWritten.push(path15.relative(cwd, qaiosMdPath));
10149
11794
  }
10150
11795
  return {
10151
11796
  exitCode: ExitCode.SUCCESS,
@@ -10179,9 +11824,9 @@ function isWritable(dir) {
10179
11824
  }
10180
11825
  }
10181
11826
  async function runConfig(opts) {
10182
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
10183
- const qaiosDir = path12.join(cwd, ".qaios");
10184
- const configPath = path12.join(qaiosDir, "config.yaml");
11827
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
11828
+ const qaiosDir = path15.join(cwd, ".qaios");
11829
+ const configPath = path15.join(qaiosDir, "config.yaml");
10185
11830
  if (!existsSync(qaiosDir)) {
10186
11831
  return {
10187
11832
  exitCode: ExitCode.USER_ERROR,
@@ -10501,8 +12146,8 @@ function validateAndWrite(configPath, text, writeOut) {
10501
12146
  return { exitCode: ExitCode.SUCCESS };
10502
12147
  }
10503
12148
  async function runMcp(opts) {
10504
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
10505
- const qaiosDir = path12.join(cwd, ".qaios");
12149
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
12150
+ const qaiosDir = path15.join(cwd, ".qaios");
10506
12151
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
10507
12152
  return {
10508
12153
  exitCode: ExitCode.USER_ERROR,
@@ -10513,7 +12158,7 @@ async function runMcp(opts) {
10513
12158
  };
10514
12159
  }
10515
12160
  const ownsStorage = opts.storage === void 0;
10516
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
12161
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
10517
12162
  const repo = new McpServersRepository(storage.db);
10518
12163
  const writeOut = (line) => {
10519
12164
  if (opts.quiet === true) return;
@@ -10726,7 +12371,7 @@ async function testServer(repo, opts, writeOut) {
10726
12371
  }
10727
12372
  }
10728
12373
  function loadLlmConfig(cwd) {
10729
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
12374
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
10730
12375
  if (!existsSync(candidate)) return void 0;
10731
12376
  try {
10732
12377
  const parsed = parse(readFileSync(candidate, "utf-8"));
@@ -10777,8 +12422,8 @@ async function applyDecision(args) {
10777
12422
  return { resumed: true };
10778
12423
  }
10779
12424
  async function runReview(opts) {
10780
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
10781
- const qaiosDir = path12.join(cwd, ".qaios");
12425
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
12426
+ const qaiosDir = path15.join(cwd, ".qaios");
10782
12427
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
10783
12428
  return {
10784
12429
  exitCode: ExitCode.USER_ERROR,
@@ -10792,7 +12437,7 @@ async function runReview(opts) {
10792
12437
  };
10793
12438
  }
10794
12439
  const ownsStorage = opts.storage === void 0;
10795
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
12440
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
10796
12441
  const auditLogger = new AuditLogger(storage.db);
10797
12442
  const gatesRepo = new GatesRepository(storage.db);
10798
12443
  const llm = resolveLlmClient(opts.llm, loadLlmConfig(cwd));
@@ -10897,7 +12542,7 @@ async function runReview(opts) {
10897
12542
  }
10898
12543
  }
10899
12544
  function loadRunConfig(cwd) {
10900
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
12545
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
10901
12546
  if (!existsSync(candidate)) return null;
10902
12547
  try {
10903
12548
  return parse(readFileSync(candidate, "utf-8"));
@@ -11006,9 +12651,32 @@ function exitCodeForOutcome(workflowStatus, runStatus, reason) {
11006
12651
  return ExitCode.INTERNAL;
11007
12652
  }
11008
12653
  }
12654
+ function readPassWithDivergence(db, workflowId) {
12655
+ let row;
12656
+ try {
12657
+ row = db.prepare(
12658
+ `SELECT payload_json FROM audit_log
12659
+ WHERE workflow_id = ? AND event = 'xray.diff'
12660
+ ORDER BY timestamp DESC LIMIT 1`
12661
+ ).get(workflowId);
12662
+ } catch {
12663
+ return null;
12664
+ }
12665
+ if (row?.payload_json === void 0) return null;
12666
+ try {
12667
+ const payload = JSON.parse(row.payload_json);
12668
+ if (payload.passWithDivergence !== true) return null;
12669
+ return { count: payload.count ?? 0, flagged: payload.flagged ?? [] };
12670
+ } catch {
12671
+ return null;
12672
+ }
12673
+ }
12674
+ function indent(s) {
12675
+ return s.split("\n").map((l) => ` ${l}`).join("\n");
12676
+ }
11009
12677
  async function runRun(opts) {
11010
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
11011
- const qaiosDir = path12.join(cwd, ".qaios");
12678
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
12679
+ const qaiosDir = path15.join(cwd, ".qaios");
11012
12680
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
11013
12681
  return {
11014
12682
  exitCode: ExitCode.USER_ERROR,
@@ -11019,7 +12687,7 @@ async function runRun(opts) {
11019
12687
  };
11020
12688
  }
11021
12689
  const ownsStorage = opts.storage === void 0;
11022
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
12690
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11023
12691
  const auditLogger = new AuditLogger(storage.db);
11024
12692
  const config = loadRunConfig(cwd);
11025
12693
  const llm = resolveLlmClient(opts.llm, config?.llm);
@@ -11044,6 +12712,19 @@ async function runRun(opts) {
11044
12712
  extraArgs.push(`--retries=${opts.retry}`);
11045
12713
  }
11046
12714
  if (extraArgs.length > 0) args["executionExtraArgs"] = extraArgs;
12715
+ const xrayCfg = config?.xray;
12716
+ const flagOn = opts.xray !== void 0 && opts.xray !== false;
12717
+ const enabled = flagOn || opts.xrayStrict === true || xrayCfg?.enabled === true;
12718
+ if (enabled) {
12719
+ const cfgLevel = xrayCfg?.level && xrayCfg.level !== "off" ? xrayCfg.level : "summary";
12720
+ const level = typeof opts.xray === "string" ? opts.xray : cfgLevel;
12721
+ const xrayArg = { level };
12722
+ if (typeof xrayCfg?.quiescenceMs === "number") xrayArg["quiescenceMs"] = xrayCfg.quiescenceMs;
12723
+ if (Array.isArray(xrayCfg?.volatileParams)) xrayArg["volatileParams"] = xrayCfg.volatileParams;
12724
+ if (Array.isArray(xrayCfg?.ignoreHosts)) xrayArg["ignoreHosts"] = xrayCfg.ignoreHosts;
12725
+ if (Array.isArray(xrayCfg?.redact)) xrayArg["redact"] = xrayCfg.redact;
12726
+ args["xray"] = xrayArg;
12727
+ }
11047
12728
  const capturePageSnapshot2 = opts.noHeal === true ? void 0 : opts.capturePageSnapshot ?? ((o) => capturePageSnapshot({ url: o.url, cwd: o.cwd }));
11048
12729
  const orchestrator = new Orchestrator({
11049
12730
  storage,
@@ -11096,7 +12777,22 @@ async function runRun(opts) {
11096
12777
  }
11097
12778
  const exec = result.outputs["execute.playwright"];
11098
12779
  const runStatus = exec?.runStatus;
11099
- const exitCode = exitCodeForOutcome(result.status, runStatus, result.blockedReason);
12780
+ let exitCode = exitCodeForOutcome(result.status, runStatus, result.blockedReason);
12781
+ if (exitCode === ExitCode.SUCCESS) {
12782
+ const divergence = readPassWithDivergence(storage.db, result.workflowId);
12783
+ if (divergence !== null) {
12784
+ if (opts.quiet !== true) {
12785
+ process.stderr.write(
12786
+ `\u26A0 xray: ${divergence.count} test(s) passed but the backend diverged from baseline:
12787
+ ` + divergence.flagged.map((f) => ` \xB7 ${f.test}
12788
+ ${indent(f.summary)}`).join("\n") + "\n"
12789
+ );
12790
+ }
12791
+ if (opts.xrayStrict === true) {
12792
+ exitCode = ExitCode.PASS_WITH_NETWORK_DIVERGENCE;
12793
+ }
12794
+ }
12795
+ }
11100
12796
  return {
11101
12797
  exitCode,
11102
12798
  workflowId: result.workflowId,
@@ -11113,8 +12809,574 @@ async function runRun(opts) {
11113
12809
  if (ownsStorage) storage.close();
11114
12810
  }
11115
12811
  }
12812
+ path15.dirname(fileURLToPath(import.meta.url));
12813
+ function rowToNetRun2(row) {
12814
+ return NetRunRow.parse({
12815
+ runId: row.run_id,
12816
+ workflowId: row.workflow_id,
12817
+ testId: row.test_id,
12818
+ browser: row.browser,
12819
+ tier: row.tier,
12820
+ captureLevel: row.capture_level,
12821
+ startedAt: row.started_at,
12822
+ isGreen: row.is_green === 1,
12823
+ isBaseline: row.is_baseline === 1
12824
+ });
12825
+ }
12826
+ var NetRunsRepository2 = class {
12827
+ constructor(db) {
12828
+ this.db = db;
12829
+ }
12830
+ db;
12831
+ create(input) {
12832
+ const r = NetRunRow.parse(input);
12833
+ this.db.prepare(
12834
+ `INSERT INTO net_runs
12835
+ (run_id, workflow_id, test_id, browser, tier, capture_level,
12836
+ started_at, is_green, is_baseline)
12837
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
12838
+ ).run(
12839
+ r.runId,
12840
+ r.workflowId,
12841
+ r.testId,
12842
+ r.browser,
12843
+ r.tier,
12844
+ r.captureLevel,
12845
+ r.startedAt,
12846
+ r.isGreen ? 1 : 0,
12847
+ r.isBaseline ? 1 : 0
12848
+ );
12849
+ return r;
12850
+ }
12851
+ findById(runId) {
12852
+ const row = this.db.prepare("SELECT * FROM net_runs WHERE run_id = ?").get(runId);
12853
+ return row ? rowToNetRun2(row) : null;
12854
+ }
12855
+ listByWorkflow(workflowId) {
12856
+ const rows = this.db.prepare("SELECT * FROM net_runs WHERE workflow_id = ? ORDER BY started_at DESC").all(workflowId);
12857
+ return rows.map(rowToNetRun2);
12858
+ }
12859
+ /**
12860
+ * Runs for a test, newest first. Used by baseline selection (last green per
12861
+ * test+browser) and volatility learning (compare consecutive green runs).
12862
+ */
12863
+ listByTest(testId, browser) {
12864
+ const sql = browser === void 0 ? "SELECT * FROM net_runs WHERE test_id = ? ORDER BY started_at DESC" : "SELECT * FROM net_runs WHERE test_id = ? AND browser = ? ORDER BY started_at DESC";
12865
+ const rows = browser === void 0 ? this.db.prepare(sql).all(testId) : this.db.prepare(sql).all(testId, browser);
12866
+ return rows.map(rowToNetRun2);
12867
+ }
12868
+ /** Flip green/baseline flags as the run is finalized / a baseline is pinned. */
12869
+ update(runId, patch) {
12870
+ const fields = [];
12871
+ const params = [];
12872
+ if (patch.isGreen !== void 0) {
12873
+ fields.push("is_green = ?");
12874
+ params.push(patch.isGreen ? 1 : 0);
12875
+ }
12876
+ if (patch.isBaseline !== void 0) {
12877
+ fields.push("is_baseline = ?");
12878
+ params.push(patch.isBaseline ? 1 : 0);
12879
+ }
12880
+ if (fields.length === 0) return this.findById(runId);
12881
+ params.push(runId);
12882
+ this.db.prepare(`UPDATE net_runs SET ${fields.join(", ")} WHERE run_id = ?`).run(...params);
12883
+ return this.findById(runId);
12884
+ }
12885
+ /** Clear the baseline pin for every run of a (test, browser). */
12886
+ clearBaseline(testId, browser) {
12887
+ this.db.prepare("UPDATE net_runs SET is_baseline = 0 WHERE test_id = ? AND browser = ?").run(testId, browser);
12888
+ }
12889
+ delete(runId) {
12890
+ this.db.prepare("DELETE FROM net_runs WHERE run_id = ?").run(runId);
12891
+ }
12892
+ };
12893
+ function rowToNetRequest2(row) {
12894
+ return NetRequestRow.parse({
12895
+ id: row.id,
12896
+ runId: row.run_id,
12897
+ stepId: row.step_id,
12898
+ attribution: row.attribution,
12899
+ targetType: row.target_type,
12900
+ method: row.method,
12901
+ urlRaw: row.url_raw,
12902
+ urlTemplate: row.url_template,
12903
+ gqlOperation: row.gql_operation,
12904
+ identityKey: row.identity_key,
12905
+ status: row.status,
12906
+ errorText: row.error_text,
12907
+ mime: row.mime,
12908
+ reqHash: row.req_hash,
12909
+ respHash: row.resp_hash,
12910
+ shapeHash: row.shape_hash,
12911
+ reqBodyRef: row.req_body_ref,
12912
+ respBodyRef: row.resp_body_ref,
12913
+ bodyStatus: row.body_status,
12914
+ redirectsJson: row.redirects_json,
12915
+ timingsJson: row.timings_json,
12916
+ initiatorJson: row.initiator_json,
12917
+ wallStart: row.wall_start
12918
+ });
12919
+ }
12920
+ var NetRequestsRepository2 = class {
12921
+ constructor(db) {
12922
+ this.db = db;
12923
+ }
12924
+ db;
12925
+ create(input) {
12926
+ const r = NetRequestRow.parse(input);
12927
+ this.db.prepare(
12928
+ `INSERT INTO net_requests
12929
+ (id, run_id, step_id, attribution, target_type, method, url_raw,
12930
+ url_template, gql_operation, identity_key, status, error_text, mime,
12931
+ req_hash, resp_hash, shape_hash, req_body_ref, resp_body_ref,
12932
+ body_status, redirects_json, timings_json, initiator_json, wall_start)
12933
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
12934
+ ).run(
12935
+ r.id,
12936
+ r.runId,
12937
+ r.stepId,
12938
+ r.attribution,
12939
+ r.targetType,
12940
+ r.method,
12941
+ r.urlRaw,
12942
+ r.urlTemplate,
12943
+ r.gqlOperation,
12944
+ r.identityKey,
12945
+ r.status,
12946
+ r.errorText,
12947
+ r.mime,
12948
+ r.reqHash,
12949
+ r.respHash,
12950
+ r.shapeHash,
12951
+ r.reqBodyRef,
12952
+ r.respBodyRef,
12953
+ r.bodyStatus,
12954
+ r.redirectsJson,
12955
+ r.timingsJson,
12956
+ r.initiatorJson,
12957
+ r.wallStart
12958
+ );
12959
+ return r;
12960
+ }
12961
+ /** Bulk insert in one transaction — a run can produce hundreds of requests. */
12962
+ createMany(rows) {
12963
+ const insert = this.db.transaction((items) => {
12964
+ for (const item of items) this.create(item);
12965
+ });
12966
+ insert(rows);
12967
+ }
12968
+ listByRun(runId) {
12969
+ const rows = this.db.prepare("SELECT * FROM net_requests WHERE run_id = ? ORDER BY wall_start ASC").all(runId);
12970
+ return rows.map(rowToNetRequest2);
12971
+ }
12972
+ listByRunStep(runId, stepId) {
12973
+ const sql = stepId === null ? "SELECT * FROM net_requests WHERE run_id = ? AND step_id IS NULL ORDER BY wall_start ASC" : "SELECT * FROM net_requests WHERE run_id = ? AND step_id = ? ORDER BY wall_start ASC";
12974
+ const rows = stepId === null ? this.db.prepare(sql).all(runId) : this.db.prepare(sql).all(runId, stepId);
12975
+ return rows.map(rowToNetRequest2);
12976
+ }
12977
+ deleteByRun(runId) {
12978
+ this.db.prepare("DELETE FROM net_requests WHERE run_id = ?").run(runId);
12979
+ }
12980
+ };
12981
+ function buildRunGraph2(requests) {
12982
+ const graph = /* @__PURE__ */ new Map();
12983
+ for (const r of requests) {
12984
+ const key = r.identityKey;
12985
+ const existing = graph.get(key);
12986
+ if (existing === void 0) {
12987
+ graph.set(key, {
12988
+ identityKey: r.identityKey,
12989
+ stepId: r.stepId,
12990
+ attribution: r.attribution,
12991
+ method: r.method,
12992
+ status: r.status,
12993
+ errorText: r.errorText,
12994
+ shapeHash: r.shapeHash,
12995
+ respHash: r.respHash,
12996
+ count: 1
12997
+ });
12998
+ } else {
12999
+ existing.count += 1;
13000
+ if (isErrorStatus3(r.status) && !isErrorStatus3(existing.status)) {
13001
+ existing.status = r.status;
13002
+ existing.errorText = r.errorText;
13003
+ }
13004
+ }
13005
+ }
13006
+ return graph;
13007
+ }
13008
+ function isErrorStatus3(status) {
13009
+ return status === null || status >= 400 || status < 0;
13010
+ }
13011
+ var BaselineSelector2 = class {
13012
+ netRuns;
13013
+ netRequests;
13014
+ constructor(db) {
13015
+ this.netRuns = new NetRunsRepository2(db);
13016
+ this.netRequests = new NetRequestsRepository2(db);
13017
+ }
13018
+ /**
13019
+ * The baseline run for a (test, browser): the pinned baseline if one exists,
13020
+ * else the most recent green run. Returns null if there's no green run yet.
13021
+ * `excludeRunId` skips a candidate run (so a run never baselines against itself).
13022
+ */
13023
+ select(testId, browser, excludeRunId) {
13024
+ const runs = this.netRuns.listByTest(testId, browser);
13025
+ const pinned = runs.find((r) => r.isBaseline && r.runId !== excludeRunId);
13026
+ if (pinned !== void 0) return pinned;
13027
+ return runs.find((r) => r.isGreen && r.runId !== excludeRunId) ?? null;
13028
+ }
13029
+ /** Build the causal graph for a run by id (or null if the run is unknown). */
13030
+ graphFor(runId) {
13031
+ if (this.netRuns.findById(runId) === null) return null;
13032
+ return buildRunGraph2(this.netRequests.listByRun(runId));
13033
+ }
13034
+ /** Pin a run as the baseline for its (test, browser), clearing any prior pin. */
13035
+ pin(runId) {
13036
+ const run = this.netRuns.findById(runId);
13037
+ if (run === null) return null;
13038
+ this.netRuns.clearBaseline(run.testId, run.browser);
13039
+ return this.netRuns.update(runId, { isBaseline: true });
13040
+ }
13041
+ /** Clear the baseline pin for a (test, browser). */
13042
+ clear(testId, browser) {
13043
+ this.netRuns.clearBaseline(testId, browser);
13044
+ }
13045
+ };
13046
+ var FAIL_SIGNAL_CLASSES2 = /* @__PURE__ */ new Set([
13047
+ "ERROR_RESPONSE",
13048
+ "STATUS_CHANGED"
13049
+ ]);
13050
+ var SEVERITY2 = {
13051
+ ERROR_RESPONSE: 6,
13052
+ STATUS_CHANGED: 5,
13053
+ MISSING_CALL: 4,
13054
+ NEW_CALL: 3,
13055
+ SCHEMA_CHANGED: 2,
13056
+ VALUE_CHANGED: 1
13057
+ };
13058
+ function statusClass2(status) {
13059
+ if (status === null || status <= 0) return "unknown";
13060
+ if (status >= 500) return "5xx";
13061
+ if (status >= 400) return "4xx";
13062
+ if (status >= 300) return "3xx";
13063
+ if (status >= 200) return "2xx";
13064
+ return "other";
13065
+ }
13066
+ function isErrorStatus22(status) {
13067
+ return status !== null && status >= 400;
13068
+ }
13069
+ function isUnknownStatus2(status, errorText) {
13070
+ return errorText === null && (status === null || status <= 0);
13071
+ }
13072
+ function diffGraphs2(run, baseline, opts = {}) {
13073
+ const presenceUnstable = new Set(
13074
+ (opts.volatility ?? []).filter((v) => v.kind === "presence").map((v) => v.identityKey)
13075
+ );
13076
+ const valueVolatile = new Set(
13077
+ (opts.volatility ?? []).filter((v) => v.kind === "value").map((v) => v.identityKey)
13078
+ );
13079
+ const backgroundFailSignal = opts.backgroundIsFailSignal === true;
13080
+ const divergences = [];
13081
+ const emit2 = (diffClass, e, detail) => {
13082
+ const isFail = FAIL_SIGNAL_CLASSES2.has(diffClass) && (backgroundFailSignal || e.attribution !== "background");
13083
+ divergences.push({
13084
+ diffClass,
13085
+ identityKey: e.identityKey,
13086
+ stepId: e.stepId,
13087
+ attribution: e.attribution,
13088
+ detail,
13089
+ failSignal: isFail
13090
+ });
13091
+ };
13092
+ for (const [key, entry] of run) {
13093
+ const base = baseline.get(key);
13094
+ if (isUnknownStatus2(entry.status, entry.errorText)) continue;
13095
+ if (isErrorStatus22(entry.status) || entry.errorText !== null) {
13096
+ const ui = opts.uiPassed === true ? " (UI assertions passed)" : "";
13097
+ emit2(
13098
+ "ERROR_RESPONSE",
13099
+ entry,
13100
+ `${describe2(entry)} returned ${entry.errorText ?? entry.status ?? "error"}${ui}`
13101
+ );
13102
+ continue;
13103
+ }
13104
+ if (base !== void 0) {
13105
+ const runClass = statusClass2(entry.status);
13106
+ const baseClass = statusClass2(base.status);
13107
+ if (runClass !== "unknown" && baseClass !== "unknown" && runClass !== baseClass) {
13108
+ emit2(
13109
+ "STATUS_CHANGED",
13110
+ entry,
13111
+ `${describe2(entry)} status ${base.status ?? "\u2014"} \u2192 ${entry.status ?? "\u2014"}`
13112
+ );
13113
+ } else if (entry.shapeHash !== null && base.shapeHash !== null && entry.shapeHash !== base.shapeHash) {
13114
+ emit2("SCHEMA_CHANGED", entry, `${describe2(entry)} response shape changed`);
13115
+ } else if (entry.respHash !== null && base.respHash !== null && entry.respHash !== base.respHash && !valueVolatile.has(key)) {
13116
+ emit2("VALUE_CHANGED", entry, `${describe2(entry)} response value changed`);
13117
+ }
13118
+ } else if (!presenceUnstable.has(key)) {
13119
+ emit2("NEW_CALL", entry, `${describe2(entry)} is new vs baseline`);
13120
+ }
13121
+ }
13122
+ for (const [key, base] of baseline) {
13123
+ if (!run.has(key) && !presenceUnstable.has(key)) {
13124
+ emit2("MISSING_CALL", base, `${describe2(base)} was expected but did not occur`);
13125
+ }
13126
+ }
13127
+ divergences.sort(
13128
+ (a, b) => SEVERITY2[b.diffClass] - SEVERITY2[a.diffClass] || a.identityKey.localeCompare(b.identityKey)
13129
+ );
13130
+ return {
13131
+ divergences,
13132
+ hasFailSignal: divergences.some((d) => d.failSignal)
13133
+ };
13134
+ }
13135
+ function describe2(e) {
13136
+ const parts = e.identityKey.split("|");
13137
+ return parts.length >= 3 ? `${parts[0]} ${parts[1]}${parts[2]}` : e.identityKey;
13138
+ }
13139
+
13140
+ // src/commands/xray.ts
13141
+ function write(s) {
13142
+ process.stdout.write(s.endsWith("\n") ? s : s + "\n");
13143
+ }
13144
+ function userError(code, message) {
13145
+ return { exitCode: ExitCode.USER_ERROR, error: { code, message } };
13146
+ }
13147
+ function renderRun(run, requests) {
13148
+ const lines = [];
13149
+ const greenMark = run.isGreen ? "\u2713 green" : "\xB7 not-green";
13150
+ const baseMark = run.isBaseline ? " \u2605 baseline" : "";
13151
+ lines.push("");
13152
+ lines.push(`\u25B8 ${run.testId}`);
13153
+ lines.push(
13154
+ ` run ${run.runId} \xB7 ${run.browser} (tier ${run.tier}) \xB7 ${run.captureLevel} \xB7 ${greenMark}${baseMark} \xB7 ${requests.length} requests`
13155
+ );
13156
+ const byStep = /* @__PURE__ */ new Map();
13157
+ for (const r of requests) {
13158
+ const key = r.stepId ?? "(background)";
13159
+ const list = byStep.get(key) ?? [];
13160
+ list.push(r);
13161
+ byStep.set(key, list);
13162
+ }
13163
+ for (const [stepKey, reqs] of byStep) {
13164
+ const stepLabel = stepKey === "(background)" ? "background / setup" : `step ${stepKey.slice(-6)}`;
13165
+ lines.push(` ${stepLabel}`);
13166
+ for (const r of reqs) {
13167
+ const status = r.status === null ? r.errorText ? `ERR ${r.errorText}` : "\u2014" : String(r.status);
13168
+ const flag = statusFlag(r);
13169
+ const gql = r.gqlOperation ? ` {gql:${r.gqlOperation}}` : "";
13170
+ lines.push(
13171
+ ` ${flag}${r.method.padEnd(5)} ${status.padEnd(14)} ${r.identityKey.split("|").slice(1).join(" ")}${gql} [${r.attribution}]`
13172
+ );
13173
+ }
13174
+ }
13175
+ return lines.join("\n");
13176
+ }
13177
+ function statusFlag(r) {
13178
+ if (r.errorText) return "\u2717 ";
13179
+ if (typeof r.status === "number" && r.status >= 400) return "\u2717 ";
13180
+ return " ";
13181
+ }
13182
+ async function runXray(opts) {
13183
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
13184
+ const qaiosDir = path15.join(cwd, ".qaios");
13185
+ if (!existsSync(qaiosDir) && opts.storage === void 0) {
13186
+ return userError(
13187
+ "qaios.xray.not_initialized",
13188
+ `${cwd} is not a QAIOS project \u2014 run \`qaios init\` first.`
13189
+ );
13190
+ }
13191
+ const ownsStorage = opts.storage === void 0;
13192
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
13193
+ try {
13194
+ switch (opts.verb) {
13195
+ case "show":
13196
+ return showVerb(storage, opts);
13197
+ case "diff":
13198
+ return diffVerb(storage, opts);
13199
+ case "baseline":
13200
+ return baselineVerb(storage, opts);
13201
+ case "volatility":
13202
+ return volatilityVerb(storage, opts);
13203
+ default:
13204
+ return userError("qaios.xray.unknown_verb", `Unknown xray verb: ${String(opts.verb)}`);
13205
+ }
13206
+ } finally {
13207
+ if (ownsStorage) storage.close();
13208
+ }
13209
+ }
13210
+ function showVerb(storage, opts) {
13211
+ const netRuns = new NetRunsRepository(storage.db);
13212
+ const netRequests = new NetRequestsRepository(storage.db);
13213
+ let runs = [];
13214
+ if (opts.target !== void 0) {
13215
+ const single = netRuns.findById(opts.target);
13216
+ runs = single ? [single] : netRuns.listByWorkflow(opts.target);
13217
+ }
13218
+ if (runs.length === 0) {
13219
+ const hint = opts.target ? ` for "${opts.target}"` : "";
13220
+ return userError(
13221
+ "qaios.xray.no_capture",
13222
+ `No xray capture found${hint}. Run \`qaios run --xray\` first, then \`qaios xray show <runId|workflowId>\`.`
13223
+ );
13224
+ }
13225
+ if (opts.json === true) {
13226
+ write(
13227
+ JSON.stringify(
13228
+ runs.map((run) => ({ run, requests: netRequests.listByRun(run.runId) })),
13229
+ null,
13230
+ 2
13231
+ )
13232
+ );
13233
+ return { exitCode: ExitCode.SUCCESS };
13234
+ }
13235
+ if (opts.quiet !== true) {
13236
+ write(`xray capture \u2014 ${runs.length} run(s)`);
13237
+ for (const run of runs) write(renderRun(run, netRequests.listByRun(run.runId)));
13238
+ write("");
13239
+ }
13240
+ return { exitCode: ExitCode.SUCCESS };
13241
+ }
13242
+ function diffVerb(storage, opts) {
13243
+ const netRuns = new NetRunsRepository(storage.db);
13244
+ const netRequests = new NetRequestsRepository(storage.db);
13245
+ const volatility = new NetVolatilityRepository(storage.db);
13246
+ const baselines = new BaselineSelector2(storage.db);
13247
+ if (opts.target === void 0) {
13248
+ return userError(
13249
+ "qaios.xray.diff_usage",
13250
+ "Usage: qaios xray diff <runA> [runB]. Omit runB to diff against the baseline."
13251
+ );
13252
+ }
13253
+ const runA = netRuns.findById(opts.target);
13254
+ if (runA === null) {
13255
+ return userError("qaios.xray.run_not_found", `No captured run with id "${opts.target}".`);
13256
+ }
13257
+ let runB;
13258
+ if (opts.target2 !== void 0) {
13259
+ runB = netRuns.findById(opts.target2);
13260
+ if (runB === null) {
13261
+ return userError("qaios.xray.run_not_found", `No captured run with id "${opts.target2}".`);
13262
+ }
13263
+ } else {
13264
+ runB = baselines.select(runA.testId, runA.browser, runA.runId);
13265
+ if (runB === null) {
13266
+ return userError(
13267
+ "qaios.xray.no_baseline",
13268
+ `No baseline (green run) for "${runA.testId}" yet \u2014 pass an explicit second run, or capture a green run first.`
13269
+ );
13270
+ }
13271
+ }
13272
+ const runGraph = buildRunGraph2(netRequests.listByRun(runA.runId));
13273
+ const baseGraph = buildRunGraph2(netRequests.listByRun(runB.runId));
13274
+ const result = diffGraphs2(runGraph, baseGraph, {
13275
+ volatility: volatility.listByTest(runA.testId),
13276
+ uiPassed: runA.isGreen
13277
+ });
13278
+ if (opts.json === true) {
13279
+ write(JSON.stringify({ runA: runA.runId, runB: runB.runId, ...result }, null, 2));
13280
+ return { exitCode: ExitCode.SUCCESS };
13281
+ }
13282
+ if (opts.quiet !== true) {
13283
+ write(`xray diff \u2014 ${runA.runId} vs ${runB.runId} (${runA.testId})`);
13284
+ if (result.divergences.length === 0) {
13285
+ write(" \u2713 no divergences \u2014 backend contract matches the baseline.");
13286
+ } else {
13287
+ for (const d of result.divergences) {
13288
+ const glyph = d.failSignal ? "\u2717" : "\u26A0";
13289
+ write(` ${glyph} [${d.diffClass}] ${d.detail} {${d.attribution}}`);
13290
+ }
13291
+ }
13292
+ write("");
13293
+ }
13294
+ return { exitCode: ExitCode.SUCCESS };
13295
+ }
13296
+ function baselineVerb(storage, opts) {
13297
+ const baselines = new BaselineSelector2(storage.db);
13298
+ const netRuns = new NetRunsRepository(storage.db);
13299
+ const sub = opts.sub ?? "set";
13300
+ if (sub === "set") {
13301
+ if (opts.target === void 0) {
13302
+ return userError("qaios.xray.baseline_usage", "Usage: qaios xray baseline set <runId>.");
13303
+ }
13304
+ const pinned = baselines.pin(opts.target);
13305
+ if (pinned === null) {
13306
+ return userError("qaios.xray.run_not_found", `No captured run with id "${opts.target}".`);
13307
+ }
13308
+ if (opts.quiet !== true) {
13309
+ write(`\u2713 baseline pinned: ${pinned.testId} (${pinned.browser}) \u2192 run ${pinned.runId}`);
13310
+ }
13311
+ return { exitCode: ExitCode.SUCCESS };
13312
+ }
13313
+ if (sub === "clear") {
13314
+ if (opts.target === void 0) {
13315
+ return userError(
13316
+ "qaios.xray.baseline_usage",
13317
+ "Usage: qaios xray baseline clear <runId|testId>."
13318
+ );
13319
+ }
13320
+ const run = netRuns.findById(opts.target);
13321
+ if (run === null) {
13322
+ return userError(
13323
+ "qaios.xray.run_not_found",
13324
+ `Pass a run id to clear its (test, browser) baseline.`
13325
+ );
13326
+ }
13327
+ baselines.clear(run.testId, run.browser);
13328
+ if (opts.quiet !== true) write(`\u2713 baseline cleared for ${run.testId} (${run.browser}).`);
13329
+ return { exitCode: ExitCode.SUCCESS };
13330
+ }
13331
+ return userError("qaios.xray.baseline_usage", `Unknown baseline action "${sub}" (set|clear).`);
13332
+ }
13333
+ function volatilityVerb(storage, opts) {
13334
+ const volatility = new NetVolatilityRepository(storage.db);
13335
+ const sub = opts.sub ?? "show";
13336
+ if (sub === "reset") {
13337
+ const n = volatility.reset(opts.target);
13338
+ if (opts.quiet !== true) {
13339
+ write(
13340
+ `\u2713 cleared ${n} learned volatility entr${n === 1 ? "y" : "ies"}${opts.target ? ` for ${opts.target}` : ""}.`
13341
+ );
13342
+ }
13343
+ return { exitCode: ExitCode.SUCCESS };
13344
+ }
13345
+ if (sub === "show") {
13346
+ if (opts.target === void 0) {
13347
+ return userError(
13348
+ "qaios.xray.volatility_usage",
13349
+ "Usage: qaios xray volatility show <testId> | reset [testId]."
13350
+ );
13351
+ }
13352
+ const rows = volatility.listByTest(opts.target);
13353
+ if (opts.json === true) {
13354
+ write(JSON.stringify(rows, null, 2));
13355
+ return { exitCode: ExitCode.SUCCESS };
13356
+ }
13357
+ if (opts.quiet !== true) {
13358
+ if (rows.length === 0) {
13359
+ const greenRuns = new NetRunsRepository(storage.db).listByTest(opts.target).filter((r) => r.isGreen).length;
13360
+ write(
13361
+ greenRuns >= 2 ? `No volatility learned for "${opts.target}" \u2014 its ${greenRuns} green runs were identical (a stable contract, nothing to relax).` : `No learned volatility for "${opts.target}" yet \u2014 needs \u22652 green runs (have ${greenRuns}).`
13362
+ );
13363
+ } else {
13364
+ write(`xray volatility \u2014 ${opts.target} (${rows.length} learned)`);
13365
+ for (const r of rows) {
13366
+ write(` \xB7 [${r.kind}] ${r.identityKey.split("|").slice(0, 3).join(" ")}`);
13367
+ }
13368
+ }
13369
+ write("");
13370
+ }
13371
+ return { exitCode: ExitCode.SUCCESS };
13372
+ }
13373
+ return userError(
13374
+ "qaios.xray.volatility_usage",
13375
+ `Unknown volatility action "${sub}" (show|reset).`
13376
+ );
13377
+ }
11116
13378
  function loadConfig4(cwd) {
11117
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
13379
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
11118
13380
  if (!existsSync(candidate)) return null;
11119
13381
  try {
11120
13382
  return parse(readFileSync(candidate, "utf-8"));
@@ -11160,8 +13422,8 @@ function loadSnapshotSpec(specPath) {
11160
13422
  };
11161
13423
  }
11162
13424
  async function runSnapshotCapture(opts) {
11163
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
11164
- const qaiosDir = path12.join(cwd, ".qaios");
13425
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
13426
+ const qaiosDir = path15.join(cwd, ".qaios");
11165
13427
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
11166
13428
  return {
11167
13429
  exitCode: ExitCode.USER_ERROR,
@@ -11180,7 +13442,7 @@ async function runSnapshotCapture(opts) {
11180
13442
  }
11181
13443
  };
11182
13444
  }
11183
- const specAbs = path12.isAbsolute(opts.spec) ? opts.spec : path12.resolve(cwd, opts.spec);
13445
+ const specAbs = path15.isAbsolute(opts.spec) ? opts.spec : path15.resolve(cwd, opts.spec);
11184
13446
  if (!existsSync(specAbs)) {
11185
13447
  return {
11186
13448
  exitCode: ExitCode.USER_ERROR,
@@ -11241,7 +13503,7 @@ async function runSnapshotCapture(opts) {
11241
13503
  };
11242
13504
  }
11243
13505
  const ownsStorage = opts.storage === void 0;
11244
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
13506
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11245
13507
  const auditLogger = new AuditLogger(storage.db);
11246
13508
  const baselineRepo = new VisualBaselinesRepository(storage.db);
11247
13509
  if (opts.update !== true) {
@@ -11329,8 +13591,8 @@ function exitCodeForCheck(summary) {
11329
13591
  return ExitCode.SUCCESS;
11330
13592
  }
11331
13593
  async function runSnapshotCheck(opts) {
11332
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
11333
- const qaiosDir = path12.join(cwd, ".qaios");
13594
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
13595
+ const qaiosDir = path15.join(cwd, ".qaios");
11334
13596
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
11335
13597
  return {
11336
13598
  exitCode: ExitCode.USER_ERROR,
@@ -11352,7 +13614,7 @@ async function runSnapshotCheck(opts) {
11352
13614
  };
11353
13615
  }
11354
13616
  const ownsStorage = opts.storage === void 0;
11355
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
13617
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11356
13618
  const auditLogger = new AuditLogger(storage.db);
11357
13619
  const baselineRepo = new VisualBaselinesRepository(storage.db);
11358
13620
  const llm = resolveLlmClient(opts.llm, config?.llm);
@@ -11500,15 +13762,15 @@ function autoDecide(item, threshold) {
11500
13762
  function applyReviewDecision(args) {
11501
13763
  const now = (/* @__PURE__ */ new Date()).toISOString();
11502
13764
  if (args.action === "approve") {
11503
- const currentAbs = path12.isAbsolute(args.diff.currentImagePath) ? args.diff.currentImagePath : path12.join(args.cwd, args.diff.currentImagePath);
13765
+ const currentAbs = path15.isAbsolute(args.diff.currentImagePath) ? args.diff.currentImagePath : path15.join(args.cwd, args.diff.currentImagePath);
11504
13766
  if (!existsSync(currentAbs)) {
11505
13767
  return {
11506
13768
  applied: false,
11507
13769
  reason: `current image not found at ${currentAbs} \u2014 cannot copy to baseline.`
11508
13770
  };
11509
13771
  }
11510
- const baselineAbs = path12.isAbsolute(args.baseline.imagePath) ? args.baseline.imagePath : path12.join(args.cwd, args.baseline.imagePath);
11511
- mkdirSync(path12.dirname(baselineAbs), { recursive: true });
13772
+ const baselineAbs = path15.isAbsolute(args.baseline.imagePath) ? args.baseline.imagePath : path15.join(args.cwd, args.baseline.imagePath);
13773
+ mkdirSync(path15.dirname(baselineAbs), { recursive: true });
11512
13774
  copyFileSync(currentAbs, baselineAbs);
11513
13775
  args.storage.db.transaction(() => {
11514
13776
  args.baselineRepo.updateImage(args.baseline.id, {
@@ -11558,8 +13820,8 @@ function applyReviewDecision(args) {
11558
13820
  return { applied: true };
11559
13821
  }
11560
13822
  async function runSnapshotReview(opts) {
11561
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
11562
- const qaiosDir = path12.join(cwd, ".qaios");
13823
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
13824
+ const qaiosDir = path15.join(cwd, ".qaios");
11563
13825
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
11564
13826
  return {
11565
13827
  exitCode: ExitCode.USER_ERROR,
@@ -11572,7 +13834,7 @@ async function runSnapshotReview(opts) {
11572
13834
  };
11573
13835
  }
11574
13836
  const ownsStorage = opts.storage === void 0;
11575
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
13837
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11576
13838
  const auditLogger = new AuditLogger(storage.db);
11577
13839
  const baselineRepo = new VisualBaselinesRepository(storage.db);
11578
13840
  const diffRepo = new VisualDiffsRepository(storage.db);
@@ -11689,7 +13951,7 @@ async function runSnapshotReview(opts) {
11689
13951
  }
11690
13952
  }
11691
13953
  function loadConfig5(cwd) {
11692
- const candidate = path12.join(cwd, ".qaios", "config.yaml");
13954
+ const candidate = path15.join(cwd, ".qaios", "config.yaml");
11693
13955
  if (!existsSync(candidate)) return null;
11694
13956
  try {
11695
13957
  return parse(readFileSync(candidate, "utf-8"));
@@ -11702,14 +13964,14 @@ function loadConfig5(cwd) {
11702
13964
  }
11703
13965
  }
11704
13966
  function detectInstallCommand(cwd) {
11705
- if (existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm add -D";
11706
- if (existsSync(path12.join(cwd, "bun.lockb"))) return "bun add -d";
11707
- if (existsSync(path12.join(cwd, "yarn.lock"))) return "yarn add -D";
13967
+ if (existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm add -D";
13968
+ if (existsSync(path15.join(cwd, "bun.lockb"))) return "bun add -d";
13969
+ if (existsSync(path15.join(cwd, "yarn.lock"))) return "yarn add -D";
11708
13970
  return "npm install --save-dev";
11709
13971
  }
11710
13972
  function filterUninstalledDeps(cwd, imports) {
11711
13973
  if (imports.length === 0) return [];
11712
- const pkgPath = path12.join(cwd, "package.json");
13974
+ const pkgPath = path15.join(cwd, "package.json");
11713
13975
  if (!existsSync(pkgPath)) return [];
11714
13976
  let installed;
11715
13977
  try {
@@ -11819,7 +14081,7 @@ function exitCodeForStatus(status, reason, runSummary) {
11819
14081
  }
11820
14082
  }
11821
14083
  async function runTest(opts) {
11822
- const cwd = path12.resolve(opts.cwd ?? process.cwd());
14084
+ const cwd = path15.resolve(opts.cwd ?? process.cwd());
11823
14085
  if (!opts.description || opts.description.trim().length === 0) {
11824
14086
  return {
11825
14087
  exitCode: ExitCode.USER_ERROR,
@@ -11829,7 +14091,7 @@ async function runTest(opts) {
11829
14091
  }
11830
14092
  };
11831
14093
  }
11832
- const qaiosDir = path12.join(cwd, ".qaios");
14094
+ const qaiosDir = path15.join(cwd, ".qaios");
11833
14095
  if (!existsSync(qaiosDir) && opts.storage === void 0) {
11834
14096
  return {
11835
14097
  exitCode: ExitCode.USER_ERROR,
@@ -11851,11 +14113,11 @@ async function runTest(opts) {
11851
14113
  const candidates = [];
11852
14114
  if (opts.apiSpec !== void 0 && opts.apiSpec.trim().length > 0) {
11853
14115
  candidates.push(
11854
- path12.isAbsolute(opts.apiSpec) ? opts.apiSpec : path12.resolve(cwd, opts.apiSpec)
14116
+ path15.isAbsolute(opts.apiSpec) ? opts.apiSpec : path15.resolve(cwd, opts.apiSpec)
11855
14117
  );
11856
14118
  }
11857
14119
  if (/\.(ya?ml|json)$/i.test(opts.description.trim())) {
11858
- const guess = path12.isAbsolute(opts.description) ? opts.description.trim() : path12.resolve(cwd, opts.description.trim());
14120
+ const guess = path15.isAbsolute(opts.description) ? opts.description.trim() : path15.resolve(cwd, opts.description.trim());
11859
14121
  candidates.push(guess);
11860
14122
  }
11861
14123
  const found = candidates.find((c) => existsSync(c)) ?? null;
@@ -11879,7 +14141,7 @@ async function runTest(opts) {
11879
14141
  };
11880
14142
  }
11881
14143
  const isPathDescription = candidates.includes(
11882
- path12.isAbsolute(opts.description) ? opts.description : path12.resolve(cwd, opts.description)
14144
+ path15.isAbsolute(opts.description) ? opts.description : path15.resolve(cwd, opts.description)
11883
14145
  );
11884
14146
  if (isPathDescription || opts.description.trim().length < 3) {
11885
14147
  const epSummary = parsedSpec.endpoints.slice(0, 8).map((e) => `${e.method} ${e.path}${e.summary ? ` \u2014 ${e.summary}` : ""}`).join("\n");
@@ -11929,7 +14191,7 @@ ${epSummary}`;
11929
14191
  if (opts.featureName !== void 0) args["featureName"] = opts.featureName;
11930
14192
  const mode = opts.mode ?? config?.mode ?? "LITE";
11931
14193
  const ownsStorage = opts.storage === void 0;
11932
- const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
14194
+ const storage = opts.storage ?? Storage.open(path15.join(qaiosDir, "workflows.db"), { skipMigrations: false });
11933
14195
  const auditLogger = new AuditLogger(storage.db);
11934
14196
  const llm = resolveLlmClient(opts.llm, config?.llm);
11935
14197
  const gateConfig = {};
@@ -11997,27 +14259,27 @@ ${epSummary}`;
11997
14259
  }
11998
14260
  const filesWritten = [];
11999
14261
  if (result.status === "succeeded") {
12000
- const specsDir = path12.join(qaiosDir, "specs", result.workflowId);
14262
+ const specsDir = path15.join(qaiosDir, "specs", result.workflowId);
12001
14263
  mkdirSync(specsDir, { recursive: true });
12002
14264
  const designKey = Object.keys(result.outputs).find((k) => k.startsWith("design."));
12003
14265
  if (designKey !== void 0) {
12004
14266
  const design2 = result.outputs[designKey];
12005
14267
  if (design2 !== void 0 && design2["designSpec"] !== void 0) {
12006
- const designSpecPath = path12.join(specsDir, "DesignSpec.json");
14268
+ const designSpecPath = path15.join(specsDir, "DesignSpec.json");
12007
14269
  writeFileSync(designSpecPath, JSON.stringify(design2["designSpec"], null, 2) + "\n");
12008
- filesWritten.push(path12.relative(cwd, designSpecPath));
14270
+ filesWritten.push(path15.relative(cwd, designSpecPath));
12009
14271
  }
12010
14272
  }
12011
14273
  let importsRequired = [];
12012
14274
  let qaiosMdSuggestions = [];
12013
14275
  const writeKey2 = Object.keys(result.outputs).find((k) => k.startsWith("write."));
12014
14276
  if (writeKey2 !== void 0) {
12015
- const write = result.outputs[writeKey2];
14277
+ const write2 = result.outputs[writeKey2];
12016
14278
  const generatedAtIso = (/* @__PURE__ */ new Date()).toISOString();
12017
14279
  const disallowedImportsByFile = /* @__PURE__ */ new Map();
12018
- for (const file of write.files ?? []) {
12019
- const absPath = path12.join(cwd, file.path);
12020
- mkdirSync(path12.dirname(absPath), { recursive: true });
14280
+ for (const file of write2.files ?? []) {
14281
+ const absPath = path15.join(cwd, file.path);
14282
+ mkdirSync(path15.dirname(absPath), { recursive: true });
12021
14283
  const injected = injectGeneratedFileHeader(
12022
14284
  file.contents,
12023
14285
  result.workflowId,
@@ -12045,8 +14307,8 @@ ${epSummary}`;
12045
14307
  `
12046
14308
  );
12047
14309
  }
12048
- if (Array.isArray(write.importsRequired)) importsRequired = write.importsRequired;
12049
- if (Array.isArray(write.qaiosMdSuggestions)) qaiosMdSuggestions = write.qaiosMdSuggestions;
14310
+ if (Array.isArray(write2.importsRequired)) importsRequired = write2.importsRequired;
14311
+ if (Array.isArray(write2.qaiosMdSuggestions)) qaiosMdSuggestions = write2.qaiosMdSuggestions;
12050
14312
  }
12051
14313
  if (!opts.quiet && opts.json !== true) {
12052
14314
  process.stdout.write(`
@@ -12119,9 +14381,9 @@ Ran generated tests: ${runSummary.passedCount} passed, ${runSummary.failedCount}
12119
14381
  }
12120
14382
 
12121
14383
  // src/index.ts
12122
- var __dirname2 = path12.dirname(fileURLToPath(import.meta.url));
14384
+ var __dirname2 = path15.dirname(fileURLToPath(import.meta.url));
12123
14385
  function readPackageVersion() {
12124
- for (const candidate of [path12.resolve(__dirname2, "..", "package.json")]) {
14386
+ for (const candidate of [path15.resolve(__dirname2, "..", "package.json")]) {
12125
14387
  try {
12126
14388
  const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
12127
14389
  if (pkg.version) return pkg.version;
@@ -12152,7 +14414,7 @@ function buildProgram() {
12152
14414
  }
12153
14415
  const detected = result.detection;
12154
14416
  const lines = [];
12155
- lines.push(`\u2713 Created ${path12.relative(process.cwd(), result.qaiosDir)}/`);
14417
+ lines.push(`\u2713 Created ${path15.relative(process.cwd(), result.qaiosDir)}/`);
12156
14418
  if (detected.hasPlaywright) {
12157
14419
  lines.push(`\u2713 Detected: Playwright ${detected.playwrightVersion}`);
12158
14420
  } else {
@@ -12197,7 +14459,9 @@ function buildProgram() {
12197
14459
  }
12198
14460
  process.exit(result.exitCode);
12199
14461
  });
12200
- program.command("test <description>").description("Generate (and optionally run) tests for a feature").option("--type <type>", "testing type: web | api", "web").option("--api-spec <path>", "path to an OpenAPI spec (required when --type=api)").option("--base-url <url>", "override app.baseUrl from .qaios/config.yaml for this run").option(
14462
+ program.command("test <description>").description(
14463
+ "GENERATE test files from a feature description (design \u2192 write); --run also executes them"
14464
+ ).option("--type <type>", "testing type: web | api", "web").option("--api-spec <path>", "path to an OpenAPI spec (required when --type=api)").option("--base-url <url>", "override app.baseUrl from .qaios/config.yaml for this run").option(
12201
14465
  "--scope <scope>",
12202
14466
  "design scope: minimal (~3 scenarios) | standard (default) | exhaustive (~15-25 scenarios)",
12203
14467
  "standard"
@@ -12234,7 +14498,12 @@ function buildProgram() {
12234
14498
  }
12235
14499
  process.exit(result.exitCode);
12236
14500
  });
12237
- program.command("run [pattern]").description("Execute Playwright tests, classify failures, persist results").option("--no-classify", "skip LLM classification of failures").option("--no-heal", "skip auto-healing on locator failures").option("--no-heal-verify", "skip the re-run that confirms an applied heal").option("--no-defect", "skip defect creation for real failures").option("--retry <n>", "retry failed tests N times before classifying", (v) => parseInt(v, 10)).option("--workers <n>", "Playwright workers count", (v) => parseInt(v, 10)).option("--headed", "run browser headed").option("--workflow <id>", "attach to an existing workflow id").action(async (pattern, cmdOpts, command) => {
14501
+ program.command("run [pattern]").description(
14502
+ "EXECUTE existing Playwright tests, classify failures, auto-heal the first broken locator inline, file defects"
14503
+ ).option("--no-classify", "skip LLM classification of failures").option("--no-heal", "skip auto-healing on locator failures").option("--no-heal-verify", "skip the re-run that confirms an applied heal").option("--no-defect", "skip defect creation for real failures").option("--retry <n>", "retry failed tests N times before classifying", (v) => parseInt(v, 10)).option("--workers <n>", "Playwright workers count", (v) => parseInt(v, 10)).option("--headed", "run browser headed").option("--xray [level]", "capture network traffic (headers|summary|full; default summary)").option(
14504
+ "--xray-strict",
14505
+ "fail (exit 6) when UI-green tests have a backend divergence (implies --xray)"
14506
+ ).option("--workflow <id>", "attach to an existing workflow id").action(async (pattern, cmdOpts, command) => {
12238
14507
  const globalOpts = command.parent?.opts() ?? {};
12239
14508
  const opts = {
12240
14509
  // Commander turns --no-X flags into `X: false` (default true);
@@ -12252,6 +14521,18 @@ function buildProgram() {
12252
14521
  if (typeof cmdOpts["retry"] === "number") opts.retry = cmdOpts["retry"];
12253
14522
  if (typeof cmdOpts["workers"] === "number") opts.workers = cmdOpts["workers"];
12254
14523
  if (typeof cmdOpts["workflow"] === "string") opts.workflowId = cmdOpts["workflow"];
14524
+ const xrayVal = cmdOpts["xray"];
14525
+ if (xrayVal === true) opts.xray = true;
14526
+ else if (xrayVal === "headers" || xrayVal === "summary" || xrayVal === "full") {
14527
+ opts.xray = xrayVal;
14528
+ } else if (typeof xrayVal === "string") {
14529
+ process.stderr.write(
14530
+ `\u2717 qaios.run.bad_xray_level: --xray must be headers|summary|full (got "${xrayVal}")
14531
+ `
14532
+ );
14533
+ process.exit(1);
14534
+ }
14535
+ if (cmdOpts["xrayStrict"] === true) opts.xrayStrict = true;
12255
14536
  if (globalOpts["mode"] === "LITE" || globalOpts["mode"] === "FULL" || globalOpts["mode"] === "TRUST") {
12256
14537
  opts.mode = globalOpts["mode"];
12257
14538
  }
@@ -12262,7 +14543,9 @@ function buildProgram() {
12262
14543
  }
12263
14544
  process.exit(result.exitCode);
12264
14545
  });
12265
- program.command("fix [target]").description("Self-heal a broken test (or a directory of tests)").option("--auto", "auto-apply fixes regardless of mode (TRUST behavior)").option("--dry-run", "propose fix, show patch, don't apply").option("--last-failure", "fix the test from the most recent failed run").option("--scenario <id>", "fix a specific scenario by ID").option(
14546
+ program.command("fix [target]").description(
14547
+ "Repair ONE specific broken test you select (targeted heal + re-verify) \u2014 vs `run` which auto-heals inline"
14548
+ ).option("--auto", "auto-apply fixes regardless of mode (TRUST behavior)").option("--dry-run", "propose fix, show patch, don't apply").option("--last-failure", "fix the test from the most recent failed run").option("--scenario <id>", "fix a specific scenario by ID").option(
12266
14549
  "--keep-failed-patch",
12267
14550
  "keep the patch on disk even if the re-run still fails (default: roll back)"
12268
14551
  ).action(async (target, cmdOpts, command) => {
@@ -12305,7 +14588,9 @@ function buildProgram() {
12305
14588
  }
12306
14589
  process.exit(result.exitCode);
12307
14590
  });
12308
- snapshot.command("review").description("Interactively review pending visual diffs (Ink TUI)").option("--workflow <id>", "restrict to one workflow id").option(
14591
+ snapshot.command("review").description(
14592
+ "Approve/reject pending VISUAL DIFFs (screenshot regressions) \u2014 not workflow gates (see top-level `review`)"
14593
+ ).option("--workflow <id>", "restrict to one workflow id").option(
12309
14594
  "--auto-apply",
12310
14595
  "non-interactive: apply high-confidence classifier suggestions; leave uncertain diffs pending"
12311
14596
  ).option(
@@ -12497,7 +14782,9 @@ function buildProgram() {
12497
14782
  process.exit(result.exitCode);
12498
14783
  });
12499
14784
  }
12500
- configGroup.command("get [key]").description("Print value (or full config when no key is given)").action((key, cmdOpts, command) => {
14785
+ configGroup.command("get [key]").description(
14786
+ "Print the RAW on-disk value (no defaults; one key or whole file) \u2014 vs `show` which applies defaults"
14787
+ ).action((key, cmdOpts, command) => {
12501
14788
  void runConfigVerb("get", cmdOpts, command, key !== void 0 ? { key } : {});
12502
14789
  });
12503
14790
  configGroup.command("set <key> <value>").description("Set a value (validates against schema before writing)").action((key, value, cmdOpts, command) => {
@@ -12509,7 +14796,9 @@ function buildProgram() {
12509
14796
  configGroup.command("show").description("Print resolved config (defaults applied)").action((cmdOpts, command) => {
12510
14797
  void runConfigVerb("show", cmdOpts, command, {});
12511
14798
  });
12512
- program.command("review").description("Walk pending gates and approve/reject (interactive Ink TUI; W9-T2)").option("--workflow <id>", "restrict to one workflow id").option("--auto-approve", "approve all pending gates without prompting (CI-friendly)").action(async (cmdOpts, command) => {
14799
+ program.command("review").description(
14800
+ "Approve/reject pending workflow GATES (design/writing review pauses), then resume the workflow \u2014 not visual diffs (see `snapshot review`)"
14801
+ ).option("--workflow <id>", "restrict to one workflow id").option("--auto-approve", "approve all pending gates without prompting (CI-friendly)").action(async (cmdOpts, command) => {
12513
14802
  const globalOpts = command.parent?.opts() ?? {};
12514
14803
  const workflowFlag = typeof cmdOpts["workflow"] === "string" ? cmdOpts["workflow"] : typeof globalOpts["workflow"] === "string" ? globalOpts["workflow"] : void 0;
12515
14804
  const opts = {
@@ -12526,7 +14815,9 @@ function buildProgram() {
12526
14815
  }
12527
14816
  process.exit(result.exitCode);
12528
14817
  });
12529
- program.command("history").description("Show workflow history and audit data").option("--workflow <id>", "show single workflow detail").option("--last", "shortcut for --workflow <most-recent-id>").option("--limit <n>", "limit row count (default 20)", (v) => parseInt(v, 10)).option("--since <duration>", "relative time (e.g. 2h, 1d, 30m)").option(
14818
+ program.command("history").description(
14819
+ "View PAST workflows + audit log (read-only; list / detail / --verify chain / --export)"
14820
+ ).option("--workflow <id>", "show single workflow detail").option("--last", "shortcut for --workflow <most-recent-id>").option("--limit <n>", "limit row count (default 20)", (v) => parseInt(v, 10)).option("--since <duration>", "relative time (e.g. 2h, 1d, 30m)").option(
12530
14821
  "--status <status>",
12531
14822
  "filter by workflow status: succeeded | failed | blocked | cancelled | running"
12532
14823
  ).option("--verify", "verify audit log hash chain integrity").option("--export <path>", "export full audit log as JSONL").option("--suggestions", "show pending QAIOS.md suggestions").action(async (cmdOpts, command) => {
@@ -12553,6 +14844,53 @@ function buildProgram() {
12553
14844
  }
12554
14845
  process.exit(result.exitCode);
12555
14846
  });
14847
+ const xrayGroup = program.command("xray").description(
14848
+ "Inspect network captured by `run --xray` (read-only: show / diff / baseline / volatility)"
14849
+ );
14850
+ function dispatchXray(verb, command, extras = {}) {
14851
+ const globalOpts = command.parent?.parent?.opts?.() ?? command.parent?.opts?.() ?? {};
14852
+ const opts = {
14853
+ verb,
14854
+ json: Boolean(globalOpts["json"]),
14855
+ quiet: Boolean(globalOpts["quiet"])
14856
+ };
14857
+ if (extras.target !== void 0) opts.target = extras.target;
14858
+ if (extras.target2 !== void 0) opts.target2 = extras.target2;
14859
+ if (extras.sub !== void 0) opts.sub = extras.sub;
14860
+ return runXray(opts).then((result) => {
14861
+ if (result.error) {
14862
+ process.stderr.write(`\u2717 ${result.error.code}: ${result.error.message}
14863
+ `);
14864
+ }
14865
+ process.exit(result.exitCode);
14866
+ });
14867
+ }
14868
+ xrayGroup.command("show [target]").description("Show the per-step causal graph of a run or workflow").action((target, _cmdOpts, command) => {
14869
+ void dispatchXray("show", command, target !== void 0 ? { target } : {});
14870
+ });
14871
+ xrayGroup.command("diff <runA> [runB]").description("Diff a run against another run, or against its baseline").action((runA, runB, _cmdOpts, command) => {
14872
+ void dispatchXray("diff", command, {
14873
+ target: runA,
14874
+ ...runB !== void 0 ? { target2: runB } : {}
14875
+ });
14876
+ });
14877
+ const baselineGroup = xrayGroup.command("baseline").description("Pin or clear a capture baseline");
14878
+ baselineGroup.command("set <runId>").description("Pin a run as the baseline for its (test, browser)").action((runId, _cmdOpts, command) => {
14879
+ void dispatchXray("baseline", command, { sub: "set", target: runId });
14880
+ });
14881
+ baselineGroup.command("clear <runId>").description("Clear a (test, browser)'s pinned baseline").action((runId, _cmdOpts, command) => {
14882
+ void dispatchXray("baseline", command, { sub: "clear", target: runId });
14883
+ });
14884
+ const volatilityGroup = xrayGroup.command("volatility").description("Show or reset learned field volatility");
14885
+ volatilityGroup.command("show <testId>").description("Show the learned volatility contract for a test").action((testId, _cmdOpts, command) => {
14886
+ void dispatchXray("volatility", command, { sub: "show", target: testId });
14887
+ });
14888
+ volatilityGroup.command("reset [testId]").description("Reset learned volatility (all tests, or one)").action((testId, _cmdOpts, command) => {
14889
+ void dispatchXray("volatility", command, {
14890
+ sub: "reset",
14891
+ ...testId !== void 0 ? { target: testId } : {}
14892
+ });
14893
+ });
12556
14894
  return program;
12557
14895
  }
12558
14896
  function formatDoctor(checks) {
@@ -12583,11 +14921,11 @@ var invokedAsScript = (() => {
12583
14921
  try {
12584
14922
  return realpathSync(p);
12585
14923
  } catch {
12586
- return path12.resolve(p);
14924
+ return path15.resolve(p);
12587
14925
  }
12588
14926
  };
12589
14927
  if (realOf(argv1) === realOf(thisFile)) return true;
12590
- const invokedName = path12.basename(argv1).replace(/\.(js|mjs|cjs)$/, "");
14928
+ const invokedName = path15.basename(argv1).replace(/\.(js|mjs|cjs)$/, "");
12591
14929
  return invokedName === "qaios";
12592
14930
  })();
12593
14931
  if (invokedAsScript) {