@jevitate/cli 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/contracts/jevitate-catalog/v1/README.md +10 -0
  2. package/contracts/jevitate-catalog/v1/approval-hash-vectors.json +64 -0
  3. package/contracts/jevitate-catalog/v1/fixtures/invalid/jobs-unknown-field.json +12 -0
  4. package/contracts/jevitate-catalog/v1/fixtures/invalid/journey-unknown-field.json +15 -0
  5. package/contracts/jevitate-catalog/v1/fixtures/valid/jobs.json +11 -0
  6. package/contracts/jevitate-catalog/v1/fixtures/valid/journey.json +14 -0
  7. package/contracts/jevitate-catalog/v1/fixtures/valid/personas.json +7 -0
  8. package/contracts/jevitate-catalog/v1/manifest.json +69 -0
  9. package/contracts/jevitate-catalog/v1/schemas/job.schema.json +605 -0
  10. package/contracts/jevitate-catalog/v1/schemas/jobs.schema.json +28 -0
  11. package/contracts/jevitate-catalog/v1/schemas/journey.schema.json +10871 -0
  12. package/contracts/jevitate-catalog/v1/schemas/personas.schema.json +223 -0
  13. package/contracts/jevitate-catalog/v2/README.md +18 -0
  14. package/contracts/jevitate-catalog/v2/approval-hash-vectors.json +75 -0
  15. package/contracts/jevitate-catalog/v2/fixtures/invalid/jobs-unknown-field.json +12 -0
  16. package/contracts/jevitate-catalog/v2/fixtures/invalid/journey-unknown-field.json +15 -0
  17. package/contracts/jevitate-catalog/v2/fixtures/valid/jobs.json +11 -0
  18. package/contracts/jevitate-catalog/v2/fixtures/valid/journey.json +14 -0
  19. package/contracts/jevitate-catalog/v2/fixtures/valid/personas.json +7 -0
  20. package/contracts/jevitate-catalog/v2/manifest.json +78 -0
  21. package/contracts/jevitate-catalog/v2/schemas/job.schema.json +605 -0
  22. package/contracts/jevitate-catalog/v2/schemas/jobs.schema.json +28 -0
  23. package/contracts/jevitate-catalog/v2/schemas/journey.schema.json +10871 -0
  24. package/contracts/jevitate-catalog/v2/schemas/personas.schema.json +223 -0
  25. package/dist/bin.js +392 -147
  26. package/dist/build-info.generated.js +3 -3
  27. package/dist/catalog-bundle.d.ts +4 -2
  28. package/dist/catalog-bundle.js +8 -6
  29. package/dist/catalog-bundle.js.map +1 -1
  30. package/dist/catalog-contract-identity.d.ts +5 -0
  31. package/dist/catalog-contract-identity.js +7 -0
  32. package/dist/catalog-contract-identity.js.map +1 -0
  33. package/dist/catalog.js +18 -7
  34. package/dist/catalog.js.map +1 -1
  35. package/dist/journey-review.d.ts +3 -0
  36. package/dist/journey-review.js +13 -2
  37. package/dist/journey-review.js.map +1 -1
  38. package/dist/journey-stale-api.js +4 -4
  39. package/dist/journey-stale-api.js.map +1 -1
  40. package/dist/log-correlation.js +10 -2
  41. package/dist/log-correlation.js.map +1 -1
  42. package/package.json +9 -6
package/dist/bin.js CHANGED
@@ -10,16 +10,36 @@ import { mkdir, stat } from "node:fs/promises";
10
10
 
11
11
  // ../domain/dist/content-hash.js
12
12
  import { createHash } from "node:crypto";
13
- function canonical(value) {
13
+ function compareUtf16CodeUnits(a, b2) {
14
+ const length = Math.min(a.length, b2.length);
15
+ for (let index = 0; index < length; index += 1) {
16
+ const difference = a.charCodeAt(index) - b2.charCodeAt(index);
17
+ if (difference !== 0)
18
+ return difference < 0 ? -1 : 1;
19
+ }
20
+ return a.length === b2.length ? 0 : a.length < b2.length ? -1 : 1;
21
+ }
22
+ function canonicalJson(value) {
14
23
  if (value === null || typeof value !== "object")
15
24
  return JSON.stringify(value);
16
25
  if (Array.isArray(value))
17
- return `[${value.map(canonical).join(",")}]`;
26
+ return `[${value.map(canonicalJson).join(",")}]`;
27
+ const entries = Object.entries(value).sort(([a], [b2]) => compareUtf16CodeUnits(a, b2));
28
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
29
+ }
30
+ function legacyCanonicalJson(value) {
31
+ if (value === null || typeof value !== "object")
32
+ return JSON.stringify(value);
33
+ if (Array.isArray(value))
34
+ return `[${value.map(legacyCanonicalJson).join(",")}]`;
18
35
  const entries = Object.entries(value).sort(([a], [b2]) => a.localeCompare(b2));
19
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
36
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${legacyCanonicalJson(v)}`).join(",")}}`;
20
37
  }
21
38
  function contentHash(value) {
22
- return createHash("sha256").update(canonical(value)).digest("hex");
39
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
40
+ }
41
+ function legacyContentHash(value) {
42
+ return createHash("sha256").update(legacyCanonicalJson(value)).digest("hex");
23
43
  }
24
44
  function sha256(text2) {
25
45
  return createHash("sha256").update(text2, "utf8").digest("hex");
@@ -33945,18 +33965,18 @@ function stepIdsOf(rec) {
33945
33965
  ids.add(s3.stepId);
33946
33966
  return ids;
33947
33967
  }
33948
- function canonicalJson(v) {
33968
+ function canonicalJson2(v) {
33949
33969
  if (Array.isArray(v))
33950
- return `[${v.map(canonicalJson).join(",")}]`;
33970
+ return `[${v.map(canonicalJson2).join(",")}]`;
33951
33971
  if (v !== null && typeof v === "object") {
33952
33972
  const o = v;
33953
- return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
33973
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson2(o[k])}`).join(",")}}`;
33954
33974
  }
33955
33975
  return JSON.stringify(v) ?? "null";
33956
33976
  }
33957
33977
  function seededRandom(rec) {
33958
33978
  let h = 2166136261;
33959
- const text2 = canonicalJson(rec);
33979
+ const text2 = canonicalJson2(rec);
33960
33980
  for (let i = 0; i < text2.length; i++) {
33961
33981
  h ^= text2.charCodeAt(i);
33962
33982
  h = Math.imul(h, 16777619) >>> 0;
@@ -35260,9 +35280,9 @@ async function confineFixture(raw, root2) {
35260
35280
  }
35261
35281
  if (!isFile)
35262
35282
  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);
35283
+ const sensitive2 = fixtureSensitiveReason(real, realRoot) ?? (isInsideRoot(candidate, resolve3(root2)) ? fixtureSensitiveReason(candidate, resolve3(root2)) : null);
35284
+ if (sensitive2 !== null)
35285
+ throw fixtureSensitive(candidate, root2, sensitive2);
35266
35286
  return real;
35267
35287
  }
35268
35288
  function fixtureEscapesRoot(raw, root2) {
@@ -39592,6 +39612,59 @@ async function clockBounded(work, ms, onTimeout) {
39592
39612
  }
39593
39613
  }
39594
39614
 
39615
+ // ../explore/dist/request-body-values.js
39616
+ var MIN_VALUE_CHARS = 3;
39617
+ var MAX_VALUES = 50;
39618
+ var MAX_BODY_CHARS = 64e3;
39619
+ 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"]);
39620
+ function sensitive(key2) {
39621
+ return key2.split(/[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).some((w) => SENSITIVE_WORDS.has(w.toLowerCase()));
39622
+ }
39623
+ function keep(out, key2, value) {
39624
+ if (out.length >= MAX_VALUES)
39625
+ return;
39626
+ if (key2 !== null && sensitive(key2))
39627
+ return;
39628
+ const v = value.replace(/\s+/g, " ").trim();
39629
+ if (v.length >= MIN_VALUE_CHARS)
39630
+ out.push(v);
39631
+ }
39632
+ function walk(out, key2, node2) {
39633
+ if (out.length >= MAX_VALUES)
39634
+ return;
39635
+ if (typeof node2 === "string")
39636
+ keep(out, key2, node2);
39637
+ else if (Array.isArray(node2))
39638
+ for (const item of node2)
39639
+ walk(out, key2, item);
39640
+ else if (node2 !== null && typeof node2 === "object") {
39641
+ for (const [k, v] of Object.entries(node2)) {
39642
+ if (sensitive(k))
39643
+ continue;
39644
+ walk(out, k, v);
39645
+ }
39646
+ }
39647
+ }
39648
+ function bodyValues(body, contentType) {
39649
+ if (body === null || body === "" || body.length > MAX_BODY_CHARS)
39650
+ return [];
39651
+ const type = (contentType ?? "").toLowerCase();
39652
+ const out = [];
39653
+ if (type.includes("application/x-www-form-urlencoded")) {
39654
+ for (const [k, v] of new URLSearchParams(body))
39655
+ keep(out, k, v);
39656
+ return out;
39657
+ }
39658
+ if (type === "" || type.includes("json")) {
39659
+ try {
39660
+ walk(out, null, JSON.parse(body));
39661
+ } catch {
39662
+ return [];
39663
+ }
39664
+ }
39665
+ return out;
39666
+ }
39667
+
39595
39668
  // ../explore/dist/page-monitor.js
39596
39669
  var RPC_TRAILER_READ_MS = 2e3;
39597
39670
  var INTERACTIVE_SELECTOR2 = "a[href],button,input:not([type=hidden]),select,textarea,[role=button],[role=link],[role=textbox],[role=checkbox],[role=combobox],[contenteditable=true]";
@@ -39797,6 +39870,8 @@ function pathOf3(url2) {
39797
39870
  return url2.split(/[?#]/)[0] ?? url2;
39798
39871
  }
39799
39872
  }
39873
+ var BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
39874
+ var MAX_WRITE_BODIES = 200;
39800
39875
  function requestContentTypeOf(r) {
39801
39876
  try {
39802
39877
  const v = r.headers()["content-type"];
@@ -39809,6 +39884,8 @@ var RECENT_REQUESTS_MS = 6e4;
39809
39884
  var PageMonitor = class {
39810
39885
  #page;
39811
39886
  #inflight = /* @__PURE__ */ new Map();
39887
+ /** #649: the string values recent writes sent in their bodies (memory only; never persisted). */
39888
+ #writeBodies = [];
39812
39889
  #completed = [];
39813
39890
  #wakers = /* @__PURE__ */ new Set();
39814
39891
  #statuses = /* @__PURE__ */ new WeakMap();
@@ -39846,6 +39923,18 @@ var PageMonitor = class {
39846
39923
  startedAt,
39847
39924
  ...requestContentType2 === void 0 ? {} : { requestContentType: requestContentType2 }
39848
39925
  });
39926
+ if (BODY_METHODS.has(r.method().toUpperCase())) {
39927
+ let values2 = [];
39928
+ try {
39929
+ values2 = bodyValues(r.postData(), requestContentType2);
39930
+ } catch {
39931
+ }
39932
+ if (values2.length > 0) {
39933
+ this.#writeBodies.push({ startedAt, url: r.url(), values: values2 });
39934
+ if (this.#writeBodies.length > MAX_WRITE_BODIES)
39935
+ this.#writeBodies.shift();
39936
+ }
39937
+ }
39849
39938
  for (const c of this.#captures) {
39850
39939
  c.began(r, {
39851
39940
  method: r.method().toUpperCase(),
@@ -40097,6 +40186,10 @@ var PageMonitor = class {
40097
40186
  clock.clearTimeout(timer);
40098
40187
  }
40099
40188
  }
40189
+ /** #649: the string values the writes started since `sinceMs` sent, from URLs `keep` accepts. */
40190
+ writeBodyValuesSince(sinceMs, keep2) {
40191
+ return this.#writeBodies.filter((w) => w.startedAt >= sinceMs && keep2(w.url)).flatMap((w) => w.values);
40192
+ }
40100
40193
  /** Requests that ended since `sinceMs` (wall clock). */
40101
40194
  completedSince(sinceMs) {
40102
40195
  return this.#completed.filter((r) => r.endedAt >= sinceMs);
@@ -41129,8 +41222,8 @@ function boundCandidates(candidates, opts) {
41129
41222
  return s3;
41130
41223
  };
41131
41224
  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));
41132
- const keep = new Set(ranked.map((r) => r.i));
41133
- return { kept: candidates.filter((_, i) => keep.has(i)), omitted: candidates.length - keep.size };
41225
+ const keep2 = new Set(ranked.map((r) => r.i));
41226
+ return { kept: candidates.filter((_, i) => keep2.has(i)), omitted: candidates.length - keep2.size };
41134
41227
  }
41135
41228
 
41136
41229
  // ../explore/dist/stuck-actions.js
@@ -41415,7 +41508,8 @@ var SideEffectGuard = class {
41415
41508
  const own2 = requests.filter((r) => this.#ours(r));
41416
41509
  const settled = !inflightAny && own2.every((r) => r.status !== null && r.status >= 200 && r.status < 300);
41417
41510
  const completedOk = own2.length > 0 && settled;
41418
- this.#lastClick = { requestSent: requests.length > 0 || inflightAny, writes: [...done, ...pending], completedOk, settled };
41511
+ const sentValues = this.#monitor.writeBodyValuesSince(o.at, (url2) => this.#ours({ url: url2 }));
41512
+ this.#lastClick = { requestSent: requests.length > 0 || inflightAny, writes: [...done, ...pending], completedOk, settled, sentValues };
41419
41513
  if (done.length + pending.length === 0)
41420
41514
  return;
41421
41515
  this.#fired.set(o.key, {
@@ -41921,6 +42015,7 @@ var UnsubmittedTypeTracker = class {
41921
42015
  var GOAL_MET_THRESHOLD = 0.75;
41922
42016
  var GOAL_CHECK_TRIGGER = 0.5;
41923
42017
  var WRITES_DONE_NOTE = "the job's writes succeeded; the page shows no change (repeat data?)";
42018
+ var READ_BACK_DONE_NOTE = "the job's writes succeeded and a page loaded from the server shows what they sent";
41924
42019
  function groundDone(e) {
41925
42020
  if (e.unsubmitted.length > 0) {
41926
42021
  return { accept: false, reason: `typed text in ${e.unsubmitted.map((l2) => `"${l2}"`).join(", ")} was never submitted` };
@@ -41946,6 +42041,9 @@ function groundDone(e) {
41946
42041
  if (w !== void 0 && writesAllSucceeded(w) && w.goalIsWrites !== null && w.goalIsWrites >= GOAL_MET_THRESHOLD) {
41947
42042
  return { accept: true, outcome: { status: "completed", verifiedBy: "succeeded-writes", note: WRITES_DONE_NOTE } };
41948
42043
  }
42044
+ if (w !== void 0 && writesAllSucceeded(w) && e.readBack === true && p >= GOAL_CHECK_TRIGGER) {
42045
+ return { accept: true, outcome: { status: "completed", verifiedBy: "succeeded-writes", note: READ_BACK_DONE_NOTE } };
42046
+ }
41949
42047
  const notMet = `the goal is not observably achieved on this page (p=${p.toFixed(2)})`;
41950
42048
  if (w !== void 0 && writesAllSucceeded(w) && w.goalIsWrites !== null) {
41951
42049
  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)})` };
@@ -42094,6 +42192,7 @@ async function judgeGoalCompletion(judge, input2) {
42094
42192
  ...input2.signInFacts === void 0 ? [] : [`SIGN-IN (observed by code, trusted): ${input2.signInFacts}`],
42095
42193
  ...input2.saveFacts === void 0 ? [] : [`SAVE (observed by code, trusted): ${input2.saveFacts}`],
42096
42194
  ...input2.writeFacts === void 0 ? [] : [`WRITES (observed by code, trusted): ${input2.writeFacts}`],
42195
+ ...input2.readBackFacts === void 0 ? [] : [`READ-BACK (observed by code, trusted): ${input2.readBackFacts}`],
42097
42196
  ...input2.pageStatus === void 0 || input2.pageStatus === "" ? [] : [`PAGE STATUS (untrusted): ${input2.pageStatus}`],
42098
42197
  `VISIBLE PAGE TEXT (untrusted): ${input2.pageText.replace(/\s+/g, " ").slice(0, GOAL_TEXT_CHARS)}`,
42099
42198
  ...input2.fieldValues === void 0 || input2.fieldValues.length === 0 ? [] : [
@@ -45529,6 +45628,7 @@ var OBSERVED_PAGE_CHARS = 2e4;
45529
45628
  var MIN_QUOTE_CHARS = 3;
45530
45629
  var MAX_OBSERVED_FIELDS = 40;
45531
45630
  var OBSERVED_FIELD_CHARS = 500;
45631
+ var MAX_READ_BACK_VALUES = 50;
45532
45632
  var MAX_OBSERVED_CONTROLS = 120;
45533
45633
  var HEADING_CHARS = 200;
45534
45634
  function controlFields(controls) {
@@ -45583,6 +45683,56 @@ var ObservedPages = class {
45583
45683
  if (document2 !== void 0)
45584
45684
  this.#writeDocument = document2;
45585
45685
  }
45686
+ /**
45687
+ * #649: a sent value the run did not type (a write's request body: a prefilled import's rows) that
45688
+ * a page on the write's own route already showed before the write — its source, not a copy the app
45689
+ * kept. Only a page on another route reads it back: reloading the source shows it either way.
45690
+ */
45691
+ #sourceRoute = /* @__PURE__ */ new Map();
45692
+ /** #649: a write's body values the run did not type, decided on the next page seen (see `add`). */
45693
+ #pendingSent = null;
45694
+ /**
45695
+ * #649: a successful write the run fired sent `values` in its body, from a page on `url`. One the run
45696
+ * did not type awaits read-back like a typed one only when the next page shows it as NEW text — text
45697
+ * no earlier page on that route showed (an import's rows listed from its CSV; never a form's
45698
+ * untouched prefilled fields, still only in their fields). Decided when that page is added.
45699
+ */
45700
+ noteSentValues(values2, url2) {
45701
+ const own2 = /* @__PURE__ */ new Set([...this.#ownInputs, ...this.#unread]);
45702
+ const fresh = values2.map((value) => fold(redactContext(value, this.secrets).slice(0, OBSERVED_FIELD_CHARS))).filter((v) => v !== "" && !own2.has(v));
45703
+ this.#pendingSent = fresh.length === 0 ? null : { route: routeOf(url2), values: fresh };
45704
+ }
45705
+ /** #649: the pending body values `text` (the page after the write) newly shows await read-back. */
45706
+ #settleSent(text2) {
45707
+ const pending = this.#pendingSent;
45708
+ if (pending === null)
45709
+ return;
45710
+ this.#pendingSent = null;
45711
+ const now2 = fold(text2);
45712
+ const before = this.#pages.filter((p) => routeOf(p.url) === pending.route);
45713
+ for (const v of pending.values) {
45714
+ if (!now2.includes(v) || before.some((p) => fold(p.text).includes(v)))
45715
+ continue;
45716
+ this.#unread.add(v);
45717
+ if (before.some((p) => (p.fields ?? []).some((f2) => fold(f2.value).includes(v))))
45718
+ this.#sourceRoute.set(v, pending.route);
45719
+ }
45720
+ }
45721
+ /**
45722
+ * #671: values the run typed into a field on one document load. A field holding one shows the run's
45723
+ * own entry, never what the app kept: retyping a sent message into its composer after a reload is
45724
+ * not a read-back.
45725
+ */
45726
+ #typedOn = null;
45727
+ /** #671: the run typed `value` into a field (a message composer too) on document load `document`. */
45728
+ noteTypedOn(value, document2) {
45729
+ const v = fold(redactContext(value, this.secrets).slice(0, OBSERVED_FIELD_CHARS));
45730
+ if (document2 === void 0 || v === "")
45731
+ return;
45732
+ if (this.#typedOn?.document !== document2)
45733
+ this.#typedOn = { document: document2, values: /* @__PURE__ */ new Set() };
45734
+ this.#typedOn.values.add(v);
45735
+ }
45586
45736
  /** #647: the run's sent values (folded) that no page loaded from the server since has shown. */
45587
45737
  unreadWrites() {
45588
45738
  return this.#unread;
@@ -45596,15 +45746,37 @@ var ObservedPages = class {
45596
45746
  * #647: a page from a later document load than the write shows `shown` (its visible text, or field
45597
45747
  * values): the sent values it contains are read back from the app. Returns those values.
45598
45748
  */
45599
- readBack(document2, shown2) {
45749
+ readBack(document2, shown2, url2) {
45600
45750
  if (this.#unread.size === 0 || document2 === void 0 || document2 === this.#writeDocument)
45601
45751
  return [];
45602
45752
  const text2 = fold(shown2);
45603
- const back = [...this.#unread].filter((v) => text2.includes(v));
45604
- for (const v of back)
45753
+ const route = url2 === void 0 ? void 0 : routeOf(url2);
45754
+ const back = [...this.#unread].filter((v) => text2.includes(v) && (route === void 0 || this.#sourceRoute.get(v) !== route));
45755
+ for (const v of back) {
45605
45756
  this.#unread.delete(v);
45757
+ this.#sourceRoute.delete(v);
45758
+ }
45759
+ this.#readBackCount += back.length;
45760
+ for (const v of back)
45761
+ if (this.#readBackValues.length < MAX_READ_BACK_VALUES)
45762
+ this.#readBackValues.push(v);
45606
45763
  return back;
45607
45764
  }
45765
+ /** #645: how many sent values a page loaded from the server has shown again. */
45766
+ #readBackCount = 0;
45767
+ /** #667: the sent values (folded) read back so far, bounded. */
45768
+ #readBackValues = [];
45769
+ /** #667: the sent values (folded) a page loaded from the server has shown again. */
45770
+ readBackValues() {
45771
+ return this.#readBackValues;
45772
+ }
45773
+ /**
45774
+ * #645: the app was seen to keep what the run's writes sent — at least one value was read back and
45775
+ * none is still unread. Code evidence the page judgment lacks (see `groundDone`).
45776
+ */
45777
+ readBackConfirmed() {
45778
+ return this.#readBackCount > 0 && this.#unread.size === 0;
45779
+ }
45608
45780
  /** #239: the run's own typed, not-yet-saved values (folded). */
45609
45781
  ownInputs() {
45610
45782
  return this.#ownInputs;
@@ -45617,9 +45789,9 @@ var ObservedPages = class {
45617
45789
  const clip5 = (h) => redactContext((h ?? "").replace(/\s+/g, " ").trim(), this.secrets).slice(0, HEADING_CHARS);
45618
45790
  const heading = clip5(headings2.heading);
45619
45791
  const title = clip5(headings2.title);
45620
- 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 !== "");
45621
- const names = keep(headings2.controlNames);
45622
- const links = keep(headings2.contentLinks);
45792
+ 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 !== "");
45793
+ const names = keep2(headings2.controlNames);
45794
+ const links = keep2(headings2.contentLinks);
45623
45795
  const capped = (headings2.controlNames ?? []).length > MAX_OBSERVED_CONTROLS || (headings2.contentLinks ?? []).length > MAX_OBSERVED_CONTROLS;
45624
45796
  const incomplete = [headings2.inventoryIncomplete ?? "", capped ? `more than ${MAX_OBSERVED_CONTROLS} controls were kept` : ""].filter((x) => x !== "").join("; ");
45625
45797
  const status = headings2.status;
@@ -45639,7 +45811,10 @@ var ObservedPages = class {
45639
45811
  if (nav.length > 0)
45640
45812
  this.#topNav = nav;
45641
45813
  }
45642
- this.readBack(headings2.document, [page.text, ...kept.map((f2) => f2.value)].join("\n"));
45814
+ this.#settleSent(page.text);
45815
+ const typed = this.#typedOn !== null && this.#typedOn.document === headings2.document ? [...this.#typedOn.values] : [];
45816
+ const appFields = kept.map((f2) => f2.value).filter((value) => !typed.some((t) => fold(value).includes(t)));
45817
+ this.readBack(headings2.document, [page.text, ...appFields].join("\n"), url2);
45643
45818
  if (page.text.trim() === "" && kept.length === 0)
45644
45819
  return;
45645
45820
  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;
@@ -45655,6 +45830,13 @@ var ObservedPages = class {
45655
45830
  return [...this.#pages].reverse();
45656
45831
  }
45657
45832
  };
45833
+ function routeOf(url2) {
45834
+ try {
45835
+ return new URL(url2).pathname;
45836
+ } catch {
45837
+ return url2.split(/[?#]/)[0] ?? url2;
45838
+ }
45839
+ }
45658
45840
  function pathOf6(url2) {
45659
45841
  try {
45660
45842
  const u = new URL(url2);
@@ -46297,7 +46479,7 @@ var ABSENCE_GOAL = /\bwhether\b|\b(?:is|are) there\b|\bthere (?:is|are) (?:no|no
46297
46479
  function goalAdmitsAbsence(goal) {
46298
46480
  return ABSENCE_GOAL.test(goal);
46299
46481
  }
46300
- var WRITE_GOAL = /(?:^\s*|[.!?;:]\s+|\b(?:then|and|please)\s+)(?:record|save|create|add|invite|submit|send|post|publish|register|book|schedule)\b/i;
46482
+ var WRITE_GOAL = /(?:^\s*|[.!?;:]\s+|\b(?:then|and|please)\s+)(?:record|save|create|add|invite|submit|send|post|publish|register|book|schedule|import|upload|update|change|edit|rename|delete|remove)\b/i;
46301
46483
  function goalAsksToWrite(goal) {
46302
46484
  return WRITE_GOAL.test(goal);
46303
46485
  }
@@ -48141,6 +48323,7 @@ function newStep(ctx, input2) {
48141
48323
  if (proposedDone)
48142
48324
  writes = jobWritesOf(ctx);
48143
48325
  const writeFacts = writes !== void 0 && saved === null && writesAllSucceeded(writes) ? writes.facts : void 0;
48326
+ const readBackFacts = ctx.observed.readBackConfirmed() ? readBackFactsOf(ctx.observed.readBackValues(), pageText) : void 0;
48144
48327
  const judged = await judgeGoalCompletion(cfg.judge, {
48145
48328
  goal: cfg.goal,
48146
48329
  url: snap.url,
@@ -48151,6 +48334,7 @@ function newStep(ctx, input2) {
48151
48334
  ...signIn === null ? {} : { signInFacts: signIn.facts },
48152
48335
  ...saved === null ? {} : { saveFacts: saved.facts },
48153
48336
  ...writeFacts === void 0 ? {} : { writeFacts },
48337
+ ...readBackFacts === void 0 ? {} : { readBackFacts },
48154
48338
  fieldValues: fieldValuesOf(snap.controls, ctx.isBound)
48155
48339
  }).catch(() => ({ goalMet: null, goalIsSignIn: null, goalIsSave: null, goalIsWrites: null }));
48156
48340
  goalMet = judged.goalMet;
@@ -48165,7 +48349,8 @@ function newStep(ctx, input2) {
48165
48349
  ...goalMet === void 0 ? {} : { goalMetProbability: goalMet },
48166
48350
  ...signIn === null || goalMet === void 0 ? {} : { signIn: { completed: signIn.completed, goalIsSignIn } },
48167
48351
  ...saved === null || goalMet === void 0 ? {} : { save: { completed: saved.completed, goalIsSave } },
48168
- ...writes === void 0 || goalMet === void 0 ? {} : { writes: { ...writes, goalIsWrites } }
48352
+ ...writes === void 0 || goalMet === void 0 ? {} : { writes: { ...writes, goalIsWrites } },
48353
+ ...writes === void 0 ? {} : { readBack: ctx.observed.readBackConfirmed() }
48169
48354
  });
48170
48355
  const judgments = {};
48171
48356
  if (goalMet !== void 0 && goalMet !== null)
@@ -48180,6 +48365,11 @@ function newStep(ctx, input2) {
48180
48365
  };
48181
48366
  return { ...input2, record: record2, signIn, pendingNote, acceptedBy, groundGoal };
48182
48367
  }
48368
+ function readBackFactsOf(values2, pageText) {
48369
+ const text2 = pageText.toLowerCase();
48370
+ const shown2 = values2.filter((v) => text2.includes(v)).length;
48371
+ return `every value this run's writes sent (${values2.length}) was shown again on a page loaded from the server after the write \u2014 the app kept them; this page shows ${shown2} of them`;
48372
+ }
48183
48373
 
48184
48374
  // ../explore/dist/goal-loop/min-effort.js
48185
48375
  function deferEnding(ctx, step, ending) {
@@ -48218,7 +48408,7 @@ async function readBackProbe(ctx, step) {
48218
48408
  return [];
48219
48409
  const text2 = await readPageText(ctx.page, ctx.secrets).catch(() => "");
48220
48410
  const values2 = await ctx.page.evaluate(() => Array.from(document.querySelectorAll("input, textarea, select")).filter((e) => e.type !== "password").map((e) => e.value ?? "")).catch(() => []);
48221
- const back = ctx.observed.readBack(await documentOf(ctx.page), redactPageText([text2, ...values2].join("\n"), ctx.secrets));
48411
+ const back = ctx.observed.readBack(await documentOf(ctx.page), redactPageText([text2, ...values2].join("\n"), ctx.secrets), ctx.page.url());
48222
48412
  const listed2 = (vs) => vs.map((v) => `"${v}"`).join(", ");
48223
48413
  const missing = unread.filter((v) => !back.includes(v));
48224
48414
  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)})`);
@@ -48309,6 +48499,7 @@ async function handleReport(ctx, step) {
48309
48499
  }
48310
48500
 
48311
48501
  // ../explore/dist/goal-loop/handle-done.js
48502
+ var NO_WRITE_YET = "no write of this run has succeeded yet \u2014 the goal asks for a change the app must receive: go back and press the control that submits it (Import, Save, Send\u2026)";
48312
48503
  async function unreadWriteHold(ctx, step, outcome) {
48313
48504
  if (outcome.status === "completed" && outcome.verifiedBy === "success-condition")
48314
48505
  return null;
@@ -48400,12 +48591,13 @@ async function handleDone(ctx, step) {
48400
48591
  }
48401
48592
  }
48402
48593
  ctx.doneRejections += 1;
48403
- ctx.history.push(`done rejected: ${verdict.reason} \u2014 keep working toward the goal`);
48404
- record2(false, `done rejected (${ctx.doneRejections}/${MAX_DONE_REJECTIONS}): ${verdict.reason}`, {
48594
+ const why = ctx.writeGoal && !ctx.wroteOk ? `${verdict.reason}; ${NO_WRITE_YET}` : verdict.reason;
48595
+ ctx.history.push(`done rejected: ${why} \u2014 keep working toward the goal`);
48596
+ record2(false, `done rejected (${ctx.doneRejections}/${MAX_DONE_REJECTIONS}): ${why}`, {
48405
48597
  ...judgments === void 0 ? {} : { judgments }
48406
48598
  });
48407
48599
  if (ctx.doneRejections >= MAX_DONE_REJECTIONS) {
48408
- ctx.incomplete = `the model proposed done ${ctx.doneRejections} times, but ${verdict.reason}`;
48600
+ ctx.incomplete = `the model proposed done ${ctx.doneRejections} times, but ${why}`;
48409
48601
  ctx.endedOnRejectedDone = true;
48410
48602
  ctx.stop = "done";
48411
48603
  return "stop";
@@ -48787,6 +48979,7 @@ async function handleCodeTypedField(ctx, step) {
48787
48979
  ctx.valueLog.typed(control.name || control.summary, value);
48788
48980
  ctx.save.noteTyped(control.name || control.summary, value);
48789
48981
  ctx.observed.noteOwnInput(value);
48982
+ ctx.observed.noteTypedOn(value, await documentOf(ctx.page));
48790
48983
  ctx.noteMutation(`type ${control.name}`, control.descriptor, snap.signature, at3, { field: keyOf(control), value });
48791
48984
  ctx.tracker.countAction();
48792
48985
  ctx.cleared(control);
@@ -49349,6 +49542,7 @@ async function handleFill(ctx, step) {
49349
49542
  ctx.typeProbe = { key: keyOf(control), label: quote(control.name || control.summary, 60), at: typedAt, background: typedBackground, state: typedState };
49350
49543
  ctx.recorder.fill(control.descriptor, text2, at3);
49351
49544
  ctx.valueLog.typed(control.name || control.summary, text2);
49545
+ ctx.observed.noteTypedOn(text2, await documentOf(ctx.page));
49352
49546
  if (!ctx.isBound(control) && !isCredentialField(control) && !sendable(control)) {
49353
49547
  ctx.save.noteTyped(control.name || control.summary, text2);
49354
49548
  ctx.observed.noteOwnInput(text2);
@@ -49396,6 +49590,7 @@ async function perceiveStep(ctx) {
49396
49590
  if (lc !== null && lc !== ctx.settledClick) {
49397
49591
  ctx.settledClick = lc;
49398
49592
  if (lc.writes.length > 0 && lc.writes.every((w) => w.status !== null && w.status >= 200 && w.status < 300)) {
49593
+ ctx.observed.noteSentValues(lc.sentValues, ctx.page.url());
49399
49594
  ctx.observed.confirmOwnInputs(await documentOf(ctx.page));
49400
49595
  ctx.wroteOk = true;
49401
49596
  }
@@ -49947,6 +50142,7 @@ async function viewPage(ctx, step) {
49947
50142
  } catch {
49948
50143
  }
49949
50144
  const document2 = await documentOf(ctx.page);
50145
+ const unreadBefore = [...ctx.observed.unreadWrites()];
49950
50146
  ctx.observed.add(snap.url, visibleText, [...controlFields(snap.controls), ...richFields], {
49951
50147
  ...await readPageHeadings(ctx.page, ctx.secrets),
49952
50148
  // #647: which document load this is — a later one that shows a sent value reads it back.
@@ -49963,6 +50159,13 @@ async function viewPage(ctx, step) {
49963
50159
  navLinks: snap.controls.filter((c) => c.role === "link" && (c.landmark === "navigation" || c.landmark === "banner")).flatMap((c) => typeof c.href === "string" && c.href !== "" ? [c.href] : []),
49964
50160
  ...ctx.documentStatus.has(ctx.docKey(ctx.page.url())) ? { status: ctx.documentStatus.get(ctx.docKey(ctx.page.url())) } : {}
49965
50161
  });
50162
+ {
50163
+ const now2 = ctx.observed.unreadWrites();
50164
+ const back = unreadBefore.filter((v) => !now2.has(v));
50165
+ if (back.length > 0) {
50166
+ 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`);
50167
+ }
50168
+ }
49966
50169
  ctx.noteReplyText(snap.url, visibleText);
49967
50170
  ctx.depth.noteState(snap.signature, snap.url);
49968
50171
  {
@@ -52889,14 +53092,14 @@ function ownAssertion(s3) {
52889
53092
  return { field: "resume", assertion: s3.resume };
52890
53093
  return "expect" in s3 ? { field: "expect", assertion: s3.expect } : null;
52891
53094
  }
52892
- function canonical2(d) {
53095
+ function canonical(d) {
52893
53096
  const sort = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b2]) => a.localeCompare(b2)).map(([k, x]) => [k, sort(x)])) : v;
52894
53097
  return JSON.stringify(sort(d));
52895
53098
  }
52896
53099
  function isOwnTargetVisible(step) {
52897
53100
  if (!("expect" in step) || !("target" in step))
52898
53101
  return false;
52899
- return step.expect.kind === "visible" && canonical2(step.expect.target) === canonical2(step.target);
53102
+ return step.expect.kind === "visible" && canonical(step.expect.target) === canonical(step.target);
52900
53103
  }
52901
53104
  function isNoClaimExpect(step) {
52902
53105
  if (!("expect" in step))
@@ -53257,13 +53460,13 @@ function lintUploadFixtures(journey, baseDir) {
53257
53460
  });
53258
53461
  continue;
53259
53462
  }
53260
- const sensitive = fixtureSensitiveReason(real, realRootOf(baseDir)) ?? fixtureSensitiveReason(candidate, resolve5(baseDir));
53261
- if (sensitive !== null) {
53463
+ const sensitive2 = fixtureSensitiveReason(real, realRootOf(baseDir)) ?? fixtureSensitiveReason(candidate, resolve5(baseDir));
53464
+ if (sensitive2 !== null) {
53262
53465
  findings.push({
53263
53466
  rule: "upload-sensitive-file",
53264
53467
  level: "error",
53265
53468
  step: i + 1,
53266
- message: `step ${i + 1}: upload fixture ${candidate} looks sensitive (${sensitive})`,
53469
+ message: `step ${i + 1}: upload fixture ${candidate} looks sensitive (${sensitive2})`,
53267
53470
  fix: "use a dedicated test fixture file"
53268
53471
  });
53269
53472
  continue;
@@ -53388,7 +53591,7 @@ function lintJourney(journey, opts = {}) {
53388
53591
  const refsBound = (a) => assertionRefsBound(journey, a);
53389
53592
  const effects = sites.filter((site) => isEffectSite(site, site.where === "step" ? stepOf(site.step) : void 0, refsBound));
53390
53593
  const endEffects = effects.filter((site) => site.where === "end-state");
53391
- const effectAt = (n3) => effects.some((site) => (site.where === "step" || site.where === "step-request") && site.step === n3);
53594
+ const effectAt = (n3) => recorded[n3 - 1]?.step.kind === "waitFor" || effects.some((site) => (site.where === "step" || site.where === "step-request") && site.step === n3);
53392
53595
  const writeRequests = recorded.map((r) => writeRequestsOf(r, classify2));
53393
53596
  const isWrite2 = recorded.map((r, i) => isWriteStep(r, classify2, writeRequests[i]));
53394
53597
  const lastWrite = isWrite2.lastIndexOf(true) + 1;
@@ -53591,9 +53794,9 @@ function splitBudget(total, stops) {
53591
53794
  var CLAIM_KEYS = /* @__PURE__ */ new Set(["expect", "check", "resume", "waitFor"]);
53592
53795
  function actionRefs(step) {
53593
53796
  const out = /* @__PURE__ */ new Set();
53594
- const walk = (v) => {
53797
+ const walk2 = (v) => {
53595
53798
  if (Array.isArray(v)) {
53596
- v.forEach(walk);
53799
+ v.forEach(walk2);
53597
53800
  return;
53598
53801
  }
53599
53802
  if (v === null || typeof v !== "object")
@@ -53608,9 +53811,9 @@ function actionRefs(step) {
53608
53811
  out.add(n3);
53609
53812
  for (const [k, x] of Object.entries(o))
53610
53813
  if (!CLAIM_KEYS.has(k))
53611
- walk(x);
53814
+ walk2(x);
53612
53815
  };
53613
- walk(step);
53816
+ walk2(step);
53614
53817
  return [...out];
53615
53818
  }
53616
53819
  function sitePageAssertion(site) {
@@ -54786,10 +54989,10 @@ function bindParams(inputs, record2, request) {
54786
54989
  return params;
54787
54990
  }
54788
54991
  function recordFingerprint(outputs, record2) {
54789
- const canonical7 = {};
54992
+ const canonical6 = {};
54790
54993
  for (const name of Object.keys(outputs.fields).sort())
54791
- canonical7[name] = record2[name] ?? null;
54792
- return `sha256:${createHash5("sha256").update(JSON.stringify(canonical7)).digest("hex")}`;
54994
+ canonical6[name] = record2[name] ?? null;
54995
+ return `sha256:${createHash5("sha256").update(JSON.stringify(canonical6)).digest("hex")}`;
54793
54996
  }
54794
54997
  function extractedNames(journey) {
54795
54998
  const names = /* @__PURE__ */ new Set();
@@ -57407,7 +57610,7 @@ function seedPath(seedUrl) {
57407
57610
  }
57408
57611
 
57409
57612
  // ../explore/dist/adversarial/scope.js
57410
- function routeOf(url2) {
57613
+ function routeOf2(url2) {
57411
57614
  const path2 = new URL(url2).pathname;
57412
57615
  return path2.length > 1 && path2.endsWith("/") ? path2.slice(0, -1) : path2;
57413
57616
  }
@@ -57427,7 +57630,7 @@ function startRouteGlobs(startUrl) {
57427
57630
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
57428
57631
  throw new ScopeUnderivableError(`a ${parsed.protocol} URL has no route`);
57429
57632
  }
57430
- const route = routeOf(startUrl);
57633
+ const route = routeOf2(startUrl);
57431
57634
  return route === "/" ? ["/", "/**"] : [route, `${route}/`, `${route}/**`];
57432
57635
  }
57433
57636
  function scopeGlobs(startUrl, extra = []) {
@@ -59559,7 +59762,7 @@ var CoverageTracker = class {
59559
59762
  if (this.#counts(c))
59560
59763
  this.#controls.add(key2);
59561
59764
  }
59562
- const route = routeOf(snapshot2.url);
59765
+ const route = routeOf2(snapshot2.url);
59563
59766
  const formControls = this.#region === void 0 ? snapshot2.controls : snapshot2.controls.filter((c) => c.inRegion === true);
59564
59767
  for (const f2 of detectForms(formControls, this.#inScope))
59565
59768
  this.#forms.add(`${route}|${f2.key}`);
@@ -59592,7 +59795,7 @@ var CoverageTracker = class {
59592
59795
  submitted(url2, formKey) {
59593
59796
  if (!this.#inScope(url2))
59594
59797
  return;
59595
- this.#submitted.add(`${routeOf(url2)}|${formKey}`);
59798
+ this.#submitted.add(`${routeOf2(url2)}|${formKey}`);
59596
59799
  }
59597
59800
  /**
59598
59801
  * A submit of `formKey` (on page `url`) never reached the server, so it never counts toward
@@ -59603,7 +59806,7 @@ var CoverageTracker = class {
59603
59806
  blocked(url2, formKey, message2, why = "validation") {
59604
59807
  if (!this.#inScope(url2))
59605
59808
  return;
59606
- const form = `${routeOf(url2)}|${formKey}`;
59809
+ const form = `${routeOf2(url2)}|${formKey}`;
59607
59810
  const key2 = `${form}|${why}`;
59608
59811
  const cur = this.#blocked.get(key2);
59609
59812
  this.#blocked.set(key2, { form, why, count: (cur?.count ?? 0) + 1, message: cur?.message ?? message2 });
@@ -60407,6 +60610,16 @@ function submitRequestSent(monitor, sinceMs) {
60407
60610
  return true;
60408
60611
  return false;
60409
60612
  }
60613
+ var SUBMIT_REQUEST_POLL_MS = 50;
60614
+ async function submitRequestSentWithin(monitor, sinceMs, boundMs) {
60615
+ const deadline = clock.now() + boundMs;
60616
+ while (!submitRequestSent(monitor, sinceMs)) {
60617
+ if (clock.now() >= deadline)
60618
+ return false;
60619
+ await clock.sleep(SUBMIT_REQUEST_POLL_MS);
60620
+ }
60621
+ return true;
60622
+ }
60410
60623
  async function nativeValidationMessage(page) {
60411
60624
  return page.evaluate(() => {
60412
60625
  const fields = Array.from(document.querySelectorAll("input,select,textarea"));
@@ -61626,6 +61839,7 @@ async function refuseMisuseStep(ctx, ep, turn, s3) {
61626
61839
  }
61627
61840
 
61628
61841
  // ../explore/dist/missions/adversarial-hunt/episode.js
61842
+ var SUBMIT_REQUEST_GRACE_MS = 1e3;
61629
61843
  async function runEpisode(ctx, overlay, turn) {
61630
61844
  const ep = {};
61631
61845
  const { ran, planning, episode } = turn;
@@ -61708,10 +61922,12 @@ async function runEpisode(ctx, overlay, turn) {
61708
61922
  if (result.ok)
61709
61923
  ctx.cov.acted(ep.stepSnap.url, s3.control);
61710
61924
  if (result.ok && s3.submitsForm !== void 0) {
61711
- if (submitRequestSent(monitorFor(ctx.sessions.page), at3)) {
61925
+ const monitor = monitorFor(ctx.sessions.page);
61926
+ const invalid = submitRequestSent(monitor, at3) ? void 0 : await nativeValidationMessage(ctx.sessions.page);
61927
+ if (invalid === void 0 && await submitRequestSentWithin(monitor, at3, SUBMIT_REQUEST_GRACE_MS)) {
61712
61928
  ctx.cov.submitted(ep.stepSnap.url, s3.submitsForm);
61713
61929
  } else {
61714
- ctx.cov.blocked(ep.stepSnap.url, s3.submitsForm, await nativeValidationMessage(ctx.sessions.page));
61930
+ ctx.cov.blocked(ep.stepSnap.url, s3.submitsForm, invalid);
61715
61931
  }
61716
61932
  }
61717
61933
  if (ran === "visit-route" && s3.control !== null)
@@ -62902,9 +63118,9 @@ function hostStarvedFailure(failure3, health) {
62902
63118
  }
62903
63119
 
62904
63120
  // src/build-info.generated.ts
62905
- var GENERATED_COMMIT = "58694b9";
62906
- var GENERATED_TREE = "3e51f17";
62907
- var GENERATED_BUILT_AT = "2026-10-23T15:47:25.272Z";
63121
+ var GENERATED_COMMIT = "e0f6f96";
63122
+ var GENERATED_TREE = "d0b472e";
63123
+ var GENERATED_BUILT_AT = "2026-10-24T16:37:57.792Z";
62908
63124
 
62909
63125
  // src/version.ts
62910
63126
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -62926,13 +63142,13 @@ function readCliVersion() {
62926
63142
 
62927
63143
  // src/engine.ts
62928
63144
  function bundledCommit() {
62929
- return true ? "58694b9" : void 0;
63145
+ return true ? "e0f6f96" : void 0;
62930
63146
  }
62931
63147
  function bundledBuiltAt() {
62932
- return true ? "2026-10-23T15:47:25.272Z" : void 0;
63148
+ return true ? "2026-10-24T16:37:57.792Z" : void 0;
62933
63149
  }
62934
63150
  function bundledTree() {
62935
- return true ? "3e51f17" : void 0;
63151
+ return true ? "d0b472e" : void 0;
62936
63152
  }
62937
63153
  function currentBuildTree() {
62938
63154
  return bundledTree() ?? GENERATED_TREE;
@@ -63575,12 +63791,12 @@ function substituteDeep(v, sub) {
63575
63791
  return v;
63576
63792
  }
63577
63793
  function fixtureSpecHash(spec, hooks) {
63578
- return sha256(canonical3({ spec: spec ?? null, hooks: hooks ?? null })).slice(0, 16);
63794
+ return sha256(canonical2({ spec: spec ?? null, hooks: hooks ?? null })).slice(0, 16);
63579
63795
  }
63580
- function canonical3(v) {
63581
- if (Array.isArray(v)) return `[${v.map(canonical3).join(",")}]`;
63796
+ function canonical2(v) {
63797
+ if (Array.isArray(v)) return `[${v.map(canonical2).join(",")}]`;
63582
63798
  if (isRecord(v)) {
63583
- return `{${Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonical3(v[k])}`).join(",")}}`;
63799
+ return `{${Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonical2(v[k])}`).join(",")}}`;
63584
63800
  }
63585
63801
  return JSON.stringify(v);
63586
63802
  }
@@ -63702,7 +63918,7 @@ var MissionFixtures = class {
63702
63918
  return redactUrl(redactText(text2, [...this.#opts.secrets ?? [], ...bound2, ...this.secrets()]));
63703
63919
  }
63704
63920
  identity() {
63705
- return `fx-${sha256(canonical3({ spec: this.#specHash, outputs: this.publicOutputs(), secret: [...this.#secretNames].sort() })).slice(0, 16)}`;
63921
+ return `fx-${sha256(canonical2({ spec: this.#specHash, outputs: this.publicOutputs(), secret: [...this.#secretNames].sort() })).slice(0, 16)}`;
63706
63922
  }
63707
63923
  record() {
63708
63924
  return {
@@ -78800,14 +79016,14 @@ var GitSourceManager = class {
78800
79016
 
78801
79017
  // ../sources/dist/hash.js
78802
79018
  import { createHash as createHash9 } from "node:crypto";
78803
- function canonicalJson2(value) {
79019
+ function canonicalJson3(value) {
78804
79020
  if (value === null)
78805
79021
  return "null";
78806
79022
  if (Array.isArray(value))
78807
- return `[${value.map(canonicalJson2).join(",")}]`;
79023
+ return `[${value.map(canonicalJson3).join(",")}]`;
78808
79024
  if (typeof value === "object") {
78809
79025
  const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b2]) => a < b2 ? -1 : a > b2 ? 1 : 0);
78810
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson2(v)}`).join(",")}}`;
79026
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson3(v)}`).join(",")}}`;
78811
79027
  }
78812
79028
  if (typeof value === "number") {
78813
79029
  if (!Number.isFinite(value))
@@ -78818,7 +79034,7 @@ function canonicalJson2(value) {
78818
79034
  }
78819
79035
  function canonicalJourneyHash(file2) {
78820
79036
  const validated = SharedJourneyFileSchema.parse(file2);
78821
- const hex3 = createHash9("sha256").update(canonicalJson2(validated), "utf8").digest("hex");
79037
+ const hex3 = createHash9("sha256").update(canonicalJson3(validated), "utf8").digest("hex");
78822
79038
  return `sha256:${hex3}`;
78823
79039
  }
78824
79040
 
@@ -80524,6 +80740,15 @@ function journeyReviewHash(journey) {
80524
80740
  for (const k of BOOKKEEPING) delete metadata[k];
80525
80741
  return contentHash({ metadata, recording: journey.recording });
80526
80742
  }
80743
+ function journeyLegacyReviewHash(journey) {
80744
+ const metadata = { ...journey.metadata };
80745
+ for (const k of BOOKKEEPING) delete metadata[k];
80746
+ return legacyContentHash({ metadata, recording: journey.recording });
80747
+ }
80748
+ function journeyApprovalMatches(journey) {
80749
+ const approved = journey.metadata.approval?.contentHash;
80750
+ return approved !== void 0 && (approved === journeyReviewHash(journey) || approved === journeyLegacyReviewHash(journey));
80751
+ }
80527
80752
  function targetOf3(step) {
80528
80753
  if (step.kind === "forEach") return step.items;
80529
80754
  return "target" in step ? step.target : void 0;
@@ -80730,7 +80955,7 @@ function buildChange(journey, hash3, snapshot2, stepIdOnly) {
80730
80955
  const idOnly = stepIdOnly ? { stepIdOnly: true } : {};
80731
80956
  if (snapshot2 === null || snapshot2 === void 0) {
80732
80957
  if (approval !== void 0) {
80733
- return { kind: "snapshot-missing", approvedHash: approval.contentHash, approvedAt: approval.at, changed: approval.contentHash !== hash3, ...idOnly };
80958
+ return { kind: "snapshot-missing", approvedHash: approval.contentHash, approvedAt: approval.at, changed: !journeyApprovalMatches(journey), ...idOnly };
80734
80959
  }
80735
80960
  return journey.metadata.promoted ? { kind: "no-record" } : { kind: "first-approval" };
80736
80961
  }
@@ -81084,9 +81309,9 @@ var UnknownCatalogItemError = class extends Error {
81084
81309
  return this.kind === "persona" ? "E_UNKNOWN_PERSONA" : "E_UNKNOWN_JOB";
81085
81310
  }
81086
81311
  };
81087
- function statusOf3(approval, hash3) {
81312
+ function statusOf3(approval, hash3, legacyHash) {
81088
81313
  if (approval === void 0) return "draft";
81089
- return approval.contentHash === hash3 ? "approved" : "stale";
81314
+ return approval.contentHash === hash3 || approval.contentHash === legacyHash ? "approved" : "stale";
81090
81315
  }
81091
81316
  async function readJsonFile(path2) {
81092
81317
  const raw = await readFile8(path2, "utf8");
@@ -81151,7 +81376,7 @@ function catalogPersona(p, path2) {
81151
81376
  hasLogin: p.fields.login !== void 0,
81152
81377
  ...approval === void 0 ? {} : { approval },
81153
81378
  contentHash: hash3,
81154
- status: statusOf3(approval, hash3)
81379
+ status: statusOf3(approval, hash3, personaLegacyContentHash(p.id, p.fields))
81155
81380
  };
81156
81381
  }
81157
81382
  function personaContentHash(id, fields) {
@@ -81159,6 +81384,11 @@ function personaContentHash(id, fields) {
81159
81384
  for (const [k, v] of Object.entries(fields)) if (k !== "approval" && !SESSION_KEYS.has(k)) described[k] = v;
81160
81385
  return contentHash(described);
81161
81386
  }
81387
+ function personaLegacyContentHash(id, fields) {
81388
+ const described = { id };
81389
+ for (const [k, v] of Object.entries(fields)) if (k !== "approval" && !SESSION_KEYS.has(k)) described[k] = v;
81390
+ return legacyContentHash(described);
81391
+ }
81162
81392
  function jobList(raw, path2) {
81163
81393
  if (Array.isArray(raw)) return raw;
81164
81394
  if (isRecord(raw) && Array.isArray(raw.jobs)) return raw.jobs;
@@ -81181,13 +81411,17 @@ function catalogJobOf(job) {
81181
81411
  personas: [.../* @__PURE__ */ new Set([...job.personas ?? [], ...job.persona === void 0 ? [] : [job.persona]])],
81182
81412
  ...job.approval === void 0 ? {} : { approval: job.approval },
81183
81413
  contentHash: hash3,
81184
- status: statusOf3(job.approval, hash3)
81414
+ status: statusOf3(job.approval, hash3, jobLegacyContentHash(job))
81185
81415
  };
81186
81416
  }
81187
81417
  function jobContentHash(job) {
81188
81418
  const { approval: _a3, ...rest } = job;
81189
81419
  return contentHash(rest);
81190
81420
  }
81421
+ function jobLegacyContentHash(job) {
81422
+ const { approval: _a3, ...rest } = job;
81423
+ return legacyContentHash(rest);
81424
+ }
81191
81425
  function catalogJourney(journey) {
81192
81426
  const m = journey.metadata;
81193
81427
  const hash3 = journeyReviewHash(journey);
@@ -81200,7 +81434,7 @@ function catalogJourney(journey) {
81200
81434
  ...m.persona === void 0 ? {} : { personaText: m.persona },
81201
81435
  ...m.approval === void 0 ? {} : { approval: m.approval },
81202
81436
  contentHash: hash3,
81203
- changedSinceApproval: m.promoted && m.approval !== void 0 && m.approval.contentHash !== hash3
81437
+ changedSinceApproval: m.promoted && m.approval !== void 0 && !journeyApprovalMatches(journey)
81204
81438
  };
81205
81439
  }
81206
81440
  var CatalogLoader = class {
@@ -82703,12 +82937,12 @@ function healFloor(recorded, riskOf) {
82703
82937
  }
82704
82938
  return { floor: "healable", reason: "" };
82705
82939
  }
82706
- function canonical4(v) {
82940
+ function canonical3(v) {
82707
82941
  if (Array.isArray(v))
82708
- return `[${v.map(canonical4).join(",")}]`;
82942
+ return `[${v.map(canonical3).join(",")}]`;
82709
82943
  if (v !== null && typeof v === "object") {
82710
82944
  const o = v;
82711
- return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonical4(o[k])}`).join(",")}}`;
82945
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonical3(o[k])}`).join(",")}}`;
82712
82946
  }
82713
82947
  return JSON.stringify(v) ?? "undefined";
82714
82948
  }
@@ -82730,7 +82964,7 @@ function assertProofUntouched(before, after) {
82730
82964
  for (const k of /* @__PURE__ */ new Set([...Object.keys(b2), ...Object.keys(a)])) {
82731
82965
  if (free.has(k))
82732
82966
  continue;
82733
- if (canonical4(b2[k]) !== canonical4(a[k]))
82967
+ if (canonical3(b2[k]) !== canonical3(a[k]))
82734
82968
  return { code: "proof-field-changed", detail: `the candidate changes the step's \`${k}\`` };
82735
82969
  }
82736
82970
  return null;
@@ -82741,15 +82975,15 @@ function assertRecordedProofUntouched(before, after) {
82741
82975
  for (const k of /* @__PURE__ */ new Set([...Object.keys(b2), ...Object.keys(a)])) {
82742
82976
  if (k === "step")
82743
82977
  continue;
82744
- if (canonical4(b2[k]) !== canonical4(a[k]))
82978
+ if (canonical3(b2[k]) !== canonical3(a[k]))
82745
82979
  return { code: "proof-field-changed", detail: `the candidate changes the recorded step's \`${k}\`` };
82746
82980
  }
82747
- return canonical4(before.step) === canonical4(after.step) ? null : assertProofUntouched(before.step, after.step);
82981
+ return canonical3(before.step) === canonical3(after.step) ? null : assertProofUntouched(before.step, after.step);
82748
82982
  }
82749
82983
  function assertRecordingProofUntouched(base, proposed) {
82750
82984
  const { pages: bp, ...bRest } = base;
82751
82985
  const { pages: pp, ...pRest } = proposed;
82752
- if (canonical4(bRest) !== canonical4(pRest))
82986
+ if (canonical3(bRest) !== canonical3(pRest))
82753
82987
  return { code: "proof-field-changed", detail: "the revision changes the recording's own fields" };
82754
82988
  if (bp.length !== pp.length)
82755
82989
  return { code: "shape-changed", detail: `the revision has ${pp.length} pages; the Journey has ${bp.length}` };
@@ -82757,7 +82991,7 @@ function assertRecordingProofUntouched(base, proposed) {
82757
82991
  for (let i = 0; i < bp.length; i++) {
82758
82992
  const { steps: bs, ...bPage } = bp[i];
82759
82993
  const { steps: ps, ...pPage } = pp[i];
82760
- if (canonical4(bPage) !== canonical4(pPage))
82994
+ if (canonical3(bPage) !== canonical3(pPage))
82761
82995
  return { code: "proof-field-changed", detail: `the revision changes page ${i + 1}` };
82762
82996
  if (bs.length !== ps.length)
82763
82997
  return { code: "shape-changed", detail: `the revision has ${ps.length} steps on page ${i + 1}; the Journey has ${bs.length}` };
@@ -84343,11 +84577,11 @@ async function removeFile(path2) {
84343
84577
  await assertNoSymlink(path2);
84344
84578
  await rm4(path2, { force: true });
84345
84579
  }
84346
- function canonical5(v) {
84347
- if (Array.isArray(v)) return `[${v.map(canonical5).join(",")}]`;
84580
+ function canonical4(v) {
84581
+ if (Array.isArray(v)) return `[${v.map(canonical4).join(",")}]`;
84348
84582
  if (v !== null && typeof v === "object") {
84349
84583
  const o = v;
84350
- return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonical5(o[k])}`).join(",")}}`;
84584
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonical4(o[k])}`).join(",")}}`;
84351
84585
  }
84352
84586
  return JSON.stringify(v) ?? "undefined";
84353
84587
  }
@@ -84372,7 +84606,7 @@ function proposalFloorViolation(stored, proposed) {
84372
84606
  for (let i = 0; i < after.length; i++) {
84373
84607
  const b2 = before[i];
84374
84608
  const a = after[i];
84375
- if (b2 !== void 0 && canonical5(b2.step) === canonical5(a.step)) continue;
84609
+ if (b2 !== void 0 && canonical4(b2.step) === canonical4(a.step)) continue;
84376
84610
  for (const [which, rec] of [["before", b2?.recorded], ["after", a.recorded]]) {
84377
84611
  if (rec === void 0) return `step ${i + 1} is not in the stored Journey`;
84378
84612
  const floor = healFloor(rec, riskOf);
@@ -86065,7 +86299,7 @@ var FINGERPRINT_MS = 1e4;
86065
86299
  function oneLine2(s3) {
86066
86300
  return s3.replace(/\s+/g, " ").trim();
86067
86301
  }
86068
- function routeOf2(url2) {
86302
+ function routeOf3(url2) {
86069
86303
  try {
86070
86304
  const u = new URL(url2);
86071
86305
  return u.pathname + (u.search === "" ? "" : u.search);
@@ -86142,7 +86376,7 @@ var RunScreenshots = class {
86142
86376
  this.#entries.push({
86143
86377
  n: n3,
86144
86378
  step: at3.step,
86145
- route: this.#redact(redactUrl(routeOf2(url2))),
86379
+ route: this.#redact(redactUrl(routeOf3(url2))),
86146
86380
  what: this.#redact(oneLine2(at3.what)).slice(0, 300),
86147
86381
  path: path2
86148
86382
  });
@@ -86466,7 +86700,7 @@ function invariantSetupTexts(spec) {
86466
86700
  return Object.fromEntries(specStrings(spec, "", []).filter(([, s3]) => s3.includes("${setup.")).map(([p, s3]) => [`--invariants ${p}`, s3]));
86467
86701
  }
86468
86702
  function substituteSpecSetupRefs(spec, b2, baseUrl) {
86469
- const walk = (v, path2) => {
86703
+ const walk2 = (v, path2) => {
86470
86704
  if (typeof v === "string") {
86471
86705
  if (!v.includes("${setup.")) return v;
86472
86706
  const where = `--invariants ${path2}`;
@@ -86481,11 +86715,11 @@ function substituteSpecSetupRefs(spec, b2, baseUrl) {
86481
86715
  if (before !== after) throw new UnboundSetupRefError(`${where}: a \${setup.*} value may fill the path or query, never the origin`);
86482
86716
  return out;
86483
86717
  }
86484
- if (Array.isArray(v)) return v.map((x, i) => walk(x, `${path2}[${i}]`));
86485
- if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, path2 === "" ? k : `${path2}.${k}`)]));
86718
+ if (Array.isArray(v)) return v.map((x, i) => walk2(x, `${path2}[${i}]`));
86719
+ if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk2(x, path2 === "" ? k : `${path2}.${k}`)]));
86486
86720
  return v;
86487
86721
  };
86488
- return walk(spec, "");
86722
+ return walk2(spec, "");
86489
86723
  }
86490
86724
  function fixtureSetupFailedResult(err, fx) {
86491
86725
  const reason = err instanceof FixtureSetupError ? err.message : `fixture setup failed: ${err.message}`;
@@ -87785,6 +88019,7 @@ var ServerLogRuntime = class {
87785
88019
  };
87786
88020
  }
87787
88021
  };
88022
+ var NON_ACTING_OPS = /* @__PURE__ */ new Set(["done", "blocked", null]);
87788
88023
  function stepWindows(transcript, stepEpoch, missionStartEpochMs, drainMs) {
87789
88024
  const windows = [];
87790
88025
  let prevEnd = missionStartEpochMs;
@@ -87793,7 +88028,9 @@ function stepWindows(transcript, stepEpoch, missionStartEpochMs, drainMs) {
87793
88028
  const settle2 = stepEpoch.get(entry.step) ?? prevEnd;
87794
88029
  const isLast = i === transcript.length - 1;
87795
88030
  const endMs = isLast ? settle2 + drainMs : settle2;
87796
- windows.push({ step: entry.step, startMs: prevEnd, endMs });
88031
+ const acting = windows[windows.length - 1];
88032
+ if (NON_ACTING_OPS.has(entry.op) && acting !== void 0) acting.endMs = endMs;
88033
+ else windows.push({ step: entry.step, startMs: prevEnd, endMs });
87797
88034
  prevEnd = settle2;
87798
88035
  }
87799
88036
  return windows;
@@ -88690,7 +88927,7 @@ function findingIdentity(f2) {
88690
88927
  function str2(v, fallback = "") {
88691
88928
  return typeof v === "string" ? v : fallback;
88692
88929
  }
88693
- function routeOf3(item) {
88930
+ function routeOf4(item) {
88694
88931
  if (typeof item.route === "string") return item.route;
88695
88932
  if (typeof item.url === "string") return normalizeRoute(item.url);
88696
88933
  return "/";
@@ -88702,7 +88939,7 @@ function extractRunFindings(data) {
88702
88939
  if (!Array.isArray(list2)) return;
88703
88940
  for (const item of list2) {
88704
88941
  if (!isRecord(item) || typeof item.fingerprint !== "string") continue;
88705
- out.push({ kind: kindOf(item), fingerprint: item.fingerprint, route: routeOf3(item), title: str2(item.title, item.fingerprint) });
88942
+ out.push({ kind: kindOf(item), fingerprint: item.fingerprint, route: routeOf4(item), title: str2(item.title, item.fingerprint) });
88706
88943
  }
88707
88944
  };
88708
88945
  fingerprinted(data.defects, (d) => d.kind === "invariant" ? "invariant" : `defect/${str2(d.kind, "unknown")}`);
@@ -88711,14 +88948,14 @@ function extractRunFindings(data) {
88711
88948
  if (isRecord(data.coverage) && Array.isArray(data.coverage.defects)) {
88712
88949
  for (const d of data.coverage.defects) {
88713
88950
  if (!isRecord(d) || typeof d.stateFingerprint !== "string") continue;
88714
- out.push({ kind: "coverage-defect", fingerprint: d.stateFingerprint, route: routeOf3(d), title: str2(d.reason, "looks broken") });
88951
+ out.push({ kind: "coverage-defect", fingerprint: d.stateFingerprint, route: routeOf4(d), title: str2(d.reason, "looks broken") });
88715
88952
  }
88716
88953
  }
88717
88954
  if (isRecord(data.report) && Array.isArray(data.report.findings)) {
88718
88955
  for (const f2 of data.report.findings) {
88719
88956
  if (!isRecord(f2) || typeof f2.rubricItemId !== "string") continue;
88720
88957
  const controls = Array.isArray(f2.controls) ? f2.controls.filter((c) => typeof c === "string") : [];
88721
- out.push({ kind: "ux", route: routeOf3(f2), title: str2(f2.observation, f2.rubricItemId), rubricItemId: f2.rubricItemId, controls });
88958
+ out.push({ kind: "ux", route: routeOf4(f2), title: str2(f2.observation, f2.rubricItemId), rubricItemId: f2.rubricItemId, controls });
88722
88959
  }
88723
88960
  }
88724
88961
  return out;
@@ -91803,18 +92040,18 @@ function rebaseRecording(recording, env) {
91803
92040
  if (mustBeAllowed && !env.allowedOrigins.includes(u.origin)) refusals.push(`${where} is on ${u.origin}`);
91804
92041
  return raw;
91805
92042
  };
91806
- const walk = (v, where) => {
91807
- if (Array.isArray(v)) return v.map((x, i) => walk(x, `${where}[${i}]`));
92043
+ const walk2 = (v, where) => {
92044
+ if (Array.isArray(v)) return v.map((x, i) => walk2(x, `${where}[${i}]`));
91808
92045
  if (v === null || typeof v !== "object") return v;
91809
92046
  const o = v;
91810
92047
  const out = {};
91811
- for (const [k, x] of Object.entries(o)) out[k] = walk(x, `${where}.${k}`);
92048
+ for (const [k, x] of Object.entries(o)) out[k] = walk2(x, `${where}.${k}`);
91812
92049
  if (o.kind === "navigate" && typeof o.url === "string") out.url = move(o.url, `${where} (navigate ${o.url})`, true);
91813
92050
  if (o.kind === "urlIncludes" && typeof o.text === "string") out.text = move(o.text, `${where}.text`, false);
91814
92051
  return out;
91815
92052
  };
91816
92053
  const pages = recording.pages.map((p, i) => ({
91817
- ...walk(p, `pages[${i}]`),
92054
+ ...walk2(p, `pages[${i}]`),
91818
92055
  url: move(p.url, `pages[${i}] (${p.url})`, true)
91819
92056
  }));
91820
92057
  if (refusals.length > 0) {
@@ -94586,7 +94823,7 @@ function classifyTarget(d, testIdAttributes, via) {
94586
94823
  function kebabCase(s3) {
94587
94824
  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, "");
94588
94825
  }
94589
- function routeOf4(url2) {
94826
+ function routeOf5(url2) {
94590
94827
  if (url2 === void 0 || url2 === "") return void 0;
94591
94828
  try {
94592
94829
  return new URL(url2).pathname;
@@ -94684,7 +94921,7 @@ function dedupeSuggestions(lists) {
94684
94921
  return [...byKey.values()].sort((a, b2) => b2.occurrences.length - a.occurrences.length || a.key.localeCompare(b2.key));
94685
94922
  }
94686
94923
  function flatWithRoute(rec) {
94687
- return rec.pages.flatMap((p) => p.steps.map((recorded) => ({ recorded, route: routeOf4(p.url) })));
94924
+ return rec.pages.flatMap((p) => p.steps.map((recorded) => ({ recorded, route: routeOf5(p.url) })));
94688
94925
  }
94689
94926
  function healthLine(h) {
94690
94927
  const total = h.stable + h.brittle;
@@ -97454,7 +97691,7 @@ function controlKey2(c) {
97454
97691
  }
97455
97692
 
97456
97693
  // ../ux/dist/route.js
97457
- function routeOf5(url2) {
97694
+ function routeOf6(url2) {
97458
97695
  try {
97459
97696
  return urlTemplate(new URL(url2).pathname || "/");
97460
97697
  } catch {
@@ -97603,7 +97840,7 @@ function a11yChecks(evidence, rubric) {
97603
97840
  const q = rubric.get(id)?.questions[0];
97604
97841
  return q?.severity ?? "minor";
97605
97842
  };
97606
- const route = routeOf5(evidence.url);
97843
+ const route = routeOf6(evidence.url);
97607
97844
  const controlByRef = new Map(evidence.controls.map((c) => [`control:${c.index}`, c]));
97608
97845
  const labelOf2 = (refId) => {
97609
97846
  const c = controlByRef.get(refId);
@@ -97946,7 +98183,7 @@ function makeSignalFinding(input2) {
97946
98183
  recommendation: input2.recommendation.trim(),
97947
98184
  tier: "signal",
97948
98185
  screenId: input2.screenId,
97949
- route: routeOf5(input2.url),
98186
+ route: routeOf6(input2.url),
97950
98187
  controls: [...input2.controls ?? []],
97951
98188
  quotes: [...input2.quotes ?? []],
97952
98189
  occurrences: input2.occurrences ?? 1,
@@ -98183,7 +98420,7 @@ function detectInternalIds(capture2) {
98183
98420
  if (typed.includes(id))
98184
98421
  continue;
98185
98422
  const line2 = quoteLine(screen.visibleText, id);
98186
- const key2 = `${routeOf5(screen.url)}|${pattern.name}`;
98423
+ const key2 = `${routeOf6(screen.url)}|${pattern.name}`;
98187
98424
  const g = groups.get(key2) ?? { pattern, ids: /* @__PURE__ */ new Set(), lines: /* @__PURE__ */ new Set(), screens: [], intentional: true };
98188
98425
  g.ids.add(id);
98189
98426
  g.lines.add(line2);
@@ -98292,7 +98529,7 @@ var PASSIVE_OPS = /* @__PURE__ */ new Set(["wait", "scroll_down", "scroll_up"]);
98292
98529
  function advanced(capture2, s3) {
98293
98530
  const before = capture2.screens.filter((x) => x.step === s3.step).pop();
98294
98531
  const after = capture2.screens.find((x) => x.step > s3.step);
98295
- if (before === void 0 || after === void 0 || routeOf5(after.url) !== routeOf5(before.url))
98532
+ if (before === void 0 || after === void 0 || routeOf6(after.url) !== routeOf6(before.url))
98296
98533
  return void 0;
98297
98534
  return after.visibleText !== before.visibleText ? after : void 0;
98298
98535
  }
@@ -98315,7 +98552,7 @@ function detectRepeatedAdvances(capture2) {
98315
98552
  close();
98316
98553
  continue;
98317
98554
  }
98318
- const k = `${routeOf5(s3.url)}|${label2.toLowerCase()}`;
98555
+ const k = `${routeOf6(s3.url)}|${label2.toLowerCase()}`;
98319
98556
  if (key2 !== null && k !== key2)
98320
98557
  close();
98321
98558
  key2 = k;
@@ -98343,7 +98580,7 @@ function detectRepeatedAdvances(capture2) {
98343
98580
  steps,
98344
98581
  requests: [],
98345
98582
  ...last.after.screenshot === void 0 ? {} : { screenshot: last.after.screenshot },
98346
- detail: `${n3} consecutive successful "${label2}" clicks on ${routeOf5(last.after.url)} with no input between them, each changing the page text`
98583
+ detail: `${n3} consecutive successful "${label2}" clicks on ${routeOf6(last.after.url)} with no input between them, each changing the page text`
98347
98584
  }
98348
98585
  });
98349
98586
  });
@@ -98395,7 +98632,7 @@ function detectStuckJobs(capture2, opts = {}) {
98395
98632
  const label2 = labelOf(start);
98396
98633
  if (label2 === null)
98397
98634
  continue;
98398
- const route = routeOf5(start.url);
98635
+ const route = routeOf6(start.url);
98399
98636
  const key2 = `${route}|${normLine(label2)}`;
98400
98637
  if (reported.has(key2))
98401
98638
  continue;
@@ -98404,7 +98641,7 @@ function detectStuckJobs(capture2, opts = {}) {
98404
98641
  continue;
98405
98642
  const window2 = [];
98406
98643
  for (const s3 of capture2.steps.filter((x) => x.step > start.step)) {
98407
- if (routeOf5(s3.url) !== route)
98644
+ if (routeOf6(s3.url) !== route)
98408
98645
  break;
98409
98646
  const other = s3.op !== null && START_OPS.has(s3.op) && controlKey3(s3) !== controlKey3(start);
98410
98647
  if (other && capture2.requests.some((r) => r.step === s3.step && isWrite(r) && okStatus(r)))
@@ -98412,7 +98649,7 @@ function detectStuckJobs(capture2, opts = {}) {
98412
98649
  window2.push(s3);
98413
98650
  }
98414
98651
  const lastStep = window2.length > 0 ? window2[window2.length - 1].step : start.step;
98415
- const screens = capture2.screens.filter((s3) => s3.step > start.step && s3.step <= lastStep + 1 && routeOf5(s3.url) === route);
98652
+ const screens = capture2.screens.filter((s3) => s3.step > start.step && s3.step <= lastStep + 1 && routeOf6(s3.url) === route);
98416
98653
  if (screens.length === 0)
98417
98654
  continue;
98418
98655
  const reclicks = window2.filter((s3) => s3.op === start.op && s3.actOk && controlKey3(s3) === controlKey3(start));
@@ -98613,7 +98850,7 @@ function detectFailedSubmits(capture2) {
98613
98850
  const after = screenAfter(capture2, s3.step);
98614
98851
  if (after === void 0)
98615
98852
  continue;
98616
- if (before !== void 0 && routeOf5(after.url) !== routeOf5(before.url))
98853
+ if (before !== void 0 && routeOf6(after.url) !== routeOf6(before.url))
98617
98854
  continue;
98618
98855
  const lines = linesOf(`${after.visibleText}
98619
98856
  ${s3.reply ?? ""}`);
@@ -98682,15 +98919,15 @@ function headingOf(s3) {
98682
98919
  function detectUrlMismatches(capture2) {
98683
98920
  const hits = /* @__PURE__ */ new Map();
98684
98921
  capture2.screens.forEach((b2, i) => {
98685
- const route = routeOf5(b2.url);
98922
+ const route = routeOf6(b2.url);
98686
98923
  const heading = headingOf(b2);
98687
98924
  if (heading.length < 3)
98688
98925
  return;
98689
98926
  const { words: words3, known } = routeWords2(route);
98690
98927
  if (words3.length === 0 || relates(heading, words3))
98691
98928
  return;
98692
- const other = capture2.screens.find((s3) => routeOf5(s3.url) !== route && normLine(headingOf(s3)) === normLine(heading) && relates(heading, routeWords2(routeOf5(s3.url)).words));
98693
- const prior = capture2.screens.slice(0, i).reverse().find((s3) => routeOf5(s3.url) === route && normLine(headingOf(s3)) !== normLine(heading));
98929
+ const other = capture2.screens.find((s3) => routeOf6(s3.url) !== route && normLine(headingOf(s3)) === normLine(heading) && relates(heading, routeWords2(routeOf6(s3.url)).words));
98930
+ const prior = capture2.screens.slice(0, i).reverse().find((s3) => routeOf6(s3.url) === route && normLine(headingOf(s3)) !== normLine(heading));
98694
98931
  const transitioned = known && prior !== void 0 && relates(headingOf(prior), words3);
98695
98932
  if (other === void 0 && !transitioned)
98696
98933
  return;
@@ -98701,7 +98938,7 @@ function detectUrlMismatches(capture2) {
98701
98938
  route,
98702
98939
  known,
98703
98940
  ...transitioned ? { was: headingOf(prior) } : {},
98704
- ...other === void 0 ? {} : { elsewhere: routeOf5(other.url) }
98941
+ ...other === void 0 ? {} : { elsewhere: routeOf6(other.url) }
98705
98942
  };
98706
98943
  g.screens.push(b2);
98707
98944
  hits.set(k, g);
@@ -98806,7 +99043,7 @@ function detectFriction(capture2, outcome) {
98806
99043
  };
98807
99044
  const routesAt = (nums) => {
98808
99045
  const set2 = new Set(nums);
98809
- 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))])];
99046
+ 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))])];
98810
99047
  };
98811
99048
  const add = (kind, impact, nums, detail) => {
98812
99049
  const sorted2 = [...new Set(nums)].filter((n3) => n3 > 0).sort((a, b2) => a - b2);
@@ -98897,7 +99134,7 @@ function detectFriction(capture2, outcome) {
98897
99134
  const blocking = reqs.some((r) => WRITE_METHODS3.has(r.method.toUpperCase()) && (r.failed === true || (r.status ?? 0) >= 500));
98898
99135
  add("error", blocking ? "blocked" : "slowed", [step], `${reqs.map((r) => `${r.endpoint} \u2192 ${r.failed === true ? "failed" : r.status}`).join(", ")} after step ${step}`);
98899
99136
  }
98900
- const seq = capture2.screens.map((s3) => ({ route: routeOf5(s3.url), step: s3.step }));
99137
+ const seq = capture2.screens.map((s3) => ({ route: routeOf6(s3.url), step: s3.step }));
98901
99138
  for (let x = 2; x < seq.length; x++) {
98902
99139
  const here = seq[x];
98903
99140
  if (seq[x - 1].route === here.route)
@@ -98924,7 +99161,7 @@ function detectFriction(capture2, outcome) {
98924
99161
  continue;
98925
99162
  if (between.some((r) => r.actOk && (r.op === "click" || r.op === "send" || r.op === "select")))
98926
99163
  continue;
98927
- 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)}`);
99164
+ 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)}`);
98928
99165
  }
98929
99166
  if (outcome?.status === "incomplete" && steps.length > 0) {
98930
99167
  const tail = steps.slice(-3).map((s3) => s3.step);
@@ -99502,8 +99739,8 @@ function boundTargetOptions(controls, point, steps, limit = MAX_CHOICE_OPTIONS -
99502
99739
  const acted = steps.filter((s3) => point.steps.includes(s3.step));
99503
99740
  const words3 = new Set(normalizeText(point.detail).split(" ").filter((w) => w.length >= 3));
99504
99741
  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);
99505
- 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));
99506
- return controls.filter((c) => keep.has(c.index));
99742
+ 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));
99743
+ return controls.filter((c) => keep2.has(c.index));
99507
99744
  }
99508
99745
  function frictionText(p) {
99509
99746
  return `${p.kind} (steps ${p.steps.join(", ")}): ${p.detail}`.slice(0, 600);
@@ -99532,7 +99769,7 @@ async function analyzeClaims(request, deps) {
99532
99769
  for (const raw of request.screens) {
99533
99770
  try {
99534
99771
  const red = redactEvidence(raw, secrets, deps.redactor);
99535
- screens.push({ raw, red, route: routeOf5(red.url) });
99772
+ screens.push({ raw, red, route: routeOf6(red.url) });
99536
99773
  } catch (cause) {
99537
99774
  return { kind: "failed", reason: `redaction failed: ${message(cause)}`, screenId: raw.screenId };
99538
99775
  }
@@ -100022,7 +100259,7 @@ ${f2.route}
100022
100259
  ${element}`, "utf8").digest("hex").slice(0, 16);
100023
100260
  }
100024
100261
  function contractRouteTemplate(urlOrRoute) {
100025
- return routeOf5(urlOrRoute).split("/").map((seg) => /^:[A-Za-z][A-Za-z0-9_]{0,63}$/.test(seg) ? `{${seg.slice(1)}}` : seg).join("/");
100262
+ return routeOf6(urlOrRoute).split("/").map((seg) => /^:[A-Za-z][A-Za-z0-9_]{0,63}$/.test(seg) ? `{${seg.slice(1)}}` : seg).join("/");
100026
100263
  }
100027
100264
 
100028
100265
  // ../ux/dist/rubric/schema.js
@@ -100651,14 +100888,14 @@ var CREDENTIAL_KEY2 = /pass|secret|token|otp|totp|pin|cvc|cvv|card|auth|session|
100651
100888
  var MAX_DIGEST_BODY = 64e3;
100652
100889
  var CredentialBody = class extends Error {
100653
100890
  };
100654
- function canonical6(v) {
100655
- if (Array.isArray(v)) return v.map(canonical6);
100891
+ function canonical5(v) {
100892
+ if (Array.isArray(v)) return v.map(canonical5);
100656
100893
  if (v !== null && typeof v === "object") {
100657
100894
  const out = {};
100658
100895
  for (const k of Object.keys(v).sort()) {
100659
100896
  if (CREDENTIAL_KEY2.test(k)) throw new CredentialBody();
100660
100897
  if (VOLATILE_KEY.test(k)) continue;
100661
- out[k] = canonical6(v[k]);
100898
+ out[k] = canonical5(v[k]);
100662
100899
  }
100663
100900
  return out;
100664
100901
  }
@@ -100675,7 +100912,7 @@ function payloadDigest(method, endpoint2, body, secrets) {
100675
100912
  value = Object.fromEntries(new URLSearchParams(body));
100676
100913
  }
100677
100914
  try {
100678
- const canon = JSON.stringify(canonical6(value));
100915
+ const canon = JSON.stringify(canonical5(value));
100679
100916
  return createHash12("sha256").update(`${method.toUpperCase()} ${endpoint2}
100680
100917
  ${canon}`).digest("hex").slice(0, 32);
100681
100918
  } catch {
@@ -101364,7 +101601,7 @@ function planGuardProbes(screens, secrets, safety) {
101364
101601
  const refused = [];
101365
101602
  for (const screen of screens) {
101366
101603
  const red = redactEvidence(screen, secrets);
101367
- const route = routeOf5(red.url);
101604
+ const route = routeOf6(red.url);
101368
101605
  screen.controls.forEach((c, i) => {
101369
101606
  if (controlRisk(c.name, c.role)?.risk !== "destructive") return;
101370
101607
  const rc = red.controls[i];
@@ -102123,11 +102360,11 @@ var MAX_SCAN_DEPTH = 8;
102123
102360
  function scanRuns(dirs) {
102124
102361
  const seen = /* @__PURE__ */ new Set();
102125
102362
  const runs = [];
102126
- const walk = (dir, depth) => {
102363
+ const walk2 = (dir, depth) => {
102127
102364
  for (const entry of readdirSync8(dir, { withFileTypes: true }).sort((a, b2) => a.name.localeCompare(b2.name))) {
102128
102365
  const path2 = resolve13(dir, entry.name);
102129
102366
  if (entry.isDirectory()) {
102130
- if (depth < MAX_SCAN_DEPTH && !entry.name.startsWith(".") && entry.name !== "node_modules") walk(path2, depth + 1);
102367
+ if (depth < MAX_SCAN_DEPTH && !entry.name.startsWith(".") && entry.name !== "node_modules") walk2(path2, depth + 1);
102131
102368
  continue;
102132
102369
  }
102133
102370
  if (!entry.isFile() || !isResultName(entry.name) || seen.has(path2)) continue;
@@ -102139,7 +102376,7 @@ function scanRuns(dirs) {
102139
102376
  };
102140
102377
  for (const dir of dirs) {
102141
102378
  if (!existsSync23(dir) || !statSync9(dir).isDirectory()) continue;
102142
- walk(dir, 0);
102379
+ walk2(dir, 0);
102143
102380
  }
102144
102381
  return runs.sort((a, b2) => (a.startedAt ?? "").localeCompare(b2.startedAt ?? "") || a.runId.localeCompare(b2.runId));
102145
102382
  }
@@ -104104,10 +104341,17 @@ import { existsSync as existsSync25 } from "node:fs";
104104
104341
  import { copyFile as copyFile4, lstat as lstat4, mkdir as mkdir25, open as open3, readdir as readdir9, readFile as readFile24, rename as rename4, rm as rm10, writeFile as writeFile29 } from "node:fs/promises";
104105
104342
  import { basename as basename15, dirname as dirname24, join as join56, resolve as resolve18, sep as sep7 } from "node:path";
104106
104343
 
104344
+ // src/catalog-contract-identity.ts
104345
+ var CATALOG_CONTRACT_IDENTITY = {
104346
+ "kind": "jevitate.catalog-contract",
104347
+ "version": "2.0.0",
104348
+ "manifestSha256": "c1292a8727d7dc0dbe1d63dc768d34b6cc94ae60fab3a67aeb0a53837e1735ad"
104349
+ };
104350
+
104107
104351
  // src/catalog-bundle.ts
104108
104352
  var BUNDLE_KIND = "journeeze.catalog-bundle";
104109
104353
  var BUNDLE_VERSION = 1;
104110
- var BUNDLE_MINOR = 1;
104354
+ var BUNDLE_MINOR = 2;
104111
104355
  var CatalogBundleInputError = class extends CatalogInputError {
104112
104356
  constructor(issues) {
104113
104357
  super(`the catalog cannot be exported as a Journeeze bundle (${issues.length} problem(s)): ${issues.join("; ")}`);
@@ -104472,7 +104716,7 @@ function bundleChecks(input2, journeys, warn) {
104472
104716
  if (outcome.outcome !== "clean") why = `the run was ${outcome.outcome}, and a baseline comes only from a clean run`;
104473
104717
  else if (journeyId === void 0 || journeyHash === void 0) why = "the item names no Journey review hash";
104474
104718
  else if (cj === void 0) why = `Journey ${journeyId} is not in the bundle`;
104475
- else if (cj.approval?.contentHash !== journeyHash || cj.contentHash !== journeyHash) why = `it was measured on a Journey revision (${journeyHash.slice(0, 12)}\u2026) that is not ${journeyId}'s approved one`;
104719
+ else if (cj.approval?.contentHash !== journeyHash || cj.changedSinceApproval) why = `it was measured on a Journey revision (${journeyHash.slice(0, 12)}\u2026) that is not ${journeyId}'s approved one`;
104476
104720
  else {
104477
104721
  const b2 = bundleBaseline(item.baseline, cj.journey);
104478
104722
  if ("drop" in b2) why = b2.drop;
@@ -104526,13 +104770,13 @@ function bundleDemos(input2, journeys, warn) {
104526
104770
  warn.push(`${where}: its Journey is not in the bundle (not promoted, or left out) \u2014 the demo is left out`);
104527
104771
  continue;
104528
104772
  }
104529
- const approved = ex.cj.approval !== void 0 && ex.cj.approval.contentHash === ex.cj.contentHash;
104773
+ const approved = ex.cj.approval !== void 0 && !ex.cj.changedSinceApproval;
104530
104774
  if (!approved) {
104531
104775
  warn.push(`${where}: its Journey changed since it was approved \u2014 the demo is left out (approve the Journey and its demo again)`);
104532
104776
  continue;
104533
104777
  }
104534
- if (d.renderedFrom !== ex.cj.contentHash) {
104535
- warn.push(`${where}: rendered from another version of its Journey (${d.renderedFrom.slice(0, 12)}, approved ${ex.cj.contentHash.slice(0, 12)}) \u2014 the demo is left out (run \`jevitate demo approve\` again)`);
104778
+ if (d.renderedFrom !== ex.cj.approval.contentHash) {
104779
+ warn.push(`${where}: rendered from another version of its Journey (${d.renderedFrom.slice(0, 12)}, approved ${ex.cj.approval.contentHash.slice(0, 12)}) \u2014 the demo is left out (run \`jevitate demo approve\` again)`);
104536
104780
  continue;
104537
104781
  }
104538
104782
  if (d.steps.length === 0 || d.steps.length > MEDIA_LIMITS.demoSteps) {
@@ -104618,8 +104862,8 @@ function bundleDemos(input2, journeys, warn) {
104618
104862
  if (demos.length > LIMITS.demos) {
104619
104863
  warn.push(`${demos.length} demos: only the first ${LIMITS.demos} are exported`);
104620
104864
  const kept = new Set(demos.slice(0, LIMITS.demos).map((d) => `media/${d.journey}/`));
104621
- const keep = (p) => [...kept].some((k) => p.startsWith(k));
104622
- return { demos: demos.slice(0, LIMITS.demos), files: files.filter((f2) => keep(f2.path)), media: media.filter((m) => keep(m.path)) };
104865
+ const keep2 = (p) => [...kept].some((k) => p.startsWith(k));
104866
+ return { demos: demos.slice(0, LIMITS.demos), files: files.filter((f2) => keep2(f2.path)), media: media.filter((m) => keep2(m.path)) };
104623
104867
  }
104624
104868
  return { demos, files, media };
104625
104869
  }
@@ -104684,6 +104928,7 @@ function buildCatalogBundle(input2) {
104684
104928
  version: BUNDLE_VERSION,
104685
104929
  minor: BUNDLE_MINOR,
104686
104930
  producer: { tool: "jevitate", version: input2.producer.version, ...commit === void 0 ? {} : { commit } },
104931
+ catalogContract: CATALOG_CONTRACT_IDENTITY,
104687
104932
  product: { name: input2.productName },
104688
104933
  catalog: { personas, jobs, journeys: journeys.map((j) => j.entry) },
104689
104934
  ...shown2.demos.length === 0 ? {} : { demos: shown2.demos },
@@ -105315,7 +105560,7 @@ function findingDropWarning(fingerprint2, kind, issues) {
105315
105560
  function bundleFindings(src) {
105316
105561
  const warnings = [];
105317
105562
  const latest = /* @__PURE__ */ new Map();
105318
- const keep = (m) => {
105563
+ const keep2 = (m) => {
105319
105564
  if (m === null) return;
105320
105565
  if ("drop" in m) {
105321
105566
  warnings.push(`${m.drop} \u2014 left out of the bundle`);
@@ -105330,10 +105575,10 @@ function bundleFindings(src) {
105330
105575
  if (prev === void 0 || prev.at <= m.finding.at) latest.set(m.finding.fingerprint, m.finding);
105331
105576
  };
105332
105577
  const byPath = new Map(src.runs.map((r) => [r.path, r]));
105333
- for (const d of consolidate(src.runs)) keep(defectFinding(d, byPath));
105578
+ for (const d of consolidate(src.runs)) keep2(defectFinding(d, byPath));
105334
105579
  for (const { report: report2, run } of src.uxReports) {
105335
105580
  if (!isRecord(report2) || !Array.isArray(report2.findings)) continue;
105336
- for (const f2 of report2.findings) if (isRecord(f2)) keep(uxFinding(f2, run));
105581
+ for (const f2 of report2.findings) if (isRecord(f2)) keep2(uxFinding(f2, run));
105337
105582
  }
105338
105583
  let findings = [...latest.values()].sort((a, b2) => a.at.localeCompare(b2.at) || a.fingerprint.localeCompare(b2.fingerprint));
105339
105584
  if (findings.length > MAX_FINDINGS) {
@@ -109647,8 +109892,8 @@ var jevReadinessAnalyzer = {
109647
109892
  // src/journey-stale-api.ts
109648
109893
  function isStepIdOnlyStale(journey, snapshot2) {
109649
109894
  const approval = journey.metadata.approval;
109650
- if (approval === void 0 || approval.contentHash === journeyReviewHash(journey)) return false;
109651
- if (snapshot2 !== null && journeyReviewHash(snapshot2) === approval.contentHash) return isStepIdOnlyChange(snapshot2, journey);
109895
+ if (approval === void 0 || journeyApprovalMatches(journey)) return false;
109896
+ if (snapshot2 !== null && [journeyReviewHash(snapshot2), journeyLegacyReviewHash(snapshot2)].includes(approval.contentHash)) return isStepIdOnlyChange(snapshot2, journey);
109652
109897
  return isStepIdOnlyChange(approval.contentHash, journey);
109653
109898
  }
109654
109899
  async function listStaleJourneys(req) {
@@ -109662,7 +109907,7 @@ async function listStaleJourneys(req) {
109662
109907
  const approval = journey.metadata.approval;
109663
109908
  if (approval === void 0) continue;
109664
109909
  const contentHash2 = journeyReviewHash(journey);
109665
- const hashStale = approval.contentHash !== contentHash2;
109910
+ const hashStale = !journeyApprovalMatches(journey);
109666
109911
  const links = journeyLinks(catalog, catalogJourney(journey));
109667
109912
  const catalogStale = !hashStale && links.linked && [links.job?.status, links.persona?.status].includes("stale");
109668
109913
  if (!hashStale && !catalogStale) continue;
@@ -116748,8 +116993,8 @@ function confineSweepFixture(raw, root2) {
116748
116993
  realRoot = realpathSync5(root2);
116749
116994
  } catch {
116750
116995
  }
116751
- const sensitive = fixtureSensitiveReason(real, realRoot) ?? fixtureSensitiveReason(absolute, resolve23(root2));
116752
- if (sensitive !== null) throw fixtureSensitive(absolute, root2, sensitive);
116996
+ const sensitive2 = fixtureSensitiveReason(real, realRoot) ?? fixtureSensitiveReason(absolute, resolve23(root2));
116997
+ if (sensitive2 !== null) throw fixtureSensitive(absolute, root2, sensitive2);
116753
116998
  return absolute;
116754
116999
  }
116755
117000
  function tsvTags(cell2, where, problems) {
@@ -120101,17 +120346,17 @@ function isDefectStatus(v) {
120101
120346
  return typeof v === "string" && DEFECT_OUTCOME_STATUSES.includes(v);
120102
120347
  }
120103
120348
  function parseResultOutcome(missionOutcome, goalOutcome, defectStatus) {
120104
- const canonical7 = parseMissionOutcome(missionOutcome);
120105
- if (canonical7 !== null) {
120349
+ const canonical6 = parseMissionOutcome(missionOutcome);
120350
+ if (canonical6 !== null) {
120106
120351
  if (goalOutcome === void 0)
120107
- return { outcome: canonical7 };
120352
+ return { outcome: canonical6 };
120108
120353
  if (!isGoalOutcome(goalOutcome))
120109
120354
  return null;
120110
120355
  if (defectStatus === void 0)
120111
- return foldGoalOutcome(goalOutcome) === canonical7 ? { outcome: canonical7, goalOutcome } : null;
120356
+ return foldGoalOutcome(goalOutcome) === canonical6 ? { outcome: canonical6, goalOutcome } : null;
120112
120357
  if (!isDefectStatus(defectStatus))
120113
120358
  return null;
120114
- return goalMissionOutcome(goalOutcome, defectStatus) === canonical7 ? { outcome: canonical7, goalOutcome } : null;
120359
+ return goalMissionOutcome(goalOutcome, defectStatus) === canonical6 ? { outcome: canonical6, goalOutcome } : null;
120115
120360
  }
120116
120361
  if (goalOutcome === void 0 && typeof missionOutcome === "string" && Object.hasOwn(LEGACY_GOAL_OUTCOMES, missionOutcome)) {
120117
120362
  return { outcome: LEGACY_GOAL_OUTCOMES[missionOutcome], goalOutcome: missionOutcome };