@jevitate/cli 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -35260,9 +35260,9 @@ async function confineFixture(raw, root2) {
35260
35260
  }
35261
35261
  if (!isFile)
35262
35262
  throw fixtureNotFound(candidate, root2);
35263
- const sensitive = fixtureSensitiveReason(real, realRoot) ?? (isInsideRoot(candidate, resolve3(root2)) ? fixtureSensitiveReason(candidate, resolve3(root2)) : null);
35264
- if (sensitive !== null)
35265
- throw fixtureSensitive(candidate, root2, sensitive);
35263
+ const sensitive2 = fixtureSensitiveReason(real, realRoot) ?? (isInsideRoot(candidate, resolve3(root2)) ? fixtureSensitiveReason(candidate, resolve3(root2)) : null);
35264
+ if (sensitive2 !== null)
35265
+ throw fixtureSensitive(candidate, root2, sensitive2);
35266
35266
  return real;
35267
35267
  }
35268
35268
  function fixtureEscapesRoot(raw, root2) {
@@ -37855,6 +37855,7 @@ var MESSAGE_FIELD = /\b(reply|message|ask|chat|inquiry|prompt|say|talk|conversat
37855
37855
  var MESSAGE_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "search"]);
37856
37856
  var SUBMIT_NAME = /\b(send|submit|reply|ask|post)\b|[→➤➔↑]|^\s*(go|ok)\s*$/i;
37857
37857
  var PAIRED_SUBMIT_DISTANCE = 6;
37858
+ var RECIPIENT_NAME = /^\s*(to|cc|bcc)\b|\b(recipients?|contacts?|addressee|send to)\b/i;
37858
37859
  function composerEvidence(node2, args) {
37859
37860
  const el = node2;
37860
37861
  const norm6 = (s3) => s3 === null ? "" : s3.replace(/\s+/g, " ").trim();
@@ -37886,6 +37887,29 @@ function composerEvidence(node2, args) {
37886
37887
  parents.set(item.parentElement, n3);
37887
37888
  }
37888
37889
  const submit2 = new RegExp(args.submit, "i");
37890
+ const recipient = new RegExp(args.recipient, "i");
37891
+ const labelOf2 = (a) => {
37892
+ const aria = norm6(a.getAttribute("aria-label"));
37893
+ if (aria !== "")
37894
+ return aria;
37895
+ const by = norm6(a.getAttribute("aria-labelledby"));
37896
+ if (by !== "") {
37897
+ const text2 = by.split(" ").map((id2) => norm6(document.getElementById(id2)?.textContent ?? null)).join(" ").trim();
37898
+ if (text2 !== "")
37899
+ return text2;
37900
+ }
37901
+ const id = a.getAttribute("id");
37902
+ if (id !== null && id !== "") {
37903
+ const forLabel = Array.from(document.querySelectorAll("label")).find((l2) => l2.htmlFor === id);
37904
+ if (forLabel !== void 0)
37905
+ return norm6(forLabel.textContent);
37906
+ }
37907
+ const wrap = a.closest("label");
37908
+ if (wrap !== null)
37909
+ return norm6(wrap.textContent);
37910
+ return norm6(a.getAttribute("placeholder")) || norm6(a.getAttribute("name"));
37911
+ };
37912
+ const addressed = (container) => Array.from(container.querySelectorAll('select, input:not([type="hidden"]):not([type="submit"]):not([type="button"]), [role="combobox"], [role="listbox"]')).some((f2) => f2 !== el && !f2.contains(el) && rendered(f2) && recipient.test(labelOf2(f2)));
37889
37913
  const up4 = (a) => {
37890
37914
  const out = [];
37891
37915
  for (let cur = a; cur !== null; cur = parentOf(cur))
@@ -37903,7 +37927,7 @@ function composerEvidence(node2, args) {
37903
37927
  for (let i = 0; i < fieldPath.length && i <= args.distance; i++) {
37904
37928
  const j = bp.indexOf(fieldPath[i]);
37905
37929
  if (j >= 0) {
37906
- if (i + j <= args.distance)
37930
+ if (i + j <= args.distance && !addressed(fieldPath[i]))
37907
37931
  return true;
37908
37932
  break;
37909
37933
  }
@@ -38535,7 +38559,7 @@ async function readSnapshot(page, opts) {
38535
38559
  const value = raw.valueBearing && !isSecretField(raw.inputType, raw.autocomplete, [raw.name]) ? await handle.evaluate(readControlValue) : null;
38536
38560
  const facts = { ...raw, value };
38537
38561
  const textEntry = raw.tag === "textarea" || raw.tag === "input" && MESSAGE_INPUT_TYPES.has(raw.inputType ?? "") || raw.role === "textbox";
38538
- const conversational = textEntry && MESSAGE_FIELD.test(raw.name) ? await handle.evaluate(composerEvidence, { submit: SUBMIT_NAME.source, distance: PAIRED_SUBMIT_DISTANCE }).catch(() => false) : null;
38562
+ const conversational = textEntry && MESSAGE_FIELD.test(raw.name) ? await handle.evaluate(composerEvidence, { submit: SUBMIT_NAME.source, distance: PAIRED_SUBMIT_DISTANCE, recipient: RECIPIENT_NAME.source }).catch(() => false) : null;
38539
38563
  const computed = await computeDescriptor(page, handle, { primaryOnly: true });
38540
38564
  const control = {
38541
38565
  index: controls.length,
@@ -39568,6 +39592,59 @@ async function clockBounded(work, ms, onTimeout) {
39568
39592
  }
39569
39593
  }
39570
39594
 
39595
+ // ../explore/dist/request-body-values.js
39596
+ var MIN_VALUE_CHARS = 3;
39597
+ var MAX_VALUES = 50;
39598
+ var MAX_BODY_CHARS = 64e3;
39599
+ var SENSITIVE_WORDS = /* @__PURE__ */ new Set(["pass", "password", "passwd", "pwd", "secret", "token", "auth", "authorization", "apikey", "key", "otp", "pin", "cvv", "cvc", "card", "ssn", "session", "cookie", "signature", "credential", "credentials"]);
39600
+ function sensitive(key2) {
39601
+ return key2.split(/[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).some((w) => SENSITIVE_WORDS.has(w.toLowerCase()));
39602
+ }
39603
+ function keep(out, key2, value) {
39604
+ if (out.length >= MAX_VALUES)
39605
+ return;
39606
+ if (key2 !== null && sensitive(key2))
39607
+ return;
39608
+ const v = value.replace(/\s+/g, " ").trim();
39609
+ if (v.length >= MIN_VALUE_CHARS)
39610
+ out.push(v);
39611
+ }
39612
+ function walk(out, key2, node2) {
39613
+ if (out.length >= MAX_VALUES)
39614
+ return;
39615
+ if (typeof node2 === "string")
39616
+ keep(out, key2, node2);
39617
+ else if (Array.isArray(node2))
39618
+ for (const item of node2)
39619
+ walk(out, key2, item);
39620
+ else if (node2 !== null && typeof node2 === "object") {
39621
+ for (const [k, v] of Object.entries(node2)) {
39622
+ if (sensitive(k))
39623
+ continue;
39624
+ walk(out, k, v);
39625
+ }
39626
+ }
39627
+ }
39628
+ function bodyValues(body, contentType) {
39629
+ if (body === null || body === "" || body.length > MAX_BODY_CHARS)
39630
+ return [];
39631
+ const type = (contentType ?? "").toLowerCase();
39632
+ const out = [];
39633
+ if (type.includes("application/x-www-form-urlencoded")) {
39634
+ for (const [k, v] of new URLSearchParams(body))
39635
+ keep(out, k, v);
39636
+ return out;
39637
+ }
39638
+ if (type === "" || type.includes("json")) {
39639
+ try {
39640
+ walk(out, null, JSON.parse(body));
39641
+ } catch {
39642
+ return [];
39643
+ }
39644
+ }
39645
+ return out;
39646
+ }
39647
+
39571
39648
  // ../explore/dist/page-monitor.js
39572
39649
  var RPC_TRAILER_READ_MS = 2e3;
39573
39650
  var INTERACTIVE_SELECTOR2 = "a[href],button,input:not([type=hidden]),select,textarea,[role=button],[role=link],[role=textbox],[role=checkbox],[role=combobox],[contenteditable=true]";
@@ -39773,6 +39850,8 @@ function pathOf3(url2) {
39773
39850
  return url2.split(/[?#]/)[0] ?? url2;
39774
39851
  }
39775
39852
  }
39853
+ var BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
39854
+ var MAX_WRITE_BODIES = 200;
39776
39855
  function requestContentTypeOf(r) {
39777
39856
  try {
39778
39857
  const v = r.headers()["content-type"];
@@ -39785,6 +39864,8 @@ var RECENT_REQUESTS_MS = 6e4;
39785
39864
  var PageMonitor = class {
39786
39865
  #page;
39787
39866
  #inflight = /* @__PURE__ */ new Map();
39867
+ /** #649: the string values recent writes sent in their bodies (memory only; never persisted). */
39868
+ #writeBodies = [];
39788
39869
  #completed = [];
39789
39870
  #wakers = /* @__PURE__ */ new Set();
39790
39871
  #statuses = /* @__PURE__ */ new WeakMap();
@@ -39822,6 +39903,18 @@ var PageMonitor = class {
39822
39903
  startedAt,
39823
39904
  ...requestContentType2 === void 0 ? {} : { requestContentType: requestContentType2 }
39824
39905
  });
39906
+ if (BODY_METHODS.has(r.method().toUpperCase())) {
39907
+ let values2 = [];
39908
+ try {
39909
+ values2 = bodyValues(r.postData(), requestContentType2);
39910
+ } catch {
39911
+ }
39912
+ if (values2.length > 0) {
39913
+ this.#writeBodies.push({ startedAt, url: r.url(), values: values2 });
39914
+ if (this.#writeBodies.length > MAX_WRITE_BODIES)
39915
+ this.#writeBodies.shift();
39916
+ }
39917
+ }
39825
39918
  for (const c of this.#captures) {
39826
39919
  c.began(r, {
39827
39920
  method: r.method().toUpperCase(),
@@ -40073,6 +40166,10 @@ var PageMonitor = class {
40073
40166
  clock.clearTimeout(timer);
40074
40167
  }
40075
40168
  }
40169
+ /** #649: the string values the writes started since `sinceMs` sent, from URLs `keep` accepts. */
40170
+ writeBodyValuesSince(sinceMs, keep2) {
40171
+ return this.#writeBodies.filter((w) => w.startedAt >= sinceMs && keep2(w.url)).flatMap((w) => w.values);
40172
+ }
40076
40173
  /** Requests that ended since `sinceMs` (wall clock). */
40077
40174
  completedSince(sinceMs) {
40078
40175
  return this.#completed.filter((r) => r.endedAt >= sinceMs);
@@ -41105,8 +41202,8 @@ function boundCandidates(candidates, opts) {
41105
41202
  return s3;
41106
41203
  };
41107
41204
  const ranked = candidates.map((c, i) => ({ c, i, s: score2(c, i) })).sort((a, b2) => b2.s - a.s || a.i - b2.i).slice(0, Math.max(0, opts.limit));
41108
- const keep = new Set(ranked.map((r) => r.i));
41109
- return { kept: candidates.filter((_, i) => keep.has(i)), omitted: candidates.length - keep.size };
41205
+ const keep2 = new Set(ranked.map((r) => r.i));
41206
+ return { kept: candidates.filter((_, i) => keep2.has(i)), omitted: candidates.length - keep2.size };
41110
41207
  }
41111
41208
 
41112
41209
  // ../explore/dist/stuck-actions.js
@@ -41391,7 +41488,8 @@ var SideEffectGuard = class {
41391
41488
  const own2 = requests.filter((r) => this.#ours(r));
41392
41489
  const settled = !inflightAny && own2.every((r) => r.status !== null && r.status >= 200 && r.status < 300);
41393
41490
  const completedOk = own2.length > 0 && settled;
41394
- this.#lastClick = { requestSent: requests.length > 0 || inflightAny, writes: [...done, ...pending], completedOk, settled };
41491
+ const sentValues = this.#monitor.writeBodyValuesSince(o.at, (url2) => this.#ours({ url: url2 }));
41492
+ this.#lastClick = { requestSent: requests.length > 0 || inflightAny, writes: [...done, ...pending], completedOk, settled, sentValues };
41395
41493
  if (done.length + pending.length === 0)
41396
41494
  return;
41397
41495
  this.#fired.set(o.key, {
@@ -41897,6 +41995,7 @@ var UnsubmittedTypeTracker = class {
41897
41995
  var GOAL_MET_THRESHOLD = 0.75;
41898
41996
  var GOAL_CHECK_TRIGGER = 0.5;
41899
41997
  var WRITES_DONE_NOTE = "the job's writes succeeded; the page shows no change (repeat data?)";
41998
+ var READ_BACK_DONE_NOTE = "the job's writes succeeded and a page loaded from the server shows what they sent";
41900
41999
  function groundDone(e) {
41901
42000
  if (e.unsubmitted.length > 0) {
41902
42001
  return { accept: false, reason: `typed text in ${e.unsubmitted.map((l2) => `"${l2}"`).join(", ")} was never submitted` };
@@ -41922,6 +42021,9 @@ function groundDone(e) {
41922
42021
  if (w !== void 0 && writesAllSucceeded(w) && w.goalIsWrites !== null && w.goalIsWrites >= GOAL_MET_THRESHOLD) {
41923
42022
  return { accept: true, outcome: { status: "completed", verifiedBy: "succeeded-writes", note: WRITES_DONE_NOTE } };
41924
42023
  }
42024
+ if (w !== void 0 && writesAllSucceeded(w) && e.readBack === true && p >= GOAL_CHECK_TRIGGER) {
42025
+ return { accept: true, outcome: { status: "completed", verifiedBy: "succeeded-writes", note: READ_BACK_DONE_NOTE } };
42026
+ }
41925
42027
  const notMet = `the goal is not observably achieved on this page (p=${p.toFixed(2)})`;
41926
42028
  if (w !== void 0 && writesAllSucceeded(w) && w.goalIsWrites !== null) {
41927
42029
  return { accept: false, reason: `${notMet}; the job's writes succeeded, but they are not judged to be the whole goal (p=${w.goalIsWrites.toFixed(2)})` };
@@ -45461,6 +45563,19 @@ function pageStatusOf(ctx) {
45461
45563
  const parts2 = [...isEmptyStatus(ctx.status) ? [] : [describeStatus(ctx.status)], ...[formMessageStatus(ctx)].filter((x) => x !== null)];
45462
45564
  return parts2.length === 0 ? {} : { pageStatus: parts2.join("; ") };
45463
45565
  }
45566
+ var DOCUMENT_LOADS = /* @__PURE__ */ new WeakMap();
45567
+ async function documentOf(page) {
45568
+ let loads = DOCUMENT_LOADS.get(page);
45569
+ if (loads === void 0) {
45570
+ const counter = { count: 0 };
45571
+ page.on("domcontentloaded", () => {
45572
+ counter.count += 1;
45573
+ });
45574
+ DOCUMENT_LOADS.set(page, counter);
45575
+ loads = counter;
45576
+ }
45577
+ return loads.count;
45578
+ }
45464
45579
 
45465
45580
  // ../explore/dist/goal-loop/context/conversation.js
45466
45581
  function initConversation(ctx, cfg) {
@@ -45485,6 +45600,7 @@ function initConversation(ctx, cfg) {
45485
45600
  }
45486
45601
 
45487
45602
  // ../explore/dist/answer.js
45603
+ var UNCONFIRMED_WRITE = "the quote shows what the run itself sent";
45488
45604
  var ANSWER_PAGES_CHARS = 8e3;
45489
45605
  var MAX_OBSERVED_PAGES = 30;
45490
45606
  var OBSERVED_PAGE_CHARS = 2e4;
@@ -45526,9 +45642,94 @@ var ObservedPages = class {
45526
45642
  if (v !== "")
45527
45643
  this.#ownInputs.add(v);
45528
45644
  }
45529
- /** #239: a submit the run clicked fired writes that all succeeded (2xx): what it typed was sent and saved. */
45530
- confirmOwnInputs() {
45645
+ /**
45646
+ * #647: values a 2xx write sent that no page loaded from the server has shown since. A 2xx says the
45647
+ * request was accepted, not that the app kept it: until a later document shows the value, a page
45648
+ * showing it may only be the client's own rendering of what the run sent.
45649
+ */
45650
+ #unread = /* @__PURE__ */ new Set();
45651
+ /** #647: the document the last such write was sent from. */
45652
+ #writeDocument = null;
45653
+ /**
45654
+ * #239: a submit the run clicked fired writes that all succeeded (2xx): what it typed was sent. #647:
45655
+ * it is read back only when a page from a later document load (`document`) shows it.
45656
+ */
45657
+ confirmOwnInputs(document2) {
45658
+ for (const v of this.#ownInputs)
45659
+ this.#unread.add(v);
45531
45660
  this.#ownInputs.clear();
45661
+ if (document2 !== void 0)
45662
+ this.#writeDocument = document2;
45663
+ }
45664
+ /**
45665
+ * #649: a sent value the run did not type (a write's request body: a prefilled import's rows) that
45666
+ * a page on the write's own route already showed before the write — its source, not a copy the app
45667
+ * kept. Only a page on another route reads it back: reloading the source shows it either way.
45668
+ */
45669
+ #sourceRoute = /* @__PURE__ */ new Map();
45670
+ /** #649: a write's body values the run did not type, decided on the next page seen (see `add`). */
45671
+ #pendingSent = null;
45672
+ /**
45673
+ * #649: a successful write the run fired sent `values` in its body, from a page on `url`. One the run
45674
+ * did not type awaits read-back like a typed one only when the next page shows it as NEW text — text
45675
+ * no earlier page on that route showed (an import's rows listed from its CSV; never a form's
45676
+ * untouched prefilled fields, still only in their fields). Decided when that page is added.
45677
+ */
45678
+ noteSentValues(values2, url2) {
45679
+ const own2 = /* @__PURE__ */ new Set([...this.#ownInputs, ...this.#unread]);
45680
+ const fresh = values2.map((value) => fold(redactContext(value, this.secrets).slice(0, OBSERVED_FIELD_CHARS))).filter((v) => v !== "" && !own2.has(v));
45681
+ this.#pendingSent = fresh.length === 0 ? null : { route: routeOf(url2), values: fresh };
45682
+ }
45683
+ /** #649: the pending body values `text` (the page after the write) newly shows await read-back. */
45684
+ #settleSent(text2) {
45685
+ const pending = this.#pendingSent;
45686
+ if (pending === null)
45687
+ return;
45688
+ this.#pendingSent = null;
45689
+ const now2 = fold(text2);
45690
+ const before = this.#pages.filter((p) => routeOf(p.url) === pending.route);
45691
+ for (const v of pending.values) {
45692
+ if (!now2.includes(v) || before.some((p) => fold(p.text).includes(v)))
45693
+ continue;
45694
+ this.#unread.add(v);
45695
+ if (before.some((p) => (p.fields ?? []).some((f2) => fold(f2.value).includes(v))))
45696
+ this.#sourceRoute.set(v, pending.route);
45697
+ }
45698
+ }
45699
+ /** #647: the run's sent values (folded) that no page loaded from the server since has shown. */
45700
+ unreadWrites() {
45701
+ return this.#unread;
45702
+ }
45703
+ /** #647: the unread sent values (folded) that `shown` (a page's text, or field values) displays. */
45704
+ unreadShownIn(shown2) {
45705
+ const text2 = fold(shown2);
45706
+ return [...this.#unread].filter((v) => text2.includes(v));
45707
+ }
45708
+ /**
45709
+ * #647: a page from a later document load than the write shows `shown` (its visible text, or field
45710
+ * values): the sent values it contains are read back from the app. Returns those values.
45711
+ */
45712
+ readBack(document2, shown2, url2) {
45713
+ if (this.#unread.size === 0 || document2 === void 0 || document2 === this.#writeDocument)
45714
+ return [];
45715
+ const text2 = fold(shown2);
45716
+ const route = url2 === void 0 ? void 0 : routeOf(url2);
45717
+ const back = [...this.#unread].filter((v) => text2.includes(v) && (route === void 0 || this.#sourceRoute.get(v) !== route));
45718
+ for (const v of back) {
45719
+ this.#unread.delete(v);
45720
+ this.#sourceRoute.delete(v);
45721
+ }
45722
+ this.#readBackCount += back.length;
45723
+ return back;
45724
+ }
45725
+ /** #645: how many sent values a page loaded from the server has shown again. */
45726
+ #readBackCount = 0;
45727
+ /**
45728
+ * #645: the app was seen to keep what the run's writes sent — at least one value was read back and
45729
+ * none is still unread. Code evidence the page judgment lacks (see `groundDone`).
45730
+ */
45731
+ readBackConfirmed() {
45732
+ return this.#readBackCount > 0 && this.#unread.size === 0;
45532
45733
  }
45533
45734
  /** #239: the run's own typed, not-yet-saved values (folded). */
45534
45735
  ownInputs() {
@@ -45542,9 +45743,9 @@ var ObservedPages = class {
45542
45743
  const clip5 = (h) => redactContext((h ?? "").replace(/\s+/g, " ").trim(), this.secrets).slice(0, HEADING_CHARS);
45543
45744
  const heading = clip5(headings2.heading);
45544
45745
  const title = clip5(headings2.title);
45545
- const keep = (xs) => (xs ?? []).slice(0, MAX_OBSERVED_CONTROLS).map((n3) => redactContext(n3.replace(/\s+/g, " ").trim(), this.secrets).slice(0, OBSERVED_FIELD_CHARS)).filter((n3) => n3 !== "");
45546
- const names = keep(headings2.controlNames);
45547
- const links = keep(headings2.contentLinks);
45746
+ const keep2 = (xs) => (xs ?? []).slice(0, MAX_OBSERVED_CONTROLS).map((n3) => redactContext(n3.replace(/\s+/g, " ").trim(), this.secrets).slice(0, OBSERVED_FIELD_CHARS)).filter((n3) => n3 !== "");
45747
+ const names = keep2(headings2.controlNames);
45748
+ const links = keep2(headings2.contentLinks);
45548
45749
  const capped = (headings2.controlNames ?? []).length > MAX_OBSERVED_CONTROLS || (headings2.contentLinks ?? []).length > MAX_OBSERVED_CONTROLS;
45549
45750
  const incomplete = [headings2.inventoryIncomplete ?? "", capped ? `more than ${MAX_OBSERVED_CONTROLS} controls were kept` : ""].filter((x) => x !== "").join("; ");
45550
45751
  const status = headings2.status;
@@ -45564,6 +45765,8 @@ var ObservedPages = class {
45564
45765
  if (nav.length > 0)
45565
45766
  this.#topNav = nav;
45566
45767
  }
45768
+ this.#settleSent(page.text);
45769
+ this.readBack(headings2.document, [page.text, ...kept.map((f2) => f2.value)].join("\n"), url2);
45567
45770
  if (page.text.trim() === "" && kept.length === 0)
45568
45771
  return;
45569
45772
  const same = (p) => JSON.stringify(p.fields ?? []) === JSON.stringify(page.fields ?? []) && p.heading === page.heading && p.title === page.title && p.status === page.status && JSON.stringify(p.controls ?? []) === JSON.stringify(page.controls ?? []) && JSON.stringify(p.contentLinks ?? []) === JSON.stringify(page.contentLinks ?? []) && p.inventoryIncomplete === page.inventoryIncomplete;
@@ -45579,6 +45782,13 @@ var ObservedPages = class {
45579
45782
  return [...this.#pages].reverse();
45580
45783
  }
45581
45784
  };
45785
+ function routeOf(url2) {
45786
+ try {
45787
+ return new URL(url2).pathname;
45788
+ } catch {
45789
+ return url2.split(/[?#]/)[0] ?? url2;
45790
+ }
45791
+ }
45582
45792
  function pathOf6(url2) {
45583
45793
  try {
45584
45794
  const u = new URL(url2);
@@ -45918,14 +46128,14 @@ function errorPageReason(page) {
45918
46128
  return `the page it is on is an error page (title "${page.title}")`;
45919
46129
  return null;
45920
46130
  }
45921
- function groundClaim(claim2, quote6, pages, given, goal, answer, own2, absent) {
46131
+ function groundClaim(claim2, quote6, pages, given, goal, answer, own2, unread, absent) {
45922
46132
  const name = (absent ?? "").replace(/\s+/g, " ").trim();
45923
46133
  if (name !== "")
45924
46134
  return { evidence: groundAbsence(claim2, name, pages, goal), notAnswer: false };
45925
- const r = groundClaimOn(claim2, quote6, pages, given, goal, answer, own2);
46135
+ const r = groundClaimOn(claim2, quote6, pages, given, goal, answer, own2, unread);
45926
46136
  return "evidence" in r ? r : { evidence: r, notAnswer: false };
45927
46137
  }
45928
- function groundClaimOn(claim2, quote6, pages, given, goal, answer, own2) {
46138
+ function groundClaimOn(claim2, quote6, pages, given, goal, answer, own2, unread) {
45929
46139
  const q = bareQuote(quote6);
45930
46140
  const base = { claim: claim2, quote: quote6 };
45931
46141
  if (q.replace(/\s/g, "").length < MIN_QUOTE_CHARS)
@@ -45943,6 +46153,11 @@ function groundClaimOn(claim2, quote6, pages, given, goal, answer, own2) {
45943
46153
  const why = `the quote is the run's own typed input in "${found.control ?? "a form field"}", never saved \u2014 it shows what the run entered, not what the app recorded (submit it, then report what the app shows)`;
45944
46154
  return { ...base, url: page.url, grounded: false, ...where, why };
45945
46155
  }
46156
+ const sent = [...unread].find((v) => v.length >= MIN_QUOTE_CHARS && q.includes(v));
46157
+ if (sent !== void 0) {
46158
+ const why = `${UNCONFIRMED_WRITE} ("${sent}"), and no page loaded since the write has shown it \u2014 the app may not have kept it (reload, or open the page where the app lists it, and report what that shows)`;
46159
+ return { ...base, url: page.url, grounded: false, ...where, why };
46160
+ }
45946
46161
  if (found.source === "page-text" && !CONTROL_GOAL.test(goal) && quoteIsOnlyControlNames(quote6, found.page)) {
45947
46162
  const why = "the quote is only a control's label (a button, a field label or a navigation link), not page content that answers the question";
45948
46163
  return { evidence: { ...base, url: page.url, grounded: false, ...where, why }, notAnswer: true };
@@ -45970,7 +46185,8 @@ function groundAnswer(proposed, pages, opts = {}) {
45970
46185
  return { accept: false, reason: NO_ANSWER_REASON, answer: null };
45971
46186
  const given = new Set(numbersIn(opts.goal ?? ""));
45972
46187
  const own2 = opts.ownInputs ?? /* @__PURE__ */ new Set();
45973
- const grounded_ = proposed.claims.map((c) => groundClaim(c.claim, c.quote, pages, given, opts.goal ?? "", text2, own2, c.absent));
46188
+ const unread = opts.unreadWrites ?? /* @__PURE__ */ new Set();
46189
+ const grounded_ = proposed.claims.map((c) => groundClaim(c.claim, c.quote, pages, given, opts.goal ?? "", text2, own2, unread, c.absent));
45974
46190
  const evidence = grounded_.map((g) => g.evidence);
45975
46191
  const answer = { text: text2, evidence };
45976
46192
  if (evidence.length === 0)
@@ -45979,6 +46195,8 @@ function groundAnswer(proposed, pages, opts = {}) {
45979
46195
  if (badAt >= 0) {
45980
46196
  const bad = evidence[badAt];
45981
46197
  const reason = `the answer is not grounded: "${bad.claim}" \u2014 ${bad.why ?? "ungrounded"}`;
46198
+ if (bad.why?.startsWith(UNCONFIRMED_WRITE) === true)
46199
+ return { accept: false, reason, answer, unconfirmedWrite: true };
45982
46200
  return grounded_[badAt].notAnswer ? { accept: false, reason, answer, notAnswer: true } : { accept: false, reason, answer };
45983
46201
  }
45984
46202
  const grounded = new Set(evidence.flatMap((e) => numbersIn(e.quote)));
@@ -46157,7 +46375,11 @@ async function reportAnswer(gen, input2) {
46157
46375
  answer: out.answer === null ? null : r(out.answer),
46158
46376
  claims: out.claims.map((c) => ({ claim: r(c.claim), quote: r(c.quote), ...c.absent == null ? {} : { absent: r(c.absent) } }))
46159
46377
  });
46160
- const grounding = { goal: input2.goal, ...input2.ownInputs === void 0 ? {} : { ownInputs: input2.ownInputs } };
46378
+ const grounding = {
46379
+ goal: input2.goal,
46380
+ ...input2.ownInputs === void 0 ? {} : { ownInputs: input2.ownInputs },
46381
+ ...input2.unreadWrites === void 0 ? {} : { unreadWrites: input2.unreadWrites }
46382
+ };
46161
46383
  const res = await gen.generate("goal.answer", ask);
46162
46384
  const verdict = await vetoed(groundAnswer(scrub3(res.output), input2.pages, grounding), input2.judge, vet, vetoes);
46163
46385
  if (verdict.accept)
@@ -46267,6 +46489,7 @@ function initAnswers(ctx, cfg) {
46267
46489
  ctx.settledClick = null;
46268
46490
  ctx.clickedRegion = null;
46269
46491
  ctx.wroteOk = false;
46492
+ ctx.readBackAsked = false;
46270
46493
  ctx.writeGoal = goalAsksToWrite(cfg.goal);
46271
46494
  ctx.vetoes = new VetoedAnswers();
46272
46495
  ctx.replies = new ObservedPages(ctx.secrets);
@@ -48076,7 +48299,8 @@ function newStep(ctx, input2) {
48076
48299
  ...goalMet === void 0 ? {} : { goalMetProbability: goalMet },
48077
48300
  ...signIn === null || goalMet === void 0 ? {} : { signIn: { completed: signIn.completed, goalIsSignIn } },
48078
48301
  ...saved === null || goalMet === void 0 ? {} : { save: { completed: saved.completed, goalIsSave } },
48079
- ...writes === void 0 || goalMet === void 0 ? {} : { writes: { ...writes, goalIsWrites } }
48302
+ ...writes === void 0 || goalMet === void 0 ? {} : { writes: { ...writes, goalIsWrites } },
48303
+ ...writes === void 0 ? {} : { readBack: ctx.observed.readBackConfirmed() }
48080
48304
  });
48081
48305
  const judgments = {};
48082
48306
  if (goalMet !== void 0 && goalMet !== null)
@@ -48116,6 +48340,29 @@ function deferEnding(ctx, step, ending) {
48116
48340
  return note;
48117
48341
  }
48118
48342
 
48343
+ // ../explore/dist/goal-loop/read-back.js
48344
+ function shownOn(step) {
48345
+ return [step.visibleText, ...step.snap.controls.map((c) => c.value ?? "")].join("\n");
48346
+ }
48347
+ async function readBackProbe(ctx, step) {
48348
+ const unread = ctx.observed.unreadShownIn(shownOn(step));
48349
+ if (unread.length === 0 || ctx.sideEffects.inflight().length > 0)
48350
+ return [];
48351
+ const r = await act(ctx.cfg.actor, { op: "reload", control: null });
48352
+ if (!r.ok)
48353
+ return [];
48354
+ const text2 = await readPageText(ctx.page, ctx.secrets).catch(() => "");
48355
+ const values2 = await ctx.page.evaluate(() => Array.from(document.querySelectorAll("input, textarea, select")).filter((e) => e.type !== "password").map((e) => e.value ?? "")).catch(() => []);
48356
+ const back = ctx.observed.readBack(await documentOf(ctx.page), redactPageText([text2, ...values2].join("\n"), ctx.secrets), ctx.page.url());
48357
+ const listed2 = (vs) => vs.map((v) => `"${v}"`).join(", ");
48358
+ const missing = unread.filter((v) => !back.includes(v));
48359
+ ctx.history.push(missing.length === 0 ? `read back after a reload: the page loaded from the server shows what the run sent (${listed2(back)})` : `after a reload, the page loaded from the server no longer shows what the run sent (${listed2(missing)})`);
48360
+ return back;
48361
+ }
48362
+ function unreadShown(ctx, step) {
48363
+ return ctx.observed.unreadShownIn(shownOn(step));
48364
+ }
48365
+
48119
48366
  // ../explore/dist/goal-loop/handle-report.js
48120
48367
  async function handleReport(ctx, step) {
48121
48368
  const { cfg } = ctx;
@@ -48137,6 +48384,8 @@ async function handleReport(ctx, step) {
48137
48384
  ctx.noteReplyText(snap.url, await readPageText(ctx.page, ctx.secrets));
48138
48385
  }
48139
48386
  const replyPages = ctx.replyGoal ? ctx.replies.pages() : null;
48387
+ if (replyPages === null)
48388
+ await readBackProbe(ctx, step);
48140
48389
  const verdict = replyPages !== null && replyPages.length === 0 ? {
48141
48390
  accept: false,
48142
48391
  reason: ctx.preSend === null ? "no reply observed: no message was sent yet \u2014 text on the page before the conversation is not a reply" : `no reply observed: no new message appeared after the send within the reply wait (${Math.round(ctx.replyWaitMs / 1e3)}s)`,
@@ -48150,7 +48399,7 @@ async function handleReport(ctx, step) {
48150
48399
  judge: cfg.judge,
48151
48400
  vetoes: ctx.vetoes,
48152
48401
  // #238: "none exists" is an answer only on observed pages that cover the app enough.
48153
- ...replyPages === null ? { topNav: ctx.observed.topNavigation(), ownInputs: ctx.observed.ownInputs() } : {}
48402
+ ...replyPages === null ? { topNav: ctx.observed.topNavigation(), ownInputs: ctx.observed.ownInputs(), unreadWrites: ctx.observed.unreadWrites() } : {}
48154
48403
  }).then((v) => v.accept && ctx.writeGoal && !ctx.wroteOk && v.answer.absent !== true ? { accept: false, reason: UNSAVED_WRITE_REASON, answer: v.answer } : v).catch((e) => ({ accept: false, reason: `no answer could be generated: ${firstLine(e)}`, answer: null }));
48155
48404
  if (verdict.accept) {
48156
48405
  const deferred = deferEnding(ctx, step, "report");
@@ -48170,6 +48419,12 @@ async function handleReport(ctx, step) {
48170
48419
  ctx.stop = "done";
48171
48420
  return "stop";
48172
48421
  }
48422
+ if (verdict.unconfirmedWrite === true && !ctx.readBackAsked) {
48423
+ ctx.readBackAsked = true;
48424
+ ctx.history.push(`report deferred: ${verdict.reason}`);
48425
+ record2(false, `report deferred: ${verdict.reason}`, { answer: verdict.answer, origin: "engine" });
48426
+ return "continue";
48427
+ }
48173
48428
  ctx.reportRejections += 1;
48174
48429
  for (const e of verdict.answer?.evidence ?? [])
48175
48430
  if (e.grounded)
@@ -48189,6 +48444,15 @@ async function handleReport(ctx, step) {
48189
48444
  }
48190
48445
 
48191
48446
  // ../explore/dist/goal-loop/handle-done.js
48447
+ async function unreadWriteHold(ctx, step, outcome) {
48448
+ if (outcome.status === "completed" && outcome.verifiedBy === "success-condition")
48449
+ return null;
48450
+ await readBackProbe(ctx, step);
48451
+ const unread = unreadShown(ctx, step);
48452
+ if (unread.length === 0)
48453
+ return null;
48454
+ return `what the run sent (${unread.map((v) => `"${v}"`).join(", ")}) has not shown on a page loaded since the write \u2014 the app may not have kept it (open the page where the app lists it)`;
48455
+ }
48192
48456
  async function handleDone(ctx, step) {
48193
48457
  const { cfg } = ctx;
48194
48458
  const { snap, decision, record: record2, signIn, pendingNote, acceptedBy, groundGoal } = step;
@@ -48200,7 +48464,7 @@ async function handleDone(ctx, step) {
48200
48464
  !(cfg.successCheck === void 0 && ctx.minEffort !== null && ctx.depth.report(ctx.tracker.actions, ctx.tracker.decisions, ctx.minEffort).minimum?.met === false)) {
48201
48465
  ctx.goalChecked.add(snap.signature);
48202
48466
  const { verdict, judgments } = await groundGoal();
48203
- if (verdict.accept) {
48467
+ if (verdict.accept && await unreadWriteHold(ctx, step, verdict.outcome) === null) {
48204
48468
  record2(true, `goal already met \u2014 stopped instead of "${decision.op}": ${acceptedBy(verdict.outcome)}`, {
48205
48469
  op: "done",
48206
48470
  control: null,
@@ -48236,7 +48500,19 @@ async function handleDone(ctx, step) {
48236
48500
  }
48237
48501
  }
48238
48502
  if (decision.op === "done") {
48239
- const { verdict, judgments } = await groundGoal(false, true);
48503
+ const grounded = await groundGoal(false, true);
48504
+ const { judgments } = grounded;
48505
+ let verdict = grounded.verdict;
48506
+ const hold = verdict.accept ? await unreadWriteHold(ctx, step, verdict.outcome) : null;
48507
+ if (hold !== null) {
48508
+ if (!ctx.readBackAsked) {
48509
+ ctx.readBackAsked = true;
48510
+ ctx.history.push(`done deferred: ${hold}`);
48511
+ record2(false, `done deferred: ${hold}`, { origin: "engine" });
48512
+ return "continue";
48513
+ }
48514
+ verdict = { accept: false, reason: hold };
48515
+ }
48240
48516
  if (verdict.accept) {
48241
48517
  record2(true, `done accepted${pendingNote(verdict.outcome) === null ? "" : " provisionally"}: ${acceptedBy(verdict.outcome)}`, {
48242
48518
  ...judgments === void 0 ? {} : { judgments }
@@ -48943,10 +49219,20 @@ async function handleSelectOption(ctx, step, op) {
48943
49219
  return "continue";
48944
49220
  }
48945
49221
  const named = text2 === null ? null : matchOption(text2, options);
49222
+ const field3 = control.name || control.summary;
49223
+ const noop2 = `${quote(named ?? "", 60)} is already selected in ${field3} \u2014 nothing to change`;
49224
+ const setByRun = () => ctx.transcript.entries().some((e) => e.op === "select" && e.actOk && e.value === named && (e.target ?? "").includes(`"${control.name}"`));
49225
+ if (named !== null && named === current2 && !isPlaceholderOption(named) && !ctx.history.includes(noop2) && !setByRun()) {
49226
+ ctx.fillHelper.commit();
49227
+ ctx.history.push(noop2);
49228
+ record2(true, noop2, { value: named });
49229
+ ctx.lastActedOp = op;
49230
+ return "continue";
49231
+ }
48946
49232
  if (named !== null && !choices.includes(named)) {
48947
49233
  ctx.fillHelper.commit();
48948
49234
  const why = named === current2 ? `${quote(named, 60)} is already selected` : `${quote(named, 60)} is a placeholder, not a choice`;
48949
- const reason = `select refused: ${why} in ${control.name || control.summary} \u2014 options not tried: ${untried()}`;
49235
+ const reason = `select refused: ${why} in ${field3} \u2014 options not tried: ${untried()}`;
48950
49236
  ctx.history.push(reason);
48951
49237
  record2(false, reason, { origin: "engine", value: named });
48952
49238
  ctx.lastActedOp = op;
@@ -49245,7 +49531,8 @@ async function perceiveStep(ctx) {
49245
49531
  if (lc !== null && lc !== ctx.settledClick) {
49246
49532
  ctx.settledClick = lc;
49247
49533
  if (lc.writes.length > 0 && lc.writes.every((w) => w.status !== null && w.status >= 200 && w.status < 300)) {
49248
- ctx.observed.confirmOwnInputs();
49534
+ ctx.observed.noteSentValues(lc.sentValues, ctx.page.url());
49535
+ ctx.observed.confirmOwnInputs(await documentOf(ctx.page));
49249
49536
  ctx.wroteOk = true;
49250
49537
  }
49251
49538
  }
@@ -49696,6 +49983,7 @@ var APPEARED_CHARS = 400;
49696
49983
  function clipLine(s3) {
49697
49984
  return s3.length <= APPEARED_CHARS ? s3 : `${s3.slice(0, APPEARED_CHARS - 1)}\u2026`;
49698
49985
  }
49986
+ var PROGRESS_LINE = /\b\d+\s*(?:of|\/)\s*\d+\b|\b(?:step|question|page|part|stage|section|confirmation)\s+\d+\b/i;
49699
49987
  function appearedLines(before, after) {
49700
49988
  const lines = (t) => t.split("\n").map((l2) => l2.replace(/\s+/g, " ").trim()).filter((l2) => l2 !== "");
49701
49989
  const shape3 = (l2) => l2.replace(/\d+/g, "#");
@@ -49710,7 +49998,10 @@ function appearedLines(before, after) {
49710
49998
  else
49711
49999
  out.push(l2);
49712
50000
  }
49713
- return out;
50001
+ if (out.length > 0)
50002
+ return out;
50003
+ const was = new Set(lines(before));
50004
+ return lines(after).filter((l2) => PROGRESS_LINE.test(l2) && !was.has(l2));
49714
50005
  }
49715
50006
 
49716
50007
  // ../explore/dist/goal-loop/page-view.js
@@ -49791,8 +50082,12 @@ async function viewPage(ctx, step) {
49791
50082
  ctx.chrome.observe(new URL(snap.url).pathname, snap.controls);
49792
50083
  } catch {
49793
50084
  }
50085
+ const document2 = await documentOf(ctx.page);
50086
+ const unreadBefore = [...ctx.observed.unreadWrites()];
49794
50087
  ctx.observed.add(snap.url, visibleText, [...controlFields(snap.controls), ...richFields], {
49795
50088
  ...await readPageHeadings(ctx.page, ctx.secrets),
50089
+ // #647: which document load this is — a later one that shows a sent value reads it back.
50090
+ ...document2 === void 0 ? {} : { document: document2 },
49796
50091
  // #223: the action / label names (a quote made only of them is a label, not an answer) and
49797
50092
  // the document's status (an answer on a 404 page is no answer). A link that is page content
49798
50093
  // (in the main content, a list, a table, a card) is NOT one: its text may be the answer.
@@ -49805,6 +50100,13 @@ async function viewPage(ctx, step) {
49805
50100
  navLinks: snap.controls.filter((c) => c.role === "link" && (c.landmark === "navigation" || c.landmark === "banner")).flatMap((c) => typeof c.href === "string" && c.href !== "" ? [c.href] : []),
49806
50101
  ...ctx.documentStatus.has(ctx.docKey(ctx.page.url())) ? { status: ctx.documentStatus.get(ctx.docKey(ctx.page.url())) } : {}
49807
50102
  });
50103
+ {
50104
+ const now2 = ctx.observed.unreadWrites();
50105
+ const back = unreadBefore.filter((v) => !now2.has(v));
50106
+ if (back.length > 0) {
50107
+ ctx.history.push(`read back: this page, loaded from the server, shows what the run sent (${back.map((v) => `"${v}"`).join(", ")}) \u2014 the app kept it`);
50108
+ }
50109
+ }
49808
50110
  ctx.noteReplyText(snap.url, visibleText);
49809
50111
  ctx.depth.noteState(snap.signature, snap.url);
49810
50112
  {
@@ -53099,13 +53401,13 @@ function lintUploadFixtures(journey, baseDir) {
53099
53401
  });
53100
53402
  continue;
53101
53403
  }
53102
- const sensitive = fixtureSensitiveReason(real, realRootOf(baseDir)) ?? fixtureSensitiveReason(candidate, resolve5(baseDir));
53103
- if (sensitive !== null) {
53404
+ const sensitive2 = fixtureSensitiveReason(real, realRootOf(baseDir)) ?? fixtureSensitiveReason(candidate, resolve5(baseDir));
53405
+ if (sensitive2 !== null) {
53104
53406
  findings.push({
53105
53407
  rule: "upload-sensitive-file",
53106
53408
  level: "error",
53107
53409
  step: i + 1,
53108
- message: `step ${i + 1}: upload fixture ${candidate} looks sensitive (${sensitive})`,
53410
+ message: `step ${i + 1}: upload fixture ${candidate} looks sensitive (${sensitive2})`,
53109
53411
  fix: "use a dedicated test fixture file"
53110
53412
  });
53111
53413
  continue;
@@ -53230,7 +53532,7 @@ function lintJourney(journey, opts = {}) {
53230
53532
  const refsBound = (a) => assertionRefsBound(journey, a);
53231
53533
  const effects = sites.filter((site) => isEffectSite(site, site.where === "step" ? stepOf(site.step) : void 0, refsBound));
53232
53534
  const endEffects = effects.filter((site) => site.where === "end-state");
53233
- const effectAt = (n3) => effects.some((site) => (site.where === "step" || site.where === "step-request") && site.step === n3);
53535
+ const effectAt = (n3) => recorded[n3 - 1]?.step.kind === "waitFor" || effects.some((site) => (site.where === "step" || site.where === "step-request") && site.step === n3);
53234
53536
  const writeRequests = recorded.map((r) => writeRequestsOf(r, classify2));
53235
53537
  const isWrite2 = recorded.map((r, i) => isWriteStep(r, classify2, writeRequests[i]));
53236
53538
  const lastWrite = isWrite2.lastIndexOf(true) + 1;
@@ -53433,9 +53735,9 @@ function splitBudget(total, stops) {
53433
53735
  var CLAIM_KEYS = /* @__PURE__ */ new Set(["expect", "check", "resume", "waitFor"]);
53434
53736
  function actionRefs(step) {
53435
53737
  const out = /* @__PURE__ */ new Set();
53436
- const walk = (v) => {
53738
+ const walk2 = (v) => {
53437
53739
  if (Array.isArray(v)) {
53438
- v.forEach(walk);
53740
+ v.forEach(walk2);
53439
53741
  return;
53440
53742
  }
53441
53743
  if (v === null || typeof v !== "object")
@@ -53450,9 +53752,9 @@ function actionRefs(step) {
53450
53752
  out.add(n3);
53451
53753
  for (const [k, x] of Object.entries(o))
53452
53754
  if (!CLAIM_KEYS.has(k))
53453
- walk(x);
53755
+ walk2(x);
53454
53756
  };
53455
- walk(step);
53757
+ walk2(step);
53456
53758
  return [...out];
53457
53759
  }
53458
53760
  function sitePageAssertion(site) {
@@ -57249,7 +57551,7 @@ function seedPath(seedUrl) {
57249
57551
  }
57250
57552
 
57251
57553
  // ../explore/dist/adversarial/scope.js
57252
- function routeOf(url2) {
57554
+ function routeOf2(url2) {
57253
57555
  const path2 = new URL(url2).pathname;
57254
57556
  return path2.length > 1 && path2.endsWith("/") ? path2.slice(0, -1) : path2;
57255
57557
  }
@@ -57269,7 +57571,7 @@ function startRouteGlobs(startUrl) {
57269
57571
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
57270
57572
  throw new ScopeUnderivableError(`a ${parsed.protocol} URL has no route`);
57271
57573
  }
57272
- const route = routeOf(startUrl);
57574
+ const route = routeOf2(startUrl);
57273
57575
  return route === "/" ? ["/", "/**"] : [route, `${route}/`, `${route}/**`];
57274
57576
  }
57275
57577
  function scopeGlobs(startUrl, extra = []) {
@@ -59401,7 +59703,7 @@ var CoverageTracker = class {
59401
59703
  if (this.#counts(c))
59402
59704
  this.#controls.add(key2);
59403
59705
  }
59404
- const route = routeOf(snapshot2.url);
59706
+ const route = routeOf2(snapshot2.url);
59405
59707
  const formControls = this.#region === void 0 ? snapshot2.controls : snapshot2.controls.filter((c) => c.inRegion === true);
59406
59708
  for (const f2 of detectForms(formControls, this.#inScope))
59407
59709
  this.#forms.add(`${route}|${f2.key}`);
@@ -59434,7 +59736,7 @@ var CoverageTracker = class {
59434
59736
  submitted(url2, formKey) {
59435
59737
  if (!this.#inScope(url2))
59436
59738
  return;
59437
- this.#submitted.add(`${routeOf(url2)}|${formKey}`);
59739
+ this.#submitted.add(`${routeOf2(url2)}|${formKey}`);
59438
59740
  }
59439
59741
  /**
59440
59742
  * A submit of `formKey` (on page `url`) never reached the server, so it never counts toward
@@ -59445,7 +59747,7 @@ var CoverageTracker = class {
59445
59747
  blocked(url2, formKey, message2, why = "validation") {
59446
59748
  if (!this.#inScope(url2))
59447
59749
  return;
59448
- const form = `${routeOf(url2)}|${formKey}`;
59750
+ const form = `${routeOf2(url2)}|${formKey}`;
59449
59751
  const key2 = `${form}|${why}`;
59450
59752
  const cur = this.#blocked.get(key2);
59451
59753
  this.#blocked.set(key2, { form, why, count: (cur?.count ?? 0) + 1, message: cur?.message ?? message2 });
@@ -60249,6 +60551,16 @@ function submitRequestSent(monitor, sinceMs) {
60249
60551
  return true;
60250
60552
  return false;
60251
60553
  }
60554
+ var SUBMIT_REQUEST_POLL_MS = 50;
60555
+ async function submitRequestSentWithin(monitor, sinceMs, boundMs) {
60556
+ const deadline = clock.now() + boundMs;
60557
+ while (!submitRequestSent(monitor, sinceMs)) {
60558
+ if (clock.now() >= deadline)
60559
+ return false;
60560
+ await clock.sleep(SUBMIT_REQUEST_POLL_MS);
60561
+ }
60562
+ return true;
60563
+ }
60252
60564
  async function nativeValidationMessage(page) {
60253
60565
  return page.evaluate(() => {
60254
60566
  const fields = Array.from(document.querySelectorAll("input,select,textarea"));
@@ -61468,6 +61780,7 @@ async function refuseMisuseStep(ctx, ep, turn, s3) {
61468
61780
  }
61469
61781
 
61470
61782
  // ../explore/dist/missions/adversarial-hunt/episode.js
61783
+ var SUBMIT_REQUEST_GRACE_MS = 1e3;
61471
61784
  async function runEpisode(ctx, overlay, turn) {
61472
61785
  const ep = {};
61473
61786
  const { ran, planning, episode } = turn;
@@ -61550,10 +61863,12 @@ async function runEpisode(ctx, overlay, turn) {
61550
61863
  if (result.ok)
61551
61864
  ctx.cov.acted(ep.stepSnap.url, s3.control);
61552
61865
  if (result.ok && s3.submitsForm !== void 0) {
61553
- if (submitRequestSent(monitorFor(ctx.sessions.page), at3)) {
61866
+ const monitor = monitorFor(ctx.sessions.page);
61867
+ const invalid = submitRequestSent(monitor, at3) ? void 0 : await nativeValidationMessage(ctx.sessions.page);
61868
+ if (invalid === void 0 && await submitRequestSentWithin(monitor, at3, SUBMIT_REQUEST_GRACE_MS)) {
61554
61869
  ctx.cov.submitted(ep.stepSnap.url, s3.submitsForm);
61555
61870
  } else {
61556
- ctx.cov.blocked(ep.stepSnap.url, s3.submitsForm, await nativeValidationMessage(ctx.sessions.page));
61871
+ ctx.cov.blocked(ep.stepSnap.url, s3.submitsForm, invalid);
61557
61872
  }
61558
61873
  }
61559
61874
  if (ran === "visit-route" && s3.control !== null)
@@ -62744,9 +63059,9 @@ function hostStarvedFailure(failure3, health) {
62744
63059
  }
62745
63060
 
62746
63061
  // src/build-info.generated.ts
62747
- var GENERATED_COMMIT = "3dd5b59";
62748
- var GENERATED_TREE = "d1ca79a";
62749
- var GENERATED_BUILT_AT = "2026-10-23T00:39:12.107Z";
63062
+ var GENERATED_COMMIT = "ad72163";
63063
+ var GENERATED_TREE = "379caf5";
63064
+ var GENERATED_BUILT_AT = "2026-10-23T21:02:56.700Z";
62750
63065
 
62751
63066
  // src/version.ts
62752
63067
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -62768,13 +63083,13 @@ function readCliVersion() {
62768
63083
 
62769
63084
  // src/engine.ts
62770
63085
  function bundledCommit() {
62771
- return true ? "3dd5b59" : void 0;
63086
+ return true ? "ad72163" : void 0;
62772
63087
  }
62773
63088
  function bundledBuiltAt() {
62774
- return true ? "2026-10-23T00:39:12.107Z" : void 0;
63089
+ return true ? "2026-10-23T21:02:56.700Z" : void 0;
62775
63090
  }
62776
63091
  function bundledTree() {
62777
- return true ? "d1ca79a" : void 0;
63092
+ return true ? "379caf5" : void 0;
62778
63093
  }
62779
63094
  function currentBuildTree() {
62780
63095
  return bundledTree() ?? GENERATED_TREE;
@@ -85907,7 +86222,7 @@ var FINGERPRINT_MS = 1e4;
85907
86222
  function oneLine2(s3) {
85908
86223
  return s3.replace(/\s+/g, " ").trim();
85909
86224
  }
85910
- function routeOf2(url2) {
86225
+ function routeOf3(url2) {
85911
86226
  try {
85912
86227
  const u = new URL(url2);
85913
86228
  return u.pathname + (u.search === "" ? "" : u.search);
@@ -85984,7 +86299,7 @@ var RunScreenshots = class {
85984
86299
  this.#entries.push({
85985
86300
  n: n3,
85986
86301
  step: at3.step,
85987
- route: this.#redact(redactUrl(routeOf2(url2))),
86302
+ route: this.#redact(redactUrl(routeOf3(url2))),
85988
86303
  what: this.#redact(oneLine2(at3.what)).slice(0, 300),
85989
86304
  path: path2
85990
86305
  });
@@ -86308,7 +86623,7 @@ function invariantSetupTexts(spec) {
86308
86623
  return Object.fromEntries(specStrings(spec, "", []).filter(([, s3]) => s3.includes("${setup.")).map(([p, s3]) => [`--invariants ${p}`, s3]));
86309
86624
  }
86310
86625
  function substituteSpecSetupRefs(spec, b2, baseUrl) {
86311
- const walk = (v, path2) => {
86626
+ const walk2 = (v, path2) => {
86312
86627
  if (typeof v === "string") {
86313
86628
  if (!v.includes("${setup.")) return v;
86314
86629
  const where = `--invariants ${path2}`;
@@ -86323,11 +86638,11 @@ function substituteSpecSetupRefs(spec, b2, baseUrl) {
86323
86638
  if (before !== after) throw new UnboundSetupRefError(`${where}: a \${setup.*} value may fill the path or query, never the origin`);
86324
86639
  return out;
86325
86640
  }
86326
- if (Array.isArray(v)) return v.map((x, i) => walk(x, `${path2}[${i}]`));
86327
- if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, path2 === "" ? k : `${path2}.${k}`)]));
86641
+ if (Array.isArray(v)) return v.map((x, i) => walk2(x, `${path2}[${i}]`));
86642
+ if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk2(x, path2 === "" ? k : `${path2}.${k}`)]));
86328
86643
  return v;
86329
86644
  };
86330
- return walk(spec, "");
86645
+ return walk2(spec, "");
86331
86646
  }
86332
86647
  function fixtureSetupFailedResult(err, fx) {
86333
86648
  const reason = err instanceof FixtureSetupError ? err.message : `fixture setup failed: ${err.message}`;
@@ -87627,6 +87942,7 @@ var ServerLogRuntime = class {
87627
87942
  };
87628
87943
  }
87629
87944
  };
87945
+ var NON_ACTING_OPS = /* @__PURE__ */ new Set(["done", "blocked", null]);
87630
87946
  function stepWindows(transcript, stepEpoch, missionStartEpochMs, drainMs) {
87631
87947
  const windows = [];
87632
87948
  let prevEnd = missionStartEpochMs;
@@ -87635,7 +87951,9 @@ function stepWindows(transcript, stepEpoch, missionStartEpochMs, drainMs) {
87635
87951
  const settle2 = stepEpoch.get(entry.step) ?? prevEnd;
87636
87952
  const isLast = i === transcript.length - 1;
87637
87953
  const endMs = isLast ? settle2 + drainMs : settle2;
87638
- windows.push({ step: entry.step, startMs: prevEnd, endMs });
87954
+ const acting = windows[windows.length - 1];
87955
+ if (NON_ACTING_OPS.has(entry.op) && acting !== void 0) acting.endMs = endMs;
87956
+ else windows.push({ step: entry.step, startMs: prevEnd, endMs });
87639
87957
  prevEnd = settle2;
87640
87958
  }
87641
87959
  return windows;
@@ -88532,7 +88850,7 @@ function findingIdentity(f2) {
88532
88850
  function str2(v, fallback = "") {
88533
88851
  return typeof v === "string" ? v : fallback;
88534
88852
  }
88535
- function routeOf3(item) {
88853
+ function routeOf4(item) {
88536
88854
  if (typeof item.route === "string") return item.route;
88537
88855
  if (typeof item.url === "string") return normalizeRoute(item.url);
88538
88856
  return "/";
@@ -88544,7 +88862,7 @@ function extractRunFindings(data) {
88544
88862
  if (!Array.isArray(list2)) return;
88545
88863
  for (const item of list2) {
88546
88864
  if (!isRecord(item) || typeof item.fingerprint !== "string") continue;
88547
- out.push({ kind: kindOf(item), fingerprint: item.fingerprint, route: routeOf3(item), title: str2(item.title, item.fingerprint) });
88865
+ out.push({ kind: kindOf(item), fingerprint: item.fingerprint, route: routeOf4(item), title: str2(item.title, item.fingerprint) });
88548
88866
  }
88549
88867
  };
88550
88868
  fingerprinted(data.defects, (d) => d.kind === "invariant" ? "invariant" : `defect/${str2(d.kind, "unknown")}`);
@@ -88553,14 +88871,14 @@ function extractRunFindings(data) {
88553
88871
  if (isRecord(data.coverage) && Array.isArray(data.coverage.defects)) {
88554
88872
  for (const d of data.coverage.defects) {
88555
88873
  if (!isRecord(d) || typeof d.stateFingerprint !== "string") continue;
88556
- out.push({ kind: "coverage-defect", fingerprint: d.stateFingerprint, route: routeOf3(d), title: str2(d.reason, "looks broken") });
88874
+ out.push({ kind: "coverage-defect", fingerprint: d.stateFingerprint, route: routeOf4(d), title: str2(d.reason, "looks broken") });
88557
88875
  }
88558
88876
  }
88559
88877
  if (isRecord(data.report) && Array.isArray(data.report.findings)) {
88560
88878
  for (const f2 of data.report.findings) {
88561
88879
  if (!isRecord(f2) || typeof f2.rubricItemId !== "string") continue;
88562
88880
  const controls = Array.isArray(f2.controls) ? f2.controls.filter((c) => typeof c === "string") : [];
88563
- out.push({ kind: "ux", route: routeOf3(f2), title: str2(f2.observation, f2.rubricItemId), rubricItemId: f2.rubricItemId, controls });
88881
+ out.push({ kind: "ux", route: routeOf4(f2), title: str2(f2.observation, f2.rubricItemId), rubricItemId: f2.rubricItemId, controls });
88564
88882
  }
88565
88883
  }
88566
88884
  return out;
@@ -91645,18 +91963,18 @@ function rebaseRecording(recording, env) {
91645
91963
  if (mustBeAllowed && !env.allowedOrigins.includes(u.origin)) refusals.push(`${where} is on ${u.origin}`);
91646
91964
  return raw;
91647
91965
  };
91648
- const walk = (v, where) => {
91649
- if (Array.isArray(v)) return v.map((x, i) => walk(x, `${where}[${i}]`));
91966
+ const walk2 = (v, where) => {
91967
+ if (Array.isArray(v)) return v.map((x, i) => walk2(x, `${where}[${i}]`));
91650
91968
  if (v === null || typeof v !== "object") return v;
91651
91969
  const o = v;
91652
91970
  const out = {};
91653
- for (const [k, x] of Object.entries(o)) out[k] = walk(x, `${where}.${k}`);
91971
+ for (const [k, x] of Object.entries(o)) out[k] = walk2(x, `${where}.${k}`);
91654
91972
  if (o.kind === "navigate" && typeof o.url === "string") out.url = move(o.url, `${where} (navigate ${o.url})`, true);
91655
91973
  if (o.kind === "urlIncludes" && typeof o.text === "string") out.text = move(o.text, `${where}.text`, false);
91656
91974
  return out;
91657
91975
  };
91658
91976
  const pages = recording.pages.map((p, i) => ({
91659
- ...walk(p, `pages[${i}]`),
91977
+ ...walk2(p, `pages[${i}]`),
91660
91978
  url: move(p.url, `pages[${i}] (${p.url})`, true)
91661
91979
  }));
91662
91980
  if (refusals.length > 0) {
@@ -94428,7 +94746,7 @@ function classifyTarget(d, testIdAttributes, via) {
94428
94746
  function kebabCase(s3) {
94429
94747
  return s3.normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
94430
94748
  }
94431
- function routeOf4(url2) {
94749
+ function routeOf5(url2) {
94432
94750
  if (url2 === void 0 || url2 === "") return void 0;
94433
94751
  try {
94434
94752
  return new URL(url2).pathname;
@@ -94526,7 +94844,7 @@ function dedupeSuggestions(lists) {
94526
94844
  return [...byKey.values()].sort((a, b2) => b2.occurrences.length - a.occurrences.length || a.key.localeCompare(b2.key));
94527
94845
  }
94528
94846
  function flatWithRoute(rec) {
94529
- return rec.pages.flatMap((p) => p.steps.map((recorded) => ({ recorded, route: routeOf4(p.url) })));
94847
+ return rec.pages.flatMap((p) => p.steps.map((recorded) => ({ recorded, route: routeOf5(p.url) })));
94530
94848
  }
94531
94849
  function healthLine(h) {
94532
94850
  const total = h.stable + h.brittle;
@@ -96830,6 +97148,43 @@ function reportSlowness(replays) {
96830
97148
  import { mkdir as mkdir22, writeFile as writeFile25 } from "node:fs/promises";
96831
97149
  import { join as join52, resolve as resolve16 } from "node:path";
96832
97150
 
97151
+ // src/outcome-targets.ts
97152
+ function evaluateOutcomeTargets(outcomes, baseline) {
97153
+ return outcomes.map((o) => evaluateOne(o, baseline));
97154
+ }
97155
+ function evaluateOne(o, baseline) {
97156
+ const metric = o.metric;
97157
+ const target = o.target;
97158
+ if (metric === void 0 || target === void 0) {
97159
+ return { outcomeId: o.id, status: "not-decidable-by-replay", reason: "no metric or no target" };
97160
+ }
97161
+ const written2 = `${target.op} ${target.value} ${target.unit}`;
97162
+ if (metric.kind !== "duration") {
97163
+ return { outcomeId: o.id, status: "not-decidable-by-replay", target: written2, reason: `a ${metric.kind} metric needs real users, not a replay` };
97164
+ }
97165
+ const at3 = (name) => baseline.anchors.find((a) => a.name === name)?.atMs;
97166
+ const from = at3(metric.from);
97167
+ const to = at3(metric.to);
97168
+ const missing = from === void 0 ? metric.from : to === void 0 ? metric.to : null;
97169
+ if (missing !== null || from === void 0 || to === void 0) {
97170
+ return { outcomeId: o.id, status: "unmeasured", target: written2, reason: `anchor '${missing}' is not in the replay's baseline` };
97171
+ }
97172
+ const floorSeconds = Math.max(0, to - from) / 1e3;
97173
+ const floor = `the replay took ${floorSeconds}s from '${metric.from}' to '${metric.to}'`;
97174
+ if (metric.stat === "share_under") {
97175
+ const wantsShare = (target.op === ">" || target.op === ">=") && target.unit === "percent" && target.value > 0;
97176
+ if (!wantsShare || metric.threshold === void 0) {
97177
+ return { outcomeId: o.id, status: "not-decidable-by-replay", floorSeconds, target: written2, reason: "only a lower bound on the share under a threshold is decidable" };
97178
+ }
97179
+ return metric.threshold < floorSeconds ? { outcomeId: o.id, status: "miss", floorSeconds, target: written2, reason: `${floor}, over the ${metric.threshold}s threshold: no one finishes under it` } : { outcomeId: o.id, status: "not-decidable", floorSeconds, target: written2, reason: `${floor}, within the ${metric.threshold}s threshold` };
97180
+ }
97181
+ if (target.unit !== "s" || target.op !== "<" && target.op !== "<=") {
97182
+ return { outcomeId: o.id, status: "not-decidable-by-replay", floorSeconds, target: written2, reason: "only an upper bound in seconds is decidable" };
97183
+ }
97184
+ const breaks = target.op === "<" ? floorSeconds >= target.value : floorSeconds > target.value;
97185
+ return breaks ? { outcomeId: o.id, status: "miss", floorSeconds, target: written2, reason: `${floor}: the fastest path already breaks ${written2}` } : { outcomeId: o.id, status: "not-decidable", floorSeconds, target: written2, reason: `${floor}, within ${written2}` };
97186
+ }
97187
+
96833
97188
  // src/ux-usability/setup.ts
96834
97189
  import { join as join46, resolve as resolvePath11 } from "node:path";
96835
97190
 
@@ -97259,7 +97614,7 @@ function controlKey2(c) {
97259
97614
  }
97260
97615
 
97261
97616
  // ../ux/dist/route.js
97262
- function routeOf5(url2) {
97617
+ function routeOf6(url2) {
97263
97618
  try {
97264
97619
  return urlTemplate(new URL(url2).pathname || "/");
97265
97620
  } catch {
@@ -97408,7 +97763,7 @@ function a11yChecks(evidence, rubric) {
97408
97763
  const q = rubric.get(id)?.questions[0];
97409
97764
  return q?.severity ?? "minor";
97410
97765
  };
97411
- const route = routeOf5(evidence.url);
97766
+ const route = routeOf6(evidence.url);
97412
97767
  const controlByRef = new Map(evidence.controls.map((c) => [`control:${c.index}`, c]));
97413
97768
  const labelOf2 = (refId) => {
97414
97769
  const c = controlByRef.get(refId);
@@ -97709,6 +98064,7 @@ var SIGNAL_RULES = {
97709
98064
  "duplicate-create": { id: "signal-duplicate-create", principle: "Error prevention", citation: NNG, severity: "major" },
97710
98065
  "failed-submit": { id: "signal-failed-submit", principle: "Help users recognize, diagnose, and recover from errors", citation: NNG, severity: "major" },
97711
98066
  "url-mismatch": { id: "signal-url-mismatch", principle: "Consistency and standards", citation: NNG, severity: "minor" },
98067
+ "repeated-advance": { id: "signal-repeated-advance", principle: "Flexibility and efficiency of use", citation: NNG, severity: "minor" },
97712
98068
  "horizontal-overflow": { id: "signal-horizontal-overflow", principle: "Flexibility and efficiency of use", citation: NNG, severity: "major" },
97713
98069
  "vertical-clipping": { id: "signal-vertical-clipping", principle: "Visibility of system status", citation: NNG, severity: "major" }
97714
98070
  };
@@ -97750,7 +98106,7 @@ function makeSignalFinding(input2) {
97750
98106
  recommendation: input2.recommendation.trim(),
97751
98107
  tier: "signal",
97752
98108
  screenId: input2.screenId,
97753
- route: routeOf5(input2.url),
98109
+ route: routeOf6(input2.url),
97754
98110
  controls: [...input2.controls ?? []],
97755
98111
  quotes: [...input2.quotes ?? []],
97756
98112
  occurrences: input2.occurrences ?? 1,
@@ -97791,6 +98147,14 @@ function quoteLine(text2, needle) {
97791
98147
  const t = line2.replace(/\s+/g, " ").trim();
97792
98148
  return t.length <= 160 ? t : `${t.slice(0, 157)}...`;
97793
98149
  }
98150
+ function labelOf(s3) {
98151
+ const quoted = /"([^"]{2,160})"/.exec(s3.target ?? "")?.[1];
98152
+ const raw = quoted ?? s3.descriptor?.name ?? s3.descriptor?.text ?? null;
98153
+ if (raw === null || raw === void 0)
98154
+ return null;
98155
+ const t = raw.replace(/[→←↗›»…]+/g, " ").replace(/\s+/g, " ").trim();
98156
+ return t.length >= 2 ? t : null;
98157
+ }
97794
98158
 
97795
98159
  // ../ux/dist/signals/basic.js
97796
98160
  function detectHungRequests(capture2, opts = {}) {
@@ -97979,7 +98343,7 @@ function detectInternalIds(capture2) {
97979
98343
  if (typed.includes(id))
97980
98344
  continue;
97981
98345
  const line2 = quoteLine(screen.visibleText, id);
97982
- const key2 = `${routeOf5(screen.url)}|${pattern.name}`;
98346
+ const key2 = `${routeOf6(screen.url)}|${pattern.name}`;
97983
98347
  const g = groups.get(key2) ?? { pattern, ids: /* @__PURE__ */ new Set(), lines: /* @__PURE__ */ new Set(), screens: [], intentional: true };
97984
98348
  g.ids.add(id);
97985
98349
  g.lines.add(line2);
@@ -98080,6 +98444,71 @@ function detectInertControls(capture2) {
98080
98444
  return out;
98081
98445
  }
98082
98446
 
98447
+ // ../ux/dist/signals/effort.js
98448
+ var REPEATED_ADVANCE_MIN = 3;
98449
+ var ADVANCE_NAME = /^(continue|next|next step|confirm|ok|okay|proceed|yes|go on)$/i;
98450
+ var INPUT_OPS = /* @__PURE__ */ new Set(["type", "select", "send", "upload", "fill", "edit_text", "check", "uncheck"]);
98451
+ var PASSIVE_OPS = /* @__PURE__ */ new Set(["wait", "scroll_down", "scroll_up"]);
98452
+ function advanced(capture2, s3) {
98453
+ const before = capture2.screens.filter((x) => x.step === s3.step).pop();
98454
+ const after = capture2.screens.find((x) => x.step > s3.step);
98455
+ if (before === void 0 || after === void 0 || routeOf6(after.url) !== routeOf6(before.url))
98456
+ return void 0;
98457
+ return after.visibleText !== before.visibleText ? after : void 0;
98458
+ }
98459
+ function detectRepeatedAdvances(capture2) {
98460
+ const streaks = [];
98461
+ let current2 = [];
98462
+ let key2 = null;
98463
+ const close = () => {
98464
+ if (current2.length >= REPEATED_ADVANCE_MIN)
98465
+ streaks.push(current2);
98466
+ current2 = [];
98467
+ key2 = null;
98468
+ };
98469
+ for (const s3 of capture2.steps) {
98470
+ if (s3.op !== null && PASSIVE_OPS.has(s3.op))
98471
+ continue;
98472
+ const label2 = s3.op === "click" && s3.actOk ? labelOf(s3) : null;
98473
+ const after = label2 !== null && ADVANCE_NAME.test(label2) ? advanced(capture2, s3) : void 0;
98474
+ if (label2 === null || after === void 0 || s3.op !== null && INPUT_OPS.has(s3.op)) {
98475
+ close();
98476
+ continue;
98477
+ }
98478
+ const k = `${routeOf6(s3.url)}|${label2.toLowerCase()}`;
98479
+ if (key2 !== null && k !== key2)
98480
+ close();
98481
+ key2 = k;
98482
+ current2.push({ step: s3, after });
98483
+ }
98484
+ close();
98485
+ return streaks.map((hits) => {
98486
+ const label2 = labelOf(hits[0].step) ?? "the advance control";
98487
+ const n3 = hits.length;
98488
+ const steps = hits.map((h) => h.step.step);
98489
+ const last = hits[n3 - 1];
98490
+ return makeSignalFinding({
98491
+ kind: "repeated-advance",
98492
+ confidence: n3 >= 5 ? 0.8 : n3 === 4 ? 0.7 : 0.6,
98493
+ url: last.after.url,
98494
+ screenId: last.after.signature,
98495
+ observation: `The flow needed "${label2}" ${n3} times in a row (steps ${steps.join(", ")}) with nothing entered between them, each only moving to the next screen.`,
98496
+ userImpact: "Every extra confirmation costs the user time and attention without adding a decision; repeated identical steps also read as the page not responding.",
98497
+ recommendation: `Fold the repeated "${label2}" screens into one step, or let the user confirm once.`,
98498
+ controls: [label2],
98499
+ occurrences: n3,
98500
+ screenIds: [...new Set(hits.map((h) => h.after.signature))],
98501
+ evidence: {
98502
+ kind: "repeated-advance",
98503
+ steps,
98504
+ requests: [],
98505
+ ...last.after.screenshot === void 0 ? {} : { screenshot: last.after.screenshot },
98506
+ detail: `${n3} consecutive successful "${label2}" clicks on ${routeOf6(last.after.url)} with no input between them, each changing the page text`
98507
+ }
98508
+ });
98509
+ });
98510
+ }
98511
+
98083
98512
  // ../ux/dist/signals/journey.js
98084
98513
  var ERROR_TEXT = /\b(error|errors|failed|failure|went wrong|unavailable|couldn['’]?t|could not|can['’]?t|cannot|unable|try again|invalid|problem|denied|forbidden|expired|not (?:be )?(?:saved|sent|found|recorded))\b/i;
98085
98514
  var GENERIC_ERROR = /\b(something went wrong|an? (?:unexpected |unknown )?error (?:has )?occurred|unexpected error|oops|try again later)\b/i;
@@ -98088,14 +98517,6 @@ var FIELD_OPS = /* @__PURE__ */ new Set(["type", "select"]);
98088
98517
  function normLine(s3) {
98089
98518
  return s3.replace(/\s+/g, " ").trim().toLowerCase();
98090
98519
  }
98091
- function labelOf(s3) {
98092
- const quoted = /"([^"]{2,160})"/.exec(s3.target ?? "")?.[1];
98093
- const raw = quoted ?? s3.descriptor?.name ?? s3.descriptor?.text ?? null;
98094
- if (raw === null || raw === void 0)
98095
- return null;
98096
- const t = raw.replace(/[→←↗›»…]+/g, " ").replace(/\s+/g, " ").trim();
98097
- return t.length >= 2 ? t : null;
98098
- }
98099
98520
  function screenBefore(capture2, step) {
98100
98521
  return capture2.screens.filter((s3) => s3.step <= step).pop();
98101
98522
  }
@@ -98134,7 +98555,7 @@ function detectStuckJobs(capture2, opts = {}) {
98134
98555
  const label2 = labelOf(start);
98135
98556
  if (label2 === null)
98136
98557
  continue;
98137
- const route = routeOf5(start.url);
98558
+ const route = routeOf6(start.url);
98138
98559
  const key2 = `${route}|${normLine(label2)}`;
98139
98560
  if (reported.has(key2))
98140
98561
  continue;
@@ -98143,7 +98564,7 @@ function detectStuckJobs(capture2, opts = {}) {
98143
98564
  continue;
98144
98565
  const window2 = [];
98145
98566
  for (const s3 of capture2.steps.filter((x) => x.step > start.step)) {
98146
- if (routeOf5(s3.url) !== route)
98567
+ if (routeOf6(s3.url) !== route)
98147
98568
  break;
98148
98569
  const other = s3.op !== null && START_OPS.has(s3.op) && controlKey3(s3) !== controlKey3(start);
98149
98570
  if (other && capture2.requests.some((r) => r.step === s3.step && isWrite(r) && okStatus(r)))
@@ -98151,7 +98572,7 @@ function detectStuckJobs(capture2, opts = {}) {
98151
98572
  window2.push(s3);
98152
98573
  }
98153
98574
  const lastStep = window2.length > 0 ? window2[window2.length - 1].step : start.step;
98154
- const screens = capture2.screens.filter((s3) => s3.step > start.step && s3.step <= lastStep + 1 && routeOf5(s3.url) === route);
98575
+ const screens = capture2.screens.filter((s3) => s3.step > start.step && s3.step <= lastStep + 1 && routeOf6(s3.url) === route);
98155
98576
  if (screens.length === 0)
98156
98577
  continue;
98157
98578
  const reclicks = window2.filter((s3) => s3.op === start.op && s3.actOk && controlKey3(s3) === controlKey3(start));
@@ -98163,7 +98584,17 @@ function detectStuckJobs(capture2, opts = {}) {
98163
98584
  }
98164
98585
  const polls = [...polled.values()].sort((a, b2) => b2.length - a.length)[0] ?? [];
98165
98586
  const inProgress = reclicks.length > 0 || waits.length > 0 || busy.length > 0 || polls.length >= 3;
98587
+ const before = screenBefore(capture2, start.step);
98588
+ const was = new Set(before === void 0 ? [] : linesOf(before.visibleText).map(normLine));
98589
+ const labelLine = normLine(label2);
98590
+ const own2 = new Set([start.value, start.message, ...capture2.typedValues ?? []].filter((v) => v !== void 0 && v.trim().length > 0).map(normLine));
98166
98591
  const last = screens[screens.length - 1];
98592
+ const completed = before !== void 0 && !last.busy && !BUSY_TEXT2.test(last.visibleText) && screens.some((s3) => linesOf(s3.visibleText).some((line2) => {
98593
+ const n3 = normLine(line2);
98594
+ return n3.length >= 2 && n3 !== labelLine && !was.has(n3) && !own2.has(n3) && !BUSY_TEXT2.test(line2);
98595
+ }));
98596
+ if (completed)
98597
+ continue;
98167
98598
  const offered = reclicks.length > 0 || normLine(last.visibleText).includes(normLine(label2));
98168
98599
  const endedHere = !capture2.screens.some((s3) => s3.step > last.step);
98169
98600
  const startedAt = Math.min(...writes.map((w) => w.startedAt));
@@ -98342,7 +98773,7 @@ function detectFailedSubmits(capture2) {
98342
98773
  const after = screenAfter(capture2, s3.step);
98343
98774
  if (after === void 0)
98344
98775
  continue;
98345
- if (before !== void 0 && routeOf5(after.url) !== routeOf5(before.url))
98776
+ if (before !== void 0 && routeOf6(after.url) !== routeOf6(before.url))
98346
98777
  continue;
98347
98778
  const lines = linesOf(`${after.visibleText}
98348
98779
  ${s3.reply ?? ""}`);
@@ -98411,15 +98842,15 @@ function headingOf(s3) {
98411
98842
  function detectUrlMismatches(capture2) {
98412
98843
  const hits = /* @__PURE__ */ new Map();
98413
98844
  capture2.screens.forEach((b2, i) => {
98414
- const route = routeOf5(b2.url);
98845
+ const route = routeOf6(b2.url);
98415
98846
  const heading = headingOf(b2);
98416
98847
  if (heading.length < 3)
98417
98848
  return;
98418
98849
  const { words: words3, known } = routeWords2(route);
98419
98850
  if (words3.length === 0 || relates(heading, words3))
98420
98851
  return;
98421
- const other = capture2.screens.find((s3) => routeOf5(s3.url) !== route && normLine(headingOf(s3)) === normLine(heading) && relates(heading, routeWords2(routeOf5(s3.url)).words));
98422
- const prior = capture2.screens.slice(0, i).reverse().find((s3) => routeOf5(s3.url) === route && normLine(headingOf(s3)) !== normLine(heading));
98852
+ const other = capture2.screens.find((s3) => routeOf6(s3.url) !== route && normLine(headingOf(s3)) === normLine(heading) && relates(heading, routeWords2(routeOf6(s3.url)).words));
98853
+ const prior = capture2.screens.slice(0, i).reverse().find((s3) => routeOf6(s3.url) === route && normLine(headingOf(s3)) !== normLine(heading));
98423
98854
  const transitioned = known && prior !== void 0 && relates(headingOf(prior), words3);
98424
98855
  if (other === void 0 && !transitioned)
98425
98856
  return;
@@ -98430,7 +98861,7 @@ function detectUrlMismatches(capture2) {
98430
98861
  route,
98431
98862
  known,
98432
98863
  ...transitioned ? { was: headingOf(prior) } : {},
98433
- ...other === void 0 ? {} : { elsewhere: routeOf5(other.url) }
98864
+ ...other === void 0 ? {} : { elsewhere: routeOf6(other.url) }
98434
98865
  };
98435
98866
  g.screens.push(b2);
98436
98867
  hits.set(k, g);
@@ -98475,6 +98906,7 @@ function detectSignals(capture2, opts = {}) {
98475
98906
  ...withoutSubsumedDuplicateWrites(detectDuplicateWrites(capture2, opts), journey),
98476
98907
  ...detectInternalIds(capture2),
98477
98908
  ...detectInertControls(capture2),
98909
+ ...detectRepeatedAdvances(capture2),
98478
98910
  ...journey
98479
98911
  ];
98480
98912
  }
@@ -98506,6 +98938,7 @@ var SIGNAL_IMPACT = {
98506
98938
  "internal-id": "confused",
98507
98939
  "inert-control": "confused",
98508
98940
  "url-mismatch": "confused",
98941
+ "repeated-advance": "slowed",
98509
98942
  "horizontal-overflow": "confused",
98510
98943
  "vertical-clipping": "confused"
98511
98944
  };
@@ -98533,7 +98966,7 @@ function detectFriction(capture2, outcome) {
98533
98966
  };
98534
98967
  const routesAt = (nums) => {
98535
98968
  const set2 = new Set(nums);
98536
- return [.../* @__PURE__ */ new Set([...steps.filter((s3) => set2.has(s3.step)).map((s3) => routeOf5(s3.url)), ...capture2.screens.filter((s3) => set2.has(s3.step)).map((s3) => routeOf5(s3.url))])];
98969
+ return [.../* @__PURE__ */ new Set([...steps.filter((s3) => set2.has(s3.step)).map((s3) => routeOf6(s3.url)), ...capture2.screens.filter((s3) => set2.has(s3.step)).map((s3) => routeOf6(s3.url))])];
98537
98970
  };
98538
98971
  const add = (kind, impact, nums, detail) => {
98539
98972
  const sorted2 = [...new Set(nums)].filter((n3) => n3 > 0).sort((a, b2) => a - b2);
@@ -98624,7 +99057,7 @@ function detectFriction(capture2, outcome) {
98624
99057
  const blocking = reqs.some((r) => WRITE_METHODS3.has(r.method.toUpperCase()) && (r.failed === true || (r.status ?? 0) >= 500));
98625
99058
  add("error", blocking ? "blocked" : "slowed", [step], `${reqs.map((r) => `${r.endpoint} \u2192 ${r.failed === true ? "failed" : r.status}`).join(", ")} after step ${step}`);
98626
99059
  }
98627
- const seq = capture2.screens.map((s3) => ({ route: routeOf5(s3.url), step: s3.step }));
99060
+ const seq = capture2.screens.map((s3) => ({ route: routeOf6(s3.url), step: s3.step }));
98628
99061
  for (let x = 2; x < seq.length; x++) {
98629
99062
  const here = seq[x];
98630
99063
  if (seq[x - 1].route === here.route)
@@ -98651,7 +99084,7 @@ function detectFriction(capture2, outcome) {
98651
99084
  continue;
98652
99085
  if (between.some((r) => r.actOk && (r.op === "click" || r.op === "send" || r.op === "select")))
98653
99086
  continue;
98654
- add("abandoned", "confused", [s3.step, rest[leftAt].step], `a value typed at step ${s3.step} was left unsubmitted when the run moved to ${routeOf5(rest[leftAt].url)}`);
99087
+ add("abandoned", "confused", [s3.step, rest[leftAt].step], `a value typed at step ${s3.step} was left unsubmitted when the run moved to ${routeOf6(rest[leftAt].url)}`);
98655
99088
  }
98656
99089
  if (outcome?.status === "incomplete" && steps.length > 0) {
98657
99090
  const tail = steps.slice(-3).map((s3) => s3.step);
@@ -99229,8 +99662,8 @@ function boundTargetOptions(controls, point, steps, limit = MAX_CHOICE_OPTIONS -
99229
99662
  const acted = steps.filter((s3) => point.steps.includes(s3.step));
99230
99663
  const words3 = new Set(normalizeText(point.detail).split(" ").filter((w) => w.length >= 3));
99231
99664
  const score2 = (c) => (acted.some((s3) => stepNames(s3, c)) ? 400 : 0) + (normalizeText(c.name).split(" ").some((w) => words3.has(w)) ? 60 : 0) + (c.enabled ? 10 : 0);
99232
- const keep = new Set(controls.map((c, i) => ({ c, i, s: score2(c) })).sort((a, b2) => b2.s - a.s || a.i - b2.i).slice(0, limit).map((x) => x.c.index));
99233
- return controls.filter((c) => keep.has(c.index));
99665
+ const keep2 = new Set(controls.map((c, i) => ({ c, i, s: score2(c) })).sort((a, b2) => b2.s - a.s || a.i - b2.i).slice(0, limit).map((x) => x.c.index));
99666
+ return controls.filter((c) => keep2.has(c.index));
99234
99667
  }
99235
99668
  function frictionText(p) {
99236
99669
  return `${p.kind} (steps ${p.steps.join(", ")}): ${p.detail}`.slice(0, 600);
@@ -99259,7 +99692,7 @@ async function analyzeClaims(request, deps) {
99259
99692
  for (const raw of request.screens) {
99260
99693
  try {
99261
99694
  const red = redactEvidence(raw, secrets, deps.redactor);
99262
- screens.push({ raw, red, route: routeOf5(red.url) });
99695
+ screens.push({ raw, red, route: routeOf6(red.url) });
99263
99696
  } catch (cause) {
99264
99697
  return { kind: "failed", reason: `redaction failed: ${message(cause)}`, screenId: raw.screenId };
99265
99698
  }
@@ -99749,7 +100182,7 @@ ${f2.route}
99749
100182
  ${element}`, "utf8").digest("hex").slice(0, 16);
99750
100183
  }
99751
100184
  function contractRouteTemplate(urlOrRoute) {
99752
- return routeOf5(urlOrRoute).split("/").map((seg) => /^:[A-Za-z][A-Za-z0-9_]{0,63}$/.test(seg) ? `{${seg.slice(1)}}` : seg).join("/");
100185
+ return routeOf6(urlOrRoute).split("/").map((seg) => /^:[A-Za-z][A-Za-z0-9_]{0,63}$/.test(seg) ? `{${seg.slice(1)}}` : seg).join("/");
99753
100186
  }
99754
100187
 
99755
100188
  // ../ux/dist/rubric/schema.js
@@ -101091,7 +101524,7 @@ function planGuardProbes(screens, secrets, safety) {
101091
101524
  const refused = [];
101092
101525
  for (const screen of screens) {
101093
101526
  const red = redactEvidence(screen, secrets);
101094
- const route = routeOf5(red.url);
101527
+ const route = routeOf6(red.url);
101095
101528
  screen.controls.forEach((c, i) => {
101096
101529
  if (controlRisk(c.name, c.role)?.risk !== "destructive") return;
101097
101530
  const rc = red.controls[i];
@@ -101850,11 +102283,11 @@ var MAX_SCAN_DEPTH = 8;
101850
102283
  function scanRuns(dirs) {
101851
102284
  const seen = /* @__PURE__ */ new Set();
101852
102285
  const runs = [];
101853
- const walk = (dir, depth) => {
102286
+ const walk2 = (dir, depth) => {
101854
102287
  for (const entry of readdirSync8(dir, { withFileTypes: true }).sort((a, b2) => a.name.localeCompare(b2.name))) {
101855
102288
  const path2 = resolve13(dir, entry.name);
101856
102289
  if (entry.isDirectory()) {
101857
- if (depth < MAX_SCAN_DEPTH && !entry.name.startsWith(".") && entry.name !== "node_modules") walk(path2, depth + 1);
102290
+ if (depth < MAX_SCAN_DEPTH && !entry.name.startsWith(".") && entry.name !== "node_modules") walk2(path2, depth + 1);
101858
102291
  continue;
101859
102292
  }
101860
102293
  if (!entry.isFile() || !isResultName(entry.name) || seen.has(path2)) continue;
@@ -101866,7 +102299,7 @@ function scanRuns(dirs) {
101866
102299
  };
101867
102300
  for (const dir of dirs) {
101868
102301
  if (!existsSync23(dir) || !statSync9(dir).isDirectory()) continue;
101869
- walk(dir, 0);
102302
+ walk2(dir, 0);
101870
102303
  }
101871
102304
  return runs.sort((a, b2) => (a.startedAt ?? "").localeCompare(b2.startedAt ?? "") || a.runId.localeCompare(b2.runId));
101872
102305
  }
@@ -103328,6 +103761,29 @@ var REAL_RUNNERS = {
103328
103761
  usability: runUsabilityMission,
103329
103762
  verifyFix: runVerifyFix
103330
103763
  };
103764
+ async function addOutcomeTargets(items, executed, opts) {
103765
+ const catalogDir = opts.projectDir === void 0 ? findProjectDir() : opts.projectDir;
103766
+ if (catalogDir === null) return;
103767
+ const catalogs = /* @__PURE__ */ new Map();
103768
+ const catalogFor = (journeysDir) => {
103769
+ const dir = resolve16(journeysDir);
103770
+ let c = catalogs.get(dir);
103771
+ if (c === void 0) {
103772
+ c = loadCatalog(catalogDir, dir).catch(() => null);
103773
+ catalogs.set(dir, c);
103774
+ }
103775
+ return c;
103776
+ };
103777
+ for (const [idx, { item }] of executed.entries()) {
103778
+ const report2 = items[idx];
103779
+ if (report2 === void 0 || item.kind !== "journey" || report2.baseline === void 0) continue;
103780
+ const catalog = await catalogFor(item.t.target.journeysDir ?? opts.journeysDir);
103781
+ const jobId = catalog?.journeys.find((j) => j.id === item.name)?.job;
103782
+ const outcomes = catalog === null || jobId === void 0 ? void 0 : findJob(catalog, jobId)?.job.desiredOutcomes;
103783
+ if (outcomes === void 0 || outcomes.length === 0) continue;
103784
+ items[idx] = { ...report2, outcomeTargets: evaluateOutcomeTargets(outcomes, report2.baseline) };
103785
+ }
103786
+ }
103331
103787
  async function approvalDefects(opts, req) {
103332
103788
  const dirs = [...new Set([opts.journeysDir, ...opts.suite.targets.flatMap((t) => t.journeysDir === void 0 ? [] : [t.journeysDir])].map((d) => resolve16(d)))];
103333
103789
  const catalogs = [];
@@ -103553,6 +104009,7 @@ async function runCheck(opts) {
103553
104009
  ...ex.locatorHealth === void 0 ? {} : { locatorHealth: ex.locatorHealth }
103554
104010
  };
103555
104011
  });
104012
+ await addOutcomeTargets(itemReports, executed, opts);
103556
104013
  const details = executed.flatMap(
103557
104014
  ({ item, ex }) => item.kind === "journey" && ex?.locatorDetail !== void 0 ? [{ name: item.name, detail: ex.locatorDetail }] : []
103558
104015
  );
@@ -104321,8 +104778,8 @@ function bundleDemos(input2, journeys, warn) {
104321
104778
  if (demos.length > LIMITS.demos) {
104322
104779
  warn.push(`${demos.length} demos: only the first ${LIMITS.demos} are exported`);
104323
104780
  const kept = new Set(demos.slice(0, LIMITS.demos).map((d) => `media/${d.journey}/`));
104324
- const keep = (p) => [...kept].some((k) => p.startsWith(k));
104325
- return { demos: demos.slice(0, LIMITS.demos), files: files.filter((f2) => keep(f2.path)), media: media.filter((m) => keep(m.path)) };
104781
+ const keep2 = (p) => [...kept].some((k) => p.startsWith(k));
104782
+ return { demos: demos.slice(0, LIMITS.demos), files: files.filter((f2) => keep2(f2.path)), media: media.filter((m) => keep2(m.path)) };
104326
104783
  }
104327
104784
  return { demos, files, media };
104328
104785
  }
@@ -105018,7 +105475,7 @@ function findingDropWarning(fingerprint2, kind, issues) {
105018
105475
  function bundleFindings(src) {
105019
105476
  const warnings = [];
105020
105477
  const latest = /* @__PURE__ */ new Map();
105021
- const keep = (m) => {
105478
+ const keep2 = (m) => {
105022
105479
  if (m === null) return;
105023
105480
  if ("drop" in m) {
105024
105481
  warnings.push(`${m.drop} \u2014 left out of the bundle`);
@@ -105033,10 +105490,10 @@ function bundleFindings(src) {
105033
105490
  if (prev === void 0 || prev.at <= m.finding.at) latest.set(m.finding.fingerprint, m.finding);
105034
105491
  };
105035
105492
  const byPath = new Map(src.runs.map((r) => [r.path, r]));
105036
- for (const d of consolidate(src.runs)) keep(defectFinding(d, byPath));
105493
+ for (const d of consolidate(src.runs)) keep2(defectFinding(d, byPath));
105037
105494
  for (const { report: report2, run } of src.uxReports) {
105038
105495
  if (!isRecord(report2) || !Array.isArray(report2.findings)) continue;
105039
- for (const f2 of report2.findings) if (isRecord(f2)) keep(uxFinding(f2, run));
105496
+ for (const f2 of report2.findings) if (isRecord(f2)) keep2(uxFinding(f2, run));
105040
105497
  }
105041
105498
  let findings = [...latest.values()].sort((a, b2) => a.at.localeCompare(b2.at) || a.fingerprint.localeCompare(b2.fingerprint));
105042
105499
  if (findings.length > MAX_FINDINGS) {
@@ -116451,8 +116908,8 @@ function confineSweepFixture(raw, root2) {
116451
116908
  realRoot = realpathSync5(root2);
116452
116909
  } catch {
116453
116910
  }
116454
- const sensitive = fixtureSensitiveReason(real, realRoot) ?? fixtureSensitiveReason(absolute, resolve23(root2));
116455
- if (sensitive !== null) throw fixtureSensitive(absolute, root2, sensitive);
116911
+ const sensitive2 = fixtureSensitiveReason(real, realRoot) ?? fixtureSensitiveReason(absolute, resolve23(root2));
116912
+ if (sensitive2 !== null) throw fixtureSensitive(absolute, root2, sensitive2);
116456
116913
  return absolute;
116457
116914
  }
116458
116915
  function tsvTags(cell2, where, problems) {