@iamu-dev/iamu 0.2.0-beta.7 → 0.2.0-beta.9

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
@@ -10488,9 +10488,275 @@ var init_prune = __esm({
10488
10488
  }
10489
10489
  });
10490
10490
 
10491
- // ../../packages/local-store/dist/human-test-link.js
10491
+ // ../../packages/local-store/dist/explore-map.js
10492
+ import { randomBytes as randomBytes2 } from "crypto";
10492
10493
  import { promises as fs19 } from "fs";
10493
10494
  import path15 from "path";
10495
+ function templateToken(token) {
10496
+ if (token === "")
10497
+ return token;
10498
+ if (/^\d+$/.test(token))
10499
+ return ":id";
10500
+ if (UUID.test(token))
10501
+ return ":id";
10502
+ if (DATE.test(token))
10503
+ return ":date";
10504
+ if (/^[0-9a-f]{8,}$/i.test(token) && /\d/.test(token))
10505
+ return ":id";
10506
+ if (token.length >= 16 && /\d/.test(token) && /[a-z]/i.test(token)) {
10507
+ return ":id";
10508
+ }
10509
+ return token;
10510
+ }
10511
+ function templatePath(pathname) {
10512
+ const trimmed = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname || "/";
10513
+ return trimmed.split("/").map((seg) => templateToken(decodeSegment(seg))).join("/");
10514
+ }
10515
+ function encodeQueryValue(v) {
10516
+ return v.startsWith(":") ? v : encodeURIComponent(v);
10517
+ }
10518
+ function decodeSegment(seg) {
10519
+ try {
10520
+ return decodeURIComponent(seg);
10521
+ } catch {
10522
+ return seg;
10523
+ }
10524
+ }
10525
+ function explorePlaceKey(raw) {
10526
+ if (!raw)
10527
+ return null;
10528
+ let u;
10529
+ try {
10530
+ u = new URL(raw);
10531
+ } catch {
10532
+ return null;
10533
+ }
10534
+ if (u.protocol !== "http:" && u.protocol !== "https:")
10535
+ return null;
10536
+ const params = [...u.searchParams.entries()].filter(([k]) => !k.toLowerCase().startsWith("utm_")).map(([k, v]) => [k, templateToken(v)]).sort(([a, av], [b, bv]) => a.localeCompare(b) || av.localeCompare(bv));
10537
+ const query = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeQueryValue(v)}`).join("&");
10538
+ let key = templatePath(u.pathname);
10539
+ if (query)
10540
+ key += `?${query}`;
10541
+ const route = /^#!?(\/.*)$/.exec(u.hash);
10542
+ if (route) {
10543
+ const [hashPath] = route[1].split("?");
10544
+ key += `#${templatePath(hashPath)}`;
10545
+ }
10546
+ return key;
10547
+ }
10548
+ function exploreViewSignature(headings) {
10549
+ return headings.map((h) => h.trim().replace(/\s+/g, " ").replace(/\d+/g, "#").toLowerCase()).filter(Boolean).join(" | ");
10550
+ }
10551
+ function exploreSignature(headings, title) {
10552
+ return exploreViewSignature(headings) || title.trim().toLowerCase();
10553
+ }
10554
+ function matchExploreSurface(map2, url, signature) {
10555
+ const place = explorePlaceKey(url);
10556
+ if (!place)
10557
+ return null;
10558
+ const atPlace = map2.surfaces.filter((s) => s.place === place);
10559
+ return atPlace.find((s) => s.signature === signature) ?? (atPlace.length === 1 ? atPlace[0] : null);
10560
+ }
10561
+ function exploreSurfaceKey(place, view) {
10562
+ return view === null ? place : `${place}~${fnv1a(view)}`;
10563
+ }
10564
+ function fnv1a(s) {
10565
+ let h = 2166136261;
10566
+ for (let i = 0; i < s.length; i++) {
10567
+ h ^= s.charCodeAt(i);
10568
+ h = Math.imul(h, 16777619) >>> 0;
10569
+ }
10570
+ return h.toString(16).padStart(8, "0");
10571
+ }
10572
+ function exploreDir(projectRoot) {
10573
+ return path15.join(cacheDir(projectRoot), "explore");
10574
+ }
10575
+ function composeExploreId(now, randomHex) {
10576
+ const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
10577
+ return `${ts}-${randomHex ?? randomBytes2(3).toString("hex")}`;
10578
+ }
10579
+ function isValidExploreId(id) {
10580
+ return MAP_ID.test(id);
10581
+ }
10582
+ function mapPath(projectRoot, id) {
10583
+ if (!isValidExploreId(id))
10584
+ throw new Error(`Not an explore map id: ${id}`);
10585
+ return path15.join(exploreDir(projectRoot), id, "map.json");
10586
+ }
10587
+ async function writeExploreMap(projectRoot, map2) {
10588
+ const file = mapPath(projectRoot, map2.id);
10589
+ await fs19.mkdir(path15.dirname(file), { recursive: true });
10590
+ await atomicWriteFile(file, `${JSON.stringify(map2, null, 2)}
10591
+ `);
10592
+ return file;
10593
+ }
10594
+ async function readExploreMap(projectRoot, id) {
10595
+ const raw = await fs19.readFile(mapPath(projectRoot, id), "utf8");
10596
+ const map2 = JSON.parse(raw);
10597
+ if (map2?.schema !== EXPLORE_MAP_SCHEMA || !Array.isArray(map2.surfaces)) {
10598
+ throw new Error(`Unreadable explore map: ${id}`);
10599
+ }
10600
+ return map2;
10601
+ }
10602
+ async function listExploreMaps(projectRoot) {
10603
+ try {
10604
+ const entries = await fs19.readdir(exploreDir(projectRoot), {
10605
+ withFileTypes: true
10606
+ });
10607
+ return entries.filter((e) => e.isDirectory() && isValidExploreId(e.name)).map((e) => e.name).sort();
10608
+ } catch (err) {
10609
+ if (err.code === "ENOENT")
10610
+ return [];
10611
+ throw err;
10612
+ }
10613
+ }
10614
+ function exploreCoverage(map2, steps) {
10615
+ const byPlace = /* @__PURE__ */ new Map();
10616
+ for (const s of map2.surfaces) {
10617
+ byPlace.set(s.place, [...byPlace.get(s.place) ?? [], s]);
10618
+ }
10619
+ const reachedBy = /* @__PURE__ */ new Map();
10620
+ const unresolvedBy = /* @__PURE__ */ new Map();
10621
+ const runOnlyBy = /* @__PURE__ */ new Map();
10622
+ const add = (m, k, s) => m.set(k, [...m.get(k) ?? [], s]);
10623
+ for (const step of steps) {
10624
+ const place = explorePlaceKey(step.url);
10625
+ if (!place)
10626
+ continue;
10627
+ const atPlace = byPlace.get(place);
10628
+ if (!atPlace) {
10629
+ add(runOnlyBy, place, step);
10630
+ continue;
10631
+ }
10632
+ const hit = tieStep(map2, atPlace, step);
10633
+ if (hit.length === 0)
10634
+ add(unresolvedBy, place, step);
10635
+ for (const s of hit)
10636
+ add(reachedBy, s.key, step);
10637
+ }
10638
+ const surfaces = map2.surfaces.map((s) => {
10639
+ const base = {
10640
+ key: s.key,
10641
+ label: s.label,
10642
+ place: s.place,
10643
+ url: s.url,
10644
+ depth: s.depth
10645
+ };
10646
+ const reached = reachedBy.get(s.key);
10647
+ if (reached)
10648
+ return { ...base, status: "reached", runs: distinctRuns(reached) };
10649
+ const unresolved = unresolvedBy.get(s.place);
10650
+ if (unresolved) {
10651
+ return { ...base, status: "place_only", runs: distinctRuns(unresolved) };
10652
+ }
10653
+ return { ...base, status: "unreached", runs: [] };
10654
+ });
10655
+ const runOnly = [...runOnlyBy].map(([place, list2]) => ({
10656
+ place,
10657
+ steps: list2.length,
10658
+ runs: distinctRuns(list2)
10659
+ }));
10660
+ runOnly.sort((a, b) => b.steps - a.steps || a.place.localeCompare(b.place));
10661
+ return {
10662
+ map_id: map2.id,
10663
+ runs: new Set(steps.map((s) => s.run_id)).size,
10664
+ steps: steps.length,
10665
+ reached: surfaces.filter((s) => s.status === "reached").length,
10666
+ place_only: surfaces.filter((s) => s.status === "place_only").length,
10667
+ unreached: surfaces.filter((s) => s.status === "unreached").length,
10668
+ surfaces,
10669
+ run_only_places: runOnly
10670
+ };
10671
+ }
10672
+ function tieStep(map2, atPlace, step) {
10673
+ if (typeof step.signature === "string") {
10674
+ const m = matchExploreSurface(map2, step.url, step.signature);
10675
+ return m ? [m] : [];
10676
+ }
10677
+ if (atPlace.length === 1)
10678
+ return [atPlace[0]];
10679
+ const name = step.surface?.trim();
10680
+ if (!name)
10681
+ return [];
10682
+ return atPlace.filter((s) => [s.label, ...s.headings].filter(Boolean).some((n) => surfaceMatches(n, name) || surfaceMatches(name, n)));
10683
+ }
10684
+ function distinctRuns(steps) {
10685
+ return [...new Set(steps.map((s) => s.run_id))].sort();
10686
+ }
10687
+ async function readAllRunSteps(projectRoot) {
10688
+ const out = [];
10689
+ for (const runId of await listRuns(projectRoot)) {
10690
+ let steps;
10691
+ try {
10692
+ steps = await readStepsForRun(projectRoot, runId);
10693
+ } catch {
10694
+ continue;
10695
+ }
10696
+ for (const s of steps) {
10697
+ out.push({ run_id: runId, url: s.url, surface: s.surface });
10698
+ }
10699
+ }
10700
+ return out;
10701
+ }
10702
+ function actionSignature(a) {
10703
+ const where = a.outcome.kind === "surface" ? a.outcome.surface : a.outcome.kind === "skipped" ? `skipped:${a.outcome.reason}` : a.outcome.kind === "external" ? `external:${a.outcome.url}` : a.outcome.kind;
10704
+ return `${a.role} "${a.name}" \u2192 ${where}`;
10705
+ }
10706
+ function diffExploreMaps(a, b) {
10707
+ const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
10708
+ const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
10709
+ const ref = (s) => ({
10710
+ key: s.key,
10711
+ label: s.label
10712
+ });
10713
+ const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
10714
+ const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
10715
+ const changed = [];
10716
+ let common2 = 0;
10717
+ for (const sa of a.surfaces) {
10718
+ const sb = bByKey.get(sa.key);
10719
+ if (!sb)
10720
+ continue;
10721
+ common2++;
10722
+ const setA = new Set(sa.actions.map(actionSignature));
10723
+ const setB = new Set(sb.actions.map(actionSignature));
10724
+ const inA = [...setA].filter((x) => !setB.has(x)).sort();
10725
+ const inB = [...setB].filter((x) => !setA.has(x)).sort();
10726
+ if (inA.length > 0 || inB.length > 0) {
10727
+ changed.push({ ...ref(sa), only_a: inA, only_b: inB });
10728
+ }
10729
+ }
10730
+ const union = common2 + onlyA.length + onlyB.length;
10731
+ return {
10732
+ a: a.id,
10733
+ b: b.id,
10734
+ common: common2,
10735
+ only_a: onlyA,
10736
+ only_b: onlyB,
10737
+ surface_overlap: union === 0 ? 1 : common2 / union,
10738
+ changed_actions: changed
10739
+ };
10740
+ }
10741
+ var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
10742
+ var init_explore_map = __esm({
10743
+ "../../packages/local-store/dist/explore-map.js"() {
10744
+ "use strict";
10745
+ init_atomic();
10746
+ init_paths();
10747
+ init_revisit();
10748
+ init_runs();
10749
+ init_walkthrough_canonical2();
10750
+ EXPLORE_MAP_SCHEMA = 1;
10751
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10752
+ DATE = /^\d{4}-\d{2}-\d{2}$/;
10753
+ MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
10754
+ }
10755
+ });
10756
+
10757
+ // ../../packages/local-store/dist/human-test-link.js
10758
+ import { promises as fs20 } from "fs";
10759
+ import path16 from "path";
10494
10760
  function humanTestLinks(verdicts) {
10495
10761
  const best = /* @__PURE__ */ new Map();
10496
10762
  for (const v of verdicts) {
@@ -10513,7 +10779,7 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
10513
10779
  const dir = entityDirPath(projectRoot, "finding");
10514
10780
  let names;
10515
10781
  try {
10516
- names = await fs19.readdir(dir);
10782
+ names = await fs20.readdir(dir);
10517
10783
  } catch {
10518
10784
  return [];
10519
10785
  }
@@ -10521,11 +10787,11 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
10521
10787
  for (const name of names) {
10522
10788
  if (!name.endsWith(".md") || name.startsWith("."))
10523
10789
  continue;
10524
- const file = path15.join(dir, name);
10790
+ const file = path16.join(dir, name);
10525
10791
  let raw;
10526
10792
  let fm;
10527
10793
  try {
10528
- raw = await fs19.readFile(file, "utf8");
10794
+ raw = await fs20.readFile(file, "utf8");
10529
10795
  fm = parseFrontmatter(raw).frontmatter;
10530
10796
  } catch {
10531
10797
  continue;
@@ -10756,14 +11022,14 @@ var init_cloud_client = __esm({
10756
11022
  });
10757
11023
 
10758
11024
  // ../../packages/local-store/dist/last-pull.js
10759
- import { promises as fs20 } from "fs";
10760
- import path16 from "path";
11025
+ import { promises as fs21 } from "fs";
11026
+ import path17 from "path";
10761
11027
  function lastPullPath(projectRoot) {
10762
- return path16.join(cacheDir(projectRoot), "last-pull.json");
11028
+ return path17.join(cacheDir(projectRoot), "last-pull.json");
10763
11029
  }
10764
11030
  async function readLastPull(projectRoot) {
10765
11031
  try {
10766
- const raw = await fs20.readFile(lastPullPath(projectRoot), "utf8");
11032
+ const raw = await fs21.readFile(lastPullPath(projectRoot), "utf8");
10767
11033
  const parsed = JSON.parse(raw);
10768
11034
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10769
11035
  return parsed;
@@ -11049,7 +11315,7 @@ var init_frontmatter2 = __esm({
11049
11315
  });
11050
11316
 
11051
11317
  // ../../packages/local-store/dist/pull/write.js
11052
- import { promises as fs21 } from "fs";
11318
+ import { promises as fs22 } from "fs";
11053
11319
  async function loadLocalIndex(projectRoot, type2) {
11054
11320
  const byId = /* @__PURE__ */ new Map();
11055
11321
  const bySlug = /* @__PURE__ */ new Map();
@@ -11078,7 +11344,7 @@ async function classifyExisting(projectRoot, type2, cloudId, localSlug, targetSl
11078
11344
  if (localSlug !== targetSlug)
11079
11345
  return "untracked";
11080
11346
  const lastSynced = localIndex.lastPushById[cloudId];
11081
- const bytes = await fs21.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
11347
+ const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
11082
11348
  return lastSynced && hashNormalizedContent(bytes) === lastSynced ? "fast_forward" : "diverged";
11083
11349
  }
11084
11350
  async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body, localIndex, mode, cloudVersion) {
@@ -11139,13 +11405,13 @@ async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body,
11139
11405
  }
11140
11406
  if (existingSlugForId && existingSlugForId !== slug) {
11141
11407
  try {
11142
- await fs21.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
11408
+ await fs22.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
11143
11409
  localIndex.bySlug.delete(existingSlugForId);
11144
11410
  } catch {
11145
11411
  }
11146
11412
  }
11147
11413
  await writeEntity(projectRoot, type2, frontmatter, text);
11148
- const bytes = await fs21.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
11414
+ const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
11149
11415
  const hash = hashNormalizedContent(bytes);
11150
11416
  localIndex.byId.set(cloudId, slug);
11151
11417
  localIndex.bySlug.set(slug, cloudId);
@@ -11199,7 +11465,7 @@ var init_write = __esm({
11199
11465
  });
11200
11466
 
11201
11467
  // ../../packages/local-store/dist/pull/entities.js
11202
- import { promises as fs22 } from "fs";
11468
+ import { promises as fs23 } from "fs";
11203
11469
  function buildContextFrontmatter(payload) {
11204
11470
  const cloudCtx = payload.context ?? {};
11205
11471
  const glossary = (payload.glossary ?? []).map((g) => ({
@@ -11254,7 +11520,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
11254
11520
  const frontmatter = buildContextFrontmatter(payload);
11255
11521
  let hasLocal = false;
11256
11522
  try {
11257
- await fs22.access(contextPath(projectRoot));
11523
+ await fs23.access(contextPath(projectRoot));
11258
11524
  hasLocal = true;
11259
11525
  } catch {
11260
11526
  hasLocal = false;
@@ -11284,7 +11550,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
11284
11550
  frontmatter,
11285
11551
  notes_body: existingNotes
11286
11552
  });
11287
- const bytes = await fs22.readFile(contextPath(projectRoot), "utf8");
11553
+ const bytes = await fs23.readFile(contextPath(projectRoot), "utf8");
11288
11554
  const hash = hashNormalizedContent(bytes);
11289
11555
  await recordLastPull(projectRoot, "context:root", hash);
11290
11556
  await recordLastPush(projectRoot, "context:root", hash);
@@ -11814,6 +12080,7 @@ var init_dist2 = __esm({
11814
12080
  init_prune();
11815
12081
  init_fix_state();
11816
12082
  init_revisit();
12083
+ init_explore_map();
11817
12084
  init_walkthrough_canonical2();
11818
12085
  init_journey_diff();
11819
12086
  init_finding_match();
@@ -11845,15 +12112,15 @@ function resolveApiUrl(configured) {
11845
12112
  assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
11846
12113
  return url;
11847
12114
  }
11848
- function parseStore(raw, path28) {
12115
+ function parseStore(raw, path30) {
11849
12116
  let parsed;
11850
12117
  try {
11851
12118
  parsed = JSON.parse(raw);
11852
12119
  } catch (err) {
11853
- throw new CredentialsFileError(path28, "not valid JSON", err);
12120
+ throw new CredentialsFileError(path30, "not valid JSON", err);
11854
12121
  }
11855
12122
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
11856
- throw new CredentialsFileError(path28, "expected a JSON object");
12123
+ throw new CredentialsFileError(path30, "expected a JSON object");
11857
12124
  }
11858
12125
  return parsed;
11859
12126
  }
@@ -11861,26 +12128,26 @@ function isNotFound(err) {
11861
12128
  return typeof err === "object" && err !== null && err.code === "ENOENT";
11862
12129
  }
11863
12130
  async function readCredentialsStore() {
11864
- const path28 = getCredentialsPath();
12131
+ const path30 = getCredentialsPath();
11865
12132
  let raw;
11866
12133
  try {
11867
- raw = await readFile(path28, "utf-8");
12134
+ raw = await readFile(path30, "utf-8");
11868
12135
  } catch (err) {
11869
12136
  if (isNotFound(err)) return {};
11870
12137
  warnUnreadableStoreOnce(err);
11871
12138
  throw err;
11872
12139
  }
11873
12140
  try {
11874
- return parseStore(raw, path28);
12141
+ return parseStore(raw, path30);
11875
12142
  } catch (err) {
11876
12143
  warnUnreadableStoreOnce(err);
11877
12144
  throw err;
11878
12145
  }
11879
12146
  }
11880
12147
  async function writeCredentialsStore(store) {
11881
- const path28 = getCredentialsPath();
11882
- await mkdir(dirname(path28), { recursive: true, mode: 448 });
11883
- await atomicWriteFile(path28, `${JSON.stringify(store, null, 2)}
12148
+ const path30 = getCredentialsPath();
12149
+ await mkdir(dirname(path30), { recursive: true, mode: 448 });
12150
+ await atomicWriteFile(path30, `${JSON.stringify(store, null, 2)}
11884
12151
  `, {
11885
12152
  mode: 384
11886
12153
  });
@@ -11900,16 +12167,16 @@ function resolveApiKey(apiUrl) {
11900
12167
  } catch {
11901
12168
  return null;
11902
12169
  }
11903
- const path28 = getCredentialsPath();
12170
+ const path30 = getCredentialsPath();
11904
12171
  let raw;
11905
12172
  try {
11906
- raw = readFileSync(path28, "utf-8");
12173
+ raw = readFileSync(path30, "utf-8");
11907
12174
  } catch (err) {
11908
12175
  if (!isNotFound(err)) warnUnreadableStoreOnce(err);
11909
12176
  return null;
11910
12177
  }
11911
12178
  try {
11912
- const entry = parseStore(raw, path28)[url];
12179
+ const entry = parseStore(raw, path30)[url];
11913
12180
  if (typeof entry !== "object" || entry === null) return null;
11914
12181
  const token = entry.token;
11915
12182
  return typeof token === "string" && token ? token : null;
@@ -11948,12 +12215,12 @@ var init_credentials = __esm({
11948
12215
  * @param reason - What is wrong with it.
11949
12216
  * @param cause - Underlying parse error, if any.
11950
12217
  */
11951
- constructor(path28, reason, cause) {
12218
+ constructor(path30, reason, cause) {
11952
12219
  super(
11953
- `Credentials file ${path28} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
12220
+ `Credentials file ${path30} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
11954
12221
  {
11955
12222
  cause,
11956
- suggestedAction: `Delete ${path28} and run \`iamu auth login\`, or set IAMU_TOKEN.`
12223
+ suggestedAction: `Delete ${path30} and run \`iamu auth login\`, or set IAMU_TOKEN.`
11957
12224
  }
11958
12225
  );
11959
12226
  }
@@ -12565,16 +12832,16 @@ async function needsWrite(filePath, content) {
12565
12832
  return true;
12566
12833
  }
12567
12834
  }
12568
- function isCliWrittenSkill(path28) {
12835
+ function isCliWrittenSkill(path30) {
12569
12836
  try {
12570
- return /^generatedBy:/m.test(readFileSync3(path28, "utf-8"));
12837
+ return /^generatedBy:/m.test(readFileSync3(path30, "utf-8"));
12571
12838
  } catch {
12572
12839
  return false;
12573
12840
  }
12574
12841
  }
12575
- function isCliWrittenPrompt(path28) {
12842
+ function isCliWrittenPrompt(path30) {
12576
12843
  try {
12577
- const content = readFileSync3(path28, "utf-8");
12844
+ const content = readFileSync3(path30, "utf-8");
12578
12845
  return content.includes("iamu") || /^generatedBy:/m.test(content);
12579
12846
  } catch {
12580
12847
  return false;
@@ -12586,8 +12853,8 @@ function planSkillFiles(hosts, projectRoot, version2) {
12586
12853
  for (const host of hosts) {
12587
12854
  if (pluginProvidesSkills(host, projectRoot)) {
12588
12855
  for (const name of skillTemplates.keys()) {
12589
- const path28 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
12590
- if (isCliWrittenSkill(path28)) files.push({ host, path: path28, content: null });
12856
+ const path30 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
12857
+ if (isCliWrittenSkill(path30)) files.push({ host, path: path30, content: null });
12591
12858
  }
12592
12859
  } else if (host.supportsSkills) {
12593
12860
  const skillsDir = host.getSkillsDir(projectRoot);
@@ -14107,7 +14374,7 @@ import {
14107
14374
  readlinkSync,
14108
14375
  realpathSync as realpathSync2
14109
14376
  } from "fs";
14110
- import path17 from "path";
14377
+ import path18 from "path";
14111
14378
  function git(args, cwd) {
14112
14379
  return execFileSync("git", args, {
14113
14380
  cwd,
@@ -14160,8 +14427,8 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
14160
14427
  const top = git(["rev-parse", "--show-toplevel"], projectRoot).toString("utf8").trim();
14161
14428
  if (!top) return void 0;
14162
14429
  const realTop = realpathSync2(top);
14163
- const realIamu = path17.join(realpathSync2(projectRoot), iamuDirName);
14164
- const iamuRel = path17.relative(realTop, realIamu).split(path17.sep).join("/");
14430
+ const realIamu = path18.join(realpathSync2(projectRoot), iamuDirName);
14431
+ const iamuRel = path18.relative(realTop, realIamu).split(path18.sep).join("/");
14165
14432
  const pathspec = iamuRel.startsWith("..") ? [":(top)"] : [":(top)", `:(top,exclude,literal)${iamuRel}`];
14166
14433
  const status2 = git(
14167
14434
  [
@@ -14191,7 +14458,7 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
14191
14458
  );
14192
14459
  for (const file of sorted.slice(0, MAX_UNTRACKED_HASHED)) {
14193
14460
  const rel = file.replace(/\/$/, "");
14194
- hash.update(`${file}\0${hashFile(path17.join(realTop, rel))}
14461
+ hash.update(`${file}\0${hashFile(path18.join(realTop, rel))}
14195
14462
  `);
14196
14463
  }
14197
14464
  if (sorted.length > MAX_UNTRACKED_HASHED) {
@@ -14364,7 +14631,7 @@ __export(start_run_exports, {
14364
14631
  resolveStartUrl: () => resolveStartUrl,
14365
14632
  startRun: () => startRun
14366
14633
  });
14367
- import path18 from "path";
14634
+ import path19 from "path";
14368
14635
  import chalk11 from "chalk";
14369
14636
  function resolveStartUrl(target, scenario) {
14370
14637
  const base = target ?? scenario.target_url ?? null;
@@ -14540,7 +14807,7 @@ async function startRun(scenarioSlug, opts) {
14540
14807
  ...viewport ? { viewport } : {}
14541
14808
  };
14542
14809
  await atomicWriteFile(
14543
- path18.join(run.absPath, "walkthrough.json"),
14810
+ path19.join(run.absPath, "walkthrough.json"),
14544
14811
  JSON.stringify(manifest, null, 2) + "\n"
14545
14812
  );
14546
14813
  if (opts.json) {
@@ -14548,7 +14815,7 @@ async function startRun(scenarioSlug, opts) {
14548
14815
  JSON.stringify(
14549
14816
  {
14550
14817
  run_id: run.runId,
14551
- path: path18.relative(projectRoot, run.absPath),
14818
+ path: path19.relative(projectRoot, run.absPath),
14552
14819
  scenario_slug: scenarioSlug,
14553
14820
  target_url: start2.url,
14554
14821
  target_reachable: targetReachable,
@@ -14596,7 +14863,7 @@ async function startRun(scenarioSlug, opts) {
14596
14863
  );
14597
14864
  }
14598
14865
  console.log(
14599
- ` ${chalk11.dim("path:")} ${path18.relative(projectRoot, run.absPath)}`
14866
+ ` ${chalk11.dim("path:")} ${path19.relative(projectRoot, run.absPath)}`
14600
14867
  );
14601
14868
  console.log();
14602
14869
  console.log(` ${chalk11.bold("Next steps for blind execution:")}`);
@@ -14637,8 +14904,8 @@ var report_exports = {};
14637
14904
  __export(report_exports, {
14638
14905
  report: () => report
14639
14906
  });
14640
- import { promises as fs23 } from "fs";
14641
- import path19 from "path";
14907
+ import { promises as fs24 } from "fs";
14908
+ import path20 from "path";
14642
14909
  import chalk12 from "chalk";
14643
14910
  async function report(scenario, opts) {
14644
14911
  const ctx = await resolveProject(opts);
@@ -14657,8 +14924,8 @@ async function report(scenario, opts) {
14657
14924
  for (const rid of runs.reverse()) {
14658
14925
  try {
14659
14926
  const dir = runDir(projectRoot, rid);
14660
- const mRaw = await fs23.readFile(
14661
- path19.join(dir, "walkthrough.json"),
14927
+ const mRaw = await fs24.readFile(
14928
+ path20.join(dir, "walkthrough.json"),
14662
14929
  "utf8"
14663
14930
  );
14664
14931
  const manifest = JSON.parse(mRaw);
@@ -14666,8 +14933,8 @@ async function report(scenario, opts) {
14666
14933
  if (scenario && manifest.scenario_slug !== scenario) continue;
14667
14934
  let canonical = null;
14668
14935
  try {
14669
- const cRaw = await fs23.readFile(
14670
- path19.join(dir, "canonical.json"),
14936
+ const cRaw = await fs24.readFile(
14937
+ path20.join(dir, "canonical.json"),
14671
14938
  "utf8"
14672
14939
  );
14673
14940
  canonical = JSON.parse(cRaw);
@@ -15944,6 +16211,35 @@ async function init(opts) {
15944
16211
  ""
15945
16212
  )
15946
16213
  );
16214
+ const testabilityPath = join5(iamuDir2, "testability.md");
16215
+ if (!existsSync7(testabilityPath)) {
16216
+ await writeFile3(
16217
+ testabilityPath,
16218
+ serializeFrontmatter(
16219
+ {
16220
+ status: "draft",
16221
+ target_url: "http://localhost:3000"
16222
+ },
16223
+ `# Project Testability & Environment
16224
+
16225
+ Target URL and execution harness for walkthroughs.
16226
+ Audited and populated during /iamu:init.
16227
+
16228
+ ## Target & Run Instructions
16229
+ - Default URL: http://localhost:3000
16230
+
16231
+ ## Reachable Surfaces
16232
+ (Run /iamu:init to discover testable routes)
16233
+
16234
+ ## Mocks & Data Fixtures
16235
+ (Run /iamu:init to inspect existing harnesses)
16236
+
16237
+ ## Untestable Boundaries
16238
+ (None documented yet)
16239
+ `
16240
+ )
16241
+ );
16242
+ }
15947
16243
  }
15948
16244
  const promptTemplates = loadPromptTemplates();
15949
16245
  const promptsDir = join5(iamuDir2, ".prompts");
@@ -16553,7 +16849,7 @@ var init_browse_driver = __esm({
16553
16849
 
16554
16850
  // src/commands/step-numbering.ts
16555
16851
  import { open, readdir, unlink } from "fs/promises";
16556
- import path20 from "path";
16852
+ import path21 from "path";
16557
16853
  function stepFileName(stepNumber2) {
16558
16854
  return `${String(stepNumber2).padStart(3, "0")}.json`;
16559
16855
  }
@@ -16575,7 +16871,7 @@ async function reserveStepFile(stepsDir, limit) {
16575
16871
  if (candidate > limit.maxSteps) {
16576
16872
  throw new Error(stepLimitMessage(limit.maxSteps, limit.runId));
16577
16873
  }
16578
- const stepPath = path20.join(stepsDir, stepFileName(candidate));
16874
+ const stepPath = path21.join(stepsDir, stepFileName(candidate));
16579
16875
  try {
16580
16876
  const handle = await open(stepPath, "wx");
16581
16877
  await handle.close();
@@ -16607,7 +16903,7 @@ __export(step_exports, {
16607
16903
  stepCommand: () => stepCommand
16608
16904
  });
16609
16905
  import { copyFile, mkdir as mkdir5, stat } from "fs/promises";
16610
- import path21 from "path";
16906
+ import path22 from "path";
16611
16907
  import chalk19 from "chalk";
16612
16908
  function parseMaxSteps(val) {
16613
16909
  if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
@@ -16626,8 +16922,8 @@ async function recordStep(input) {
16626
16922
  if (!runDirPath) {
16627
16923
  throw new Error(`Run directory not found for run "${input.runId}"`);
16628
16924
  }
16629
- const stepsDir = path21.join(runDirPath, "steps");
16630
- const screenshotsDir = path21.join(runDirPath, "screenshots");
16925
+ const stepsDir = path22.join(runDirPath, "steps");
16926
+ const screenshotsDir = path22.join(runDirPath, "screenshots");
16631
16927
  await mkdir5(stepsDir, { recursive: true });
16632
16928
  await mkdir5(screenshotsDir, { recursive: true });
16633
16929
  const shotSource = input.shot ? await existingShot(input.shot) : null;
@@ -16641,11 +16937,11 @@ async function recordStep(input) {
16641
16937
  let finalScreenshotPath = null;
16642
16938
  if (shotSource) {
16643
16939
  const destName = `step-${stepNumber2}.png`;
16644
- finalScreenshotPath = path21.join(screenshotsDir, destName);
16645
- if (path21.resolve(finalScreenshotPath) !== shotSource) {
16940
+ finalScreenshotPath = path22.join(screenshotsDir, destName);
16941
+ if (path22.resolve(finalScreenshotPath) !== shotSource) {
16646
16942
  await copyFile(shotSource, finalScreenshotPath);
16647
16943
  }
16648
- screenshotRef = path21.join("screenshots", destName);
16944
+ screenshotRef = path22.join("screenshots", destName);
16649
16945
  }
16650
16946
  const parsedStep = runStepSchema.parse({
16651
16947
  step_number: stepNumber2,
@@ -16670,7 +16966,7 @@ async function recordStep(input) {
16670
16966
  }
16671
16967
  }
16672
16968
  async function existingShot(shot) {
16673
- const resolved = path21.resolve(shot);
16969
+ const resolved = path22.resolve(shot);
16674
16970
  const isFile = await stat(resolved).then((s) => s.isFile()).catch(() => false);
16675
16971
  if (!isFile) {
16676
16972
  throw new Error(
@@ -16712,7 +17008,7 @@ async function stepCommand(runArg, opts) {
16712
17008
  JSON.stringify(
16713
17009
  {
16714
17010
  step_number: res.step.step_number,
16715
- path: path21.relative(projectRoot, res.stepPath),
17011
+ path: path22.relative(projectRoot, res.stepPath),
16716
17012
  action: res.step.action,
16717
17013
  note: res.step.note,
16718
17014
  url: res.step.url,
@@ -16739,7 +17035,7 @@ async function stepCommand(runArg, opts) {
16739
17035
  console.log(` ${chalk19.dim("shot:")} ${res.step.screenshot}`);
16740
17036
  }
16741
17037
  console.log(
16742
- ` ${chalk19.dim("file:")} ${path21.relative(projectRoot, res.stepPath)}`
17038
+ ` ${chalk19.dim("file:")} ${path22.relative(projectRoot, res.stepPath)}`
16743
17039
  );
16744
17040
  console.log();
16745
17041
  } catch (err) {
@@ -16782,21 +17078,21 @@ import {
16782
17078
  } from "fs";
16783
17079
  import { rm as rm2 } from "fs/promises";
16784
17080
  import { homedir as homedir3 } from "os";
16785
- import path22 from "path";
17081
+ import path23 from "path";
16786
17082
  import chalk20 from "chalk";
16787
17083
  function stateDir() {
16788
- return path22.join(
16789
- process.env.IAMU_HOME ?? path22.join(homedir3(), ".iamu"),
17084
+ return path23.join(
17085
+ process.env.IAMU_HOME ?? path23.join(homedir3(), ".iamu"),
16790
17086
  "browse"
16791
17087
  );
16792
17088
  }
16793
17089
  function profileDir() {
16794
- return path22.join(stateDir(), "profile");
17090
+ return path23.join(stateDir(), "profile");
16795
17091
  }
16796
17092
  function readBrowseState() {
16797
17093
  try {
16798
17094
  return JSON.parse(
16799
- readFileSync6(path22.join(stateDir(), "state.json"), "utf8")
17095
+ readFileSync6(path23.join(stateDir(), "state.json"), "utf8")
16800
17096
  );
16801
17097
  } catch {
16802
17098
  return null;
@@ -16804,7 +17100,7 @@ function readBrowseState() {
16804
17100
  }
16805
17101
  function writeState(state) {
16806
17102
  mkdirSync(stateDir(), { recursive: true });
16807
- writeFileSync(path22.join(stateDir(), "state.json"), JSON.stringify(state));
17103
+ writeFileSync(path23.join(stateDir(), "state.json"), JSON.stringify(state));
16808
17104
  }
16809
17105
  function fail(message, opts) {
16810
17106
  if (opts?.json) {
@@ -16867,7 +17163,7 @@ async function requestedViewport(opts) {
16867
17163
  let manifest;
16868
17164
  try {
16869
17165
  manifest = JSON.parse(
16870
- readFileSync6(path22.join(dir, "walkthrough.json"), "utf8")
17166
+ readFileSync6(path23.join(dir, "walkthrough.json"), "utf8")
16871
17167
  );
16872
17168
  } catch {
16873
17169
  fail(
@@ -16973,7 +17269,7 @@ async function stop(opts, driver) {
16973
17269
  await driver.closeBrowser(state.port);
16974
17270
  if (state.pid) await reapBrowser(state.pid, profileDir(), driver);
16975
17271
  try {
16976
- unlinkSync(path22.join(stateDir(), "state.json"));
17272
+ unlinkSync(path23.join(stateDir(), "state.json"));
16977
17273
  } catch {
16978
17274
  }
16979
17275
  report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
@@ -17039,8 +17335,8 @@ async function withPage(opts, driver, fn, target) {
17039
17335
  let screenshot = null;
17040
17336
  if (opts.shot) {
17041
17337
  await waitForVisualStability(page);
17042
- screenshot = path22.resolve(opts.shot);
17043
- mkdirSync(path22.dirname(screenshot), { recursive: true });
17338
+ screenshot = path23.resolve(opts.shot);
17339
+ mkdirSync(path23.dirname(screenshot), { recursive: true });
17044
17340
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
17045
17341
  }
17046
17342
  return { url: page.url(), title: await page.title(), screenshot };
@@ -17119,7 +17415,7 @@ function printStep(opts, record, projectRoot) {
17119
17415
  if (opts.json) {
17120
17416
  const doc = {
17121
17417
  step_number: step.step_number,
17122
- path: path22.relative(projectRoot, record.stepPath),
17418
+ path: path23.relative(projectRoot, record.stepPath),
17123
17419
  action: step.action,
17124
17420
  note: step.note,
17125
17421
  url: step.url,
@@ -17193,10 +17489,10 @@ async function browseStep(args, opts, driver) {
17193
17489
  const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
17194
17490
  if (!runDirPath) return;
17195
17491
  const maxSteps = parseMaxSteps(opts.maxSteps);
17196
- if (await peekNextStepNumber(path22.join(runDirPath, "steps")) > maxSteps) {
17492
+ if (await peekNextStepNumber(path23.join(runDirPath, "steps")) > maxSteps) {
17197
17493
  return fail(stepLimitMessage(maxSteps, opts.run), opts);
17198
17494
  }
17199
- const pendingShot = path22.join(
17495
+ const pendingShot = path23.join(
17200
17496
  runDirPath,
17201
17497
  "screenshots",
17202
17498
  `.pending-${randomUUID7()}.png`
@@ -17267,11 +17563,14 @@ var doctor_exports = {};
17267
17563
  __export(doctor_exports, {
17268
17564
  checkCdpResponsive: () => checkCdpResponsive,
17269
17565
  checkPortAvailable: () => checkPortAvailable,
17566
+ checkProjectHarnesses: () => checkProjectHarnesses,
17567
+ checkProjectTestabilityContract: () => checkProjectTestabilityContract,
17568
+ checkTargetHealth: () => checkTargetHealth,
17270
17569
  doctor: () => doctor,
17271
17570
  runDoctorChecks: () => runDoctorChecks
17272
17571
  });
17273
17572
  import { createServer } from "net";
17274
- import { existsSync as existsSync10 } from "fs";
17573
+ import { existsSync as existsSync10, promises as fs25 } from "fs";
17275
17574
  import { join as join7 } from "path";
17276
17575
  import chalk21 from "chalk";
17277
17576
  function checkPortAvailable(port) {
@@ -17549,11 +17848,186 @@ async function checkAgentIntegrations(projectRoot) {
17549
17848
  }
17550
17849
  return checks;
17551
17850
  }
17552
- async function runDoctorChecks(projectRoot) {
17851
+ async function checkTargetHealth(targetUrl) {
17852
+ const checks = [];
17853
+ let parsedUrl;
17854
+ try {
17855
+ parsedUrl = new URL(targetUrl);
17856
+ } catch {
17857
+ checks.push({
17858
+ category: "target",
17859
+ name: "Target URL Format",
17860
+ status: "error",
17861
+ message: `Invalid target URL: "${targetUrl}"`,
17862
+ remedy: "Provide a valid URL with protocol (e.g. http://localhost:3000)."
17863
+ });
17864
+ return checks;
17865
+ }
17866
+ try {
17867
+ const res = await fetch(targetUrl, {
17868
+ method: "GET",
17869
+ redirect: "manual",
17870
+ signal: AbortSignal.timeout(3e3)
17871
+ });
17872
+ if ([301, 302, 303, 307, 308].includes(res.status)) {
17873
+ const location = res.headers.get("location") || "";
17874
+ const isExternalAuth = /auth0\.com|okta\.com|clerk\.com|accounts\.google\.com|cognito|login\.microsoftonline\.com/i.test(
17875
+ location
17876
+ ) || location.includes("/oauth");
17877
+ if (isExternalAuth) {
17878
+ let redirectHost = location;
17879
+ try {
17880
+ redirectHost = new URL(location, targetUrl).host;
17881
+ } catch {
17882
+ }
17883
+ checks.push({
17884
+ category: "target",
17885
+ name: "Authentication Boundary",
17886
+ status: "warn",
17887
+ message: `Target immediately redirects to external auth (${redirectHost})`,
17888
+ remedy: "Configure a dev auth bypass or prepare for interactive login handshake before walking."
17889
+ });
17890
+ return checks;
17891
+ }
17892
+ checks.push({
17893
+ category: "target",
17894
+ name: "Target Reachability",
17895
+ status: "ok",
17896
+ message: `HTTP ${res.status} redirect -> ${location}`
17897
+ });
17898
+ return checks;
17899
+ }
17900
+ if (res.status >= 500) {
17901
+ checks.push({
17902
+ category: "target",
17903
+ name: "Target Health",
17904
+ status: "error",
17905
+ message: `Target returned HTTP ${res.status} ${res.statusText || "Server Error"}`,
17906
+ remedy: "Check dev server logs for unhandled exceptions or missing environment variables."
17907
+ });
17908
+ return checks;
17909
+ }
17910
+ checks.push({
17911
+ category: "target",
17912
+ name: "Target Reachability",
17913
+ status: "ok",
17914
+ message: `HTTP ${res.status} OK (${parsedUrl.origin})`
17915
+ });
17916
+ } catch (err) {
17917
+ const message = err.message;
17918
+ checks.push({
17919
+ category: "target",
17920
+ name: "Target Reachability",
17921
+ status: "warn",
17922
+ message: `Cannot connect to ${targetUrl} (${message})`,
17923
+ remedy: "Ensure your dev server is running and listening on the specified port."
17924
+ });
17925
+ }
17926
+ return checks;
17927
+ }
17928
+ async function checkProjectHarnesses(projectRoot) {
17929
+ const checks = [];
17930
+ const pkgPath = join7(projectRoot, "package.json");
17931
+ if (!existsSync10(pkgPath)) return checks;
17932
+ try {
17933
+ const raw = await fs25.readFile(pkgPath, "utf8");
17934
+ const pkg = JSON.parse(raw);
17935
+ const scripts = pkg.scripts || {};
17936
+ const allDeps = {
17937
+ ...pkg.dependencies || {},
17938
+ ...pkg.devDependencies || {}
17939
+ };
17940
+ const devScripts = ["dev", "start", "dev:mock", "serve"].filter(
17941
+ (s) => s in scripts
17942
+ );
17943
+ if (devScripts.length > 0) {
17944
+ checks.push({
17945
+ category: "target",
17946
+ name: "Dev Server Script",
17947
+ status: "ok",
17948
+ message: `Available scripts: ${devScripts.map((s) => `pnpm ${s}`).join(", ")}`
17949
+ });
17950
+ } else {
17951
+ checks.push({
17952
+ category: "target",
17953
+ name: "Dev Server Script",
17954
+ status: "warn",
17955
+ message: "No standard dev script (dev, start, dev:mock) found in package.json",
17956
+ remedy: "Add a `dev` or `start` script to package.json to boot your app locally."
17957
+ });
17958
+ }
17959
+ if (allDeps.msw || existsSync10(join7(projectRoot, "src/mocks")) || existsSync10(join7(projectRoot, "mocks"))) {
17960
+ checks.push({
17961
+ category: "target",
17962
+ name: "Mock Harness (MSW)",
17963
+ status: "ok",
17964
+ message: "Mock Service Worker detected. API mocks can decouple walks from live backends."
17965
+ });
17966
+ }
17967
+ const seedScript = Object.keys(scripts).find(
17968
+ (s) => /seed|db:seed|fixtures/i.test(s)
17969
+ );
17970
+ if (seedScript) {
17971
+ checks.push({
17972
+ category: "target",
17973
+ name: "Database Seed",
17974
+ status: "ok",
17975
+ message: `Seed script detected (${seedScript}). Preconditions can be reset between runs.`
17976
+ });
17977
+ }
17978
+ } catch {
17979
+ }
17980
+ return checks;
17981
+ }
17982
+ async function checkProjectTestabilityContract(projectRoot) {
17983
+ const checks = [];
17984
+ const testabilityPath = join7(projectRoot, "iamu", "testability.md");
17985
+ if (!existsSync10(testabilityPath)) {
17986
+ checks.push({
17987
+ category: "target",
17988
+ name: "Testability Contract (testability.md)",
17989
+ status: "warn",
17990
+ message: "iamu/testability.md not found",
17991
+ remedy: "Run `/iamu:init` to audit your codebase and record testable routes, auth strategies, and mock harnesses."
17992
+ });
17993
+ return { checks };
17994
+ }
17995
+ try {
17996
+ const raw = await fs25.readFile(testabilityPath, "utf8");
17997
+ const { frontmatter } = parseFrontmatter(raw);
17998
+ const targetUrl = typeof frontmatter.target_url === "string" ? frontmatter.target_url : void 0;
17999
+ const details = [];
18000
+ if (targetUrl) details.push(`Target: ${targetUrl}`);
18001
+ if (frontmatter.dev_command) {
18002
+ details.push(`Command: ${frontmatter.dev_command}`);
18003
+ }
18004
+ if (frontmatter.auth_strategy) {
18005
+ details.push(`Auth: ${frontmatter.auth_strategy}`);
18006
+ }
18007
+ checks.push({
18008
+ category: "target",
18009
+ name: "Testability Contract (testability.md)",
18010
+ status: "ok",
18011
+ message: `Configured (${details.join(" \xB7 ") || "present"})`
18012
+ });
18013
+ return { checks, targetUrl };
18014
+ } catch (err) {
18015
+ checks.push({
18016
+ category: "target",
18017
+ name: "Testability Contract (testability.md)",
18018
+ status: "warn",
18019
+ message: `Unreadable testability contract: ${err.message}`,
18020
+ remedy: "Repair iamu/testability.md frontmatter."
18021
+ });
18022
+ return { checks };
18023
+ }
18024
+ }
18025
+ async function runDoctorChecks(projectRoot, opts) {
17553
18026
  const checks = [
17554
18027
  ...checkRuntimeChecks(),
17555
18028
  ...await checkBrowserChecks()
17556
18029
  ];
18030
+ let effectiveTarget = opts?.target;
17557
18031
  if (!projectRoot || !existsSync10(join7(projectRoot, "iamu"))) {
17558
18032
  checks.push({
17559
18033
  category: "project",
@@ -17573,6 +18047,15 @@ async function runDoctorChecks(projectRoot) {
17573
18047
  checks.push(...await checkProjectConfiguration(projectRoot));
17574
18048
  checks.push(...await checkProjectContextAndEntities(projectRoot));
17575
18049
  checks.push(...await checkAgentIntegrations(projectRoot));
18050
+ checks.push(...await checkProjectHarnesses(projectRoot));
18051
+ const testability = await checkProjectTestabilityContract(projectRoot);
18052
+ checks.push(...testability.checks);
18053
+ if (!effectiveTarget && testability.targetUrl) {
18054
+ effectiveTarget = testability.targetUrl;
18055
+ }
18056
+ }
18057
+ if (effectiveTarget) {
18058
+ checks.push(...await checkTargetHealth(effectiveTarget));
17576
18059
  }
17577
18060
  const okCount = checks.filter((c) => c.status === "ok").length;
17578
18061
  const warnCount = checks.filter((c) => c.status === "warn").length;
@@ -17589,7 +18072,7 @@ async function runDoctorChecks(projectRoot) {
17589
18072
  }
17590
18073
  async function doctor(opts) {
17591
18074
  const projectRoot = findProjectRoot(process.cwd()) ?? void 0;
17592
- const result = await runDoctorChecks(projectRoot);
18075
+ const result = await runDoctorChecks(projectRoot, { target: opts?.target });
17593
18076
  if (opts?.json) {
17594
18077
  console.log(JSON.stringify(result, null, 2));
17595
18078
  if (!result.ok) process.exitCode = 1;
@@ -17603,7 +18086,8 @@ async function doctor(opts) {
17603
18086
  { key: "browser", title: "Browser Automation" },
17604
18087
  { key: "project", title: "Project Configuration" },
17605
18088
  { key: "agent", title: "Agent Integrations" },
17606
- { key: "cloud", title: "Cloud & Authentication" }
18089
+ { key: "cloud", title: "Cloud & Authentication" },
18090
+ { key: "target", title: "Target & Testability" }
17607
18091
  ];
17608
18092
  for (const cat of categories) {
17609
18093
  const catChecks = result.checks.filter((c) => c.category === cat.key);
@@ -18081,13 +18565,13 @@ __export(validate_exports, {
18081
18565
  validate: () => validate
18082
18566
  });
18083
18567
  import { randomUUID as randomUUID8 } from "crypto";
18084
- import { promises as fs24 } from "fs";
18085
- import path23 from "path";
18568
+ import { promises as fs26 } from "fs";
18569
+ import path24 from "path";
18086
18570
  import chalk23 from "chalk";
18087
18571
  function inferTypeFromPath(filePath, projectRoot) {
18088
- const rel = path23.relative(path23.join(projectRoot, "iamu"), filePath);
18089
- if (rel.startsWith("..") || path23.isAbsolute(rel)) return null;
18090
- const segments = rel.split(path23.sep);
18572
+ const rel = path24.relative(path24.join(projectRoot, "iamu"), filePath);
18573
+ if (rel.startsWith("..") || path24.isAbsolute(rel)) return null;
18574
+ const segments = rel.split(path24.sep);
18091
18575
  if (segments.length < 2) return null;
18092
18576
  return TYPE_BY_ALIAS.get(segments[0]) ?? null;
18093
18577
  }
@@ -18117,7 +18601,7 @@ function validateFile(filePath, raw, projectRoot) {
18117
18601
  );
18118
18602
  return result;
18119
18603
  }
18120
- if (path23.basename(filePath) === "context.yml") {
18604
+ if (path24.basename(filePath) === "context.yml") {
18121
18605
  applyZodValidation(contextFrontmatterSchema, frontmatter, result);
18122
18606
  const warnings = [];
18123
18607
  const hasNotes = Boolean(
@@ -18177,17 +18661,17 @@ async function buildRepairLookups(projectRoot) {
18177
18661
  }
18178
18662
  }
18179
18663
  const runScenario = /* @__PURE__ */ new Map();
18180
- const runsDir2 = path23.join(projectRoot, "iamu", ".runs");
18664
+ const runsDir2 = path24.join(projectRoot, "iamu", ".runs");
18181
18665
  let runIds = [];
18182
18666
  try {
18183
- runIds = await fs24.readdir(runsDir2);
18667
+ runIds = await fs26.readdir(runsDir2);
18184
18668
  } catch {
18185
18669
  }
18186
18670
  for (const runId of runIds) {
18187
18671
  try {
18188
18672
  const manifest = JSON.parse(
18189
- await fs24.readFile(
18190
- path23.join(runsDir2, runId, "walkthrough.json"),
18673
+ await fs26.readFile(
18674
+ path24.join(runsDir2, runId, "walkthrough.json"),
18191
18675
  "utf8"
18192
18676
  )
18193
18677
  );
@@ -18209,7 +18693,7 @@ async function fixFindings(projectRoot, results, dryRun) {
18209
18693
  let raw;
18210
18694
  let parsed;
18211
18695
  try {
18212
- raw = await fs24.readFile(result.path, "utf8");
18696
+ raw = await fs26.readFile(result.path, "utf8");
18213
18697
  parsed = parseFrontmatter(raw);
18214
18698
  } catch {
18215
18699
  continue;
@@ -18218,7 +18702,7 @@ async function fixFindings(projectRoot, results, dryRun) {
18218
18702
  parsed.frontmatter,
18219
18703
  {
18220
18704
  ...lookups,
18221
- fileSlug: path23.basename(result.path, ".md"),
18705
+ fileSlug: path24.basename(result.path, ".md"),
18222
18706
  newId: randomUUID8,
18223
18707
  body: parsed.body
18224
18708
  }
@@ -18253,9 +18737,9 @@ async function validate(filePath, opts) {
18253
18737
  if (process.exitCode === 1) return;
18254
18738
  const results = [];
18255
18739
  if (filePath) {
18256
- const abs = path23.isAbsolute(filePath) ? filePath : path23.resolve(process.cwd(), filePath);
18740
+ const abs = path24.isAbsolute(filePath) ? filePath : path24.resolve(process.cwd(), filePath);
18257
18741
  try {
18258
- const raw = await fs24.readFile(abs, "utf8");
18742
+ const raw = await fs26.readFile(abs, "utf8");
18259
18743
  results.push(validateFile(abs, raw, projectRoot));
18260
18744
  } catch (err) {
18261
18745
  commandError(
@@ -18265,24 +18749,24 @@ async function validate(filePath, opts) {
18265
18749
  return;
18266
18750
  }
18267
18751
  } else if (opts.run) {
18268
- const findingsDir = path23.join(projectRoot, "iamu", ENTITY_DIRS.finding);
18752
+ const findingsDir = path24.join(projectRoot, "iamu", ENTITY_DIRS.finding);
18269
18753
  let names = [];
18270
18754
  try {
18271
- names = await fs24.readdir(findingsDir);
18755
+ names = await fs26.readdir(findingsDir);
18272
18756
  } catch {
18273
18757
  }
18274
18758
  for (const name of names) {
18275
18759
  if (!name.endsWith(".md")) continue;
18276
- const abs = path23.join(findingsDir, name);
18760
+ const abs = path24.join(findingsDir, name);
18277
18761
  try {
18278
- const raw = await fs24.readFile(abs, "utf8");
18762
+ const raw = await fs26.readFile(abs, "utf8");
18279
18763
  const { frontmatter } = parseFrontmatter(raw);
18280
18764
  if (frontmatter.run === opts.run) {
18281
18765
  results.push(validateFile(abs, raw, projectRoot));
18282
18766
  }
18283
18767
  } catch {
18284
18768
  results.push({
18285
- path: path23.relative(projectRoot, abs),
18769
+ path: path24.relative(projectRoot, abs),
18286
18770
  valid: false,
18287
18771
  errors: ["Cannot read file or parse frontmatter"]
18288
18772
  });
@@ -18315,31 +18799,31 @@ async function validate(filePath, opts) {
18315
18799
  } else {
18316
18800
  const typesToCheck = typeFilter ? ENTITY_TYPES.filter((t) => typeFilter.has(t)) : ENTITY_TYPES;
18317
18801
  for (const type2 of typesToCheck) {
18318
- const iamuTypeDir = path23.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
18802
+ const iamuTypeDir = path24.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
18319
18803
  let names;
18320
18804
  try {
18321
- names = await fs24.readdir(iamuTypeDir);
18805
+ names = await fs26.readdir(iamuTypeDir);
18322
18806
  } catch {
18323
18807
  continue;
18324
18808
  }
18325
18809
  for (const name of names) {
18326
18810
  if (!name.endsWith(".md")) continue;
18327
- const abs = path23.join(iamuTypeDir, name);
18811
+ const abs = path24.join(iamuTypeDir, name);
18328
18812
  try {
18329
- const raw = await fs24.readFile(abs, "utf8");
18813
+ const raw = await fs26.readFile(abs, "utf8");
18330
18814
  results.push(validateFile(abs, raw, projectRoot));
18331
18815
  } catch {
18332
18816
  results.push({
18333
- path: path23.relative(projectRoot, abs),
18817
+ path: path24.relative(projectRoot, abs),
18334
18818
  valid: false,
18335
18819
  errors: ["Cannot read file"]
18336
18820
  });
18337
18821
  }
18338
18822
  }
18339
18823
  }
18340
- const ctxPath = path23.join(projectRoot, "iamu", "context.yml");
18824
+ const ctxPath = path24.join(projectRoot, "iamu", "context.yml");
18341
18825
  try {
18342
- const raw = await fs24.readFile(ctxPath, "utf8");
18826
+ const raw = await fs26.readFile(ctxPath, "utf8");
18343
18827
  results.push(validateFile(ctxPath, raw, projectRoot));
18344
18828
  } catch {
18345
18829
  }
@@ -18358,19 +18842,19 @@ async function validate(filePath, opts) {
18358
18842
  valid,
18359
18843
  errors: results.filter((r) => !r.valid).flatMap(
18360
18844
  (r) => r.errors.map((e) => ({
18361
- path: path23.relative(projectRoot, r.path),
18845
+ path: path24.relative(projectRoot, r.path),
18362
18846
  message: e
18363
18847
  }))
18364
18848
  ),
18365
18849
  warnings: results.filter((r) => r.warnings && r.warnings.length > 0).flatMap(
18366
18850
  (r) => (r.warnings ?? []).map((w) => ({
18367
- path: path23.relative(projectRoot, r.path),
18851
+ path: path24.relative(projectRoot, r.path),
18368
18852
  message: w
18369
18853
  }))
18370
18854
  ),
18371
18855
  ...opts.fix ? {
18372
18856
  repaired: repaired.map((r) => ({
18373
- path: path23.relative(projectRoot, r.path),
18857
+ path: path24.relative(projectRoot, r.path),
18374
18858
  changes: r.repairs,
18375
18859
  valid: r.valid
18376
18860
  }))
@@ -18392,7 +18876,7 @@ async function validate(filePath, opts) {
18392
18876
  }
18393
18877
  let hasErrors = false;
18394
18878
  for (const r of results) {
18395
- const relPath = path23.relative(projectRoot, r.path);
18879
+ const relPath = path24.relative(projectRoot, r.path);
18396
18880
  if (r.valid) {
18397
18881
  if (r.warnings && r.warnings.length > 0) {
18398
18882
  console.log(` ${symbols.warning} ` + chalk23.yellow(relPath));
@@ -18706,6 +19190,797 @@ var init_new_entity = __esm({
18706
19190
  }
18707
19191
  });
18708
19192
 
19193
+ // src/commands/explore-crawl.ts
19194
+ function candidateSig(c) {
19195
+ return `${c.role}
19196
+ ${c.name}
19197
+ ${c.occurrence}`;
19198
+ }
19199
+ function originOf(url) {
19200
+ try {
19201
+ return new URL(url).origin;
19202
+ } catch {
19203
+ return null;
19204
+ }
19205
+ }
19206
+ function skipReason(c, origin, deny) {
19207
+ if (deny.some((re) => re.test(c.name) || c.href && re.test(c.href))) {
19208
+ return "denied";
19209
+ }
19210
+ if (c.submit) return "submit";
19211
+ if (UNSAFE_NAME.test(c.name)) return "unsafe_name";
19212
+ if (c.href) {
19213
+ let u = null;
19214
+ try {
19215
+ u = new URL(c.href);
19216
+ } catch {
19217
+ }
19218
+ if (u && u.protocol !== "javascript:") {
19219
+ if (u.protocol !== "http:" && u.protocol !== "https:") return "external";
19220
+ if (u.origin !== origin) return "external";
19221
+ if (UNSAFE_PATH.test(u.pathname)) return "unsafe_name";
19222
+ }
19223
+ if (c.download) return "download";
19224
+ }
19225
+ return null;
19226
+ }
19227
+ async function settle(page, settleMs) {
19228
+ await page.waitForLoadState("domcontentloaded", { timeout: NAV_TIMEOUT_MS }).catch(() => {
19229
+ });
19230
+ await page.waitForLoadState("networkidle", { timeout: settleMs }).catch(() => {
19231
+ });
19232
+ }
19233
+ async function readPage(page) {
19234
+ return page.evaluate(READ_PAGE);
19235
+ }
19236
+ async function markDocument(page) {
19237
+ await page.evaluate("window.__iamuDoc = true").catch(() => {
19238
+ });
19239
+ }
19240
+ async function isSameDocument(page) {
19241
+ return page.evaluate("window.__iamuDoc === true").catch(() => false);
19242
+ }
19243
+ async function clickCandidate(page, want) {
19244
+ const state = await readPage(page);
19245
+ const c = state.candidates.find(
19246
+ (x) => candidateSig(x) === candidateSig(want)
19247
+ );
19248
+ if (!c) return false;
19249
+ await page.locator(`[data-iamu-x="${c.index}"]`).click({ timeout: CLICK_TIMEOUT_MS });
19250
+ return true;
19251
+ }
19252
+ async function crawl(page, opts) {
19253
+ const origin = originOf(opts.startUrl);
19254
+ if (!origin) throw new Error(`Not a URL: ${opts.startUrl}`);
19255
+ let blocked = [];
19256
+ await page.route("**/*", (route) => {
19257
+ const req = route.request();
19258
+ if (SAFE_METHODS.has(req.method())) return route.continue();
19259
+ let where = req.url();
19260
+ try {
19261
+ const u = new URL(where);
19262
+ where = u.origin === origin ? u.pathname : `${u.origin}${u.pathname}`;
19263
+ } catch {
19264
+ }
19265
+ blocked.push(`${req.method()} ${where}`);
19266
+ return route.abort("blockedbyclient");
19267
+ });
19268
+ const onDialog = (d) => {
19269
+ d.dismiss().catch(() => {
19270
+ });
19271
+ };
19272
+ const onPopup = (p) => {
19273
+ if (p !== page) p.close().catch(() => {
19274
+ });
19275
+ };
19276
+ page.on("dialog", onDialog);
19277
+ page.context().on("page", onPopup);
19278
+ try {
19279
+ return await crawlFrom(page, opts, origin, () => {
19280
+ const out = blocked;
19281
+ blocked = [];
19282
+ return out;
19283
+ });
19284
+ } finally {
19285
+ page.off("dialog", onDialog);
19286
+ page.context().off("page", onPopup);
19287
+ await page.unroute("**/*").catch(() => {
19288
+ });
19289
+ }
19290
+ }
19291
+ async function crawlFrom(page, opts, origin, takeBlocked) {
19292
+ const deny = opts.deny ?? [];
19293
+ const settleMs = opts.settleMs ?? 1500;
19294
+ const { budget } = opts;
19295
+ const stats = {
19296
+ surfaces: 0,
19297
+ actions_tried: 0,
19298
+ skipped: 0,
19299
+ blocked_requests: 0,
19300
+ errors: 0,
19301
+ truncated: false
19302
+ };
19303
+ const surfaces = [];
19304
+ const byKey = /* @__PURE__ */ new Map();
19305
+ const byPlaceView = /* @__PURE__ */ new Map();
19306
+ const signatureOf = (s) => exploreSignature(s.headings, s.title);
19307
+ function addSurface(state, place, view, path30, depth) {
19308
+ const key = exploreSurfaceKey(place, view);
19309
+ const surface = {
19310
+ key,
19311
+ url: state.url,
19312
+ place,
19313
+ view,
19314
+ signature: signatureOf(state),
19315
+ label: state.headings[0] ?? (state.title || place),
19316
+ headings: state.headings,
19317
+ fields: state.fields,
19318
+ depth,
19319
+ path: path30,
19320
+ actions: []
19321
+ };
19322
+ surfaces.push(surface);
19323
+ byKey.set(key, surface);
19324
+ byPlaceView.set(`${place}
19325
+ ${surface.signature}`, key);
19326
+ stats.surfaces++;
19327
+ opts.log?.(` ${" ".repeat(depth)}${key} ${surface.label}`);
19328
+ return surface;
19329
+ }
19330
+ async function goto(url) {
19331
+ await page.goto(url, {
19332
+ waitUntil: "domcontentloaded",
19333
+ timeout: NAV_TIMEOUT_MS
19334
+ });
19335
+ await settle(page, settleMs);
19336
+ }
19337
+ async function replay(path30) {
19338
+ if (!("goto" in (path30[0] ?? {}))) await goto(opts.startUrl);
19339
+ for (const step of path30) {
19340
+ if ("goto" in step) {
19341
+ await goto(step.goto);
19342
+ } else {
19343
+ if (!await clickCandidate(page, step.click)) return false;
19344
+ await settle(page, settleMs);
19345
+ }
19346
+ }
19347
+ return true;
19348
+ }
19349
+ await goto(opts.startUrl);
19350
+ if (await page.evaluate("Boolean(navigator.serviceWorker?.controller)").catch(() => false)) {
19351
+ throw new Error(
19352
+ "A service worker controls this page, and the write guard cannot see its requests. Explore without --attach (service workers are blocked there)."
19353
+ );
19354
+ }
19355
+ const first = await readPage(page);
19356
+ const firstPlace = explorePlaceKey(first.url);
19357
+ if (!firstPlace || originOf(first.url) !== origin) {
19358
+ throw new Error(`The start URL led off the product, to ${first.url}`);
19359
+ }
19360
+ addSurface(first, firstPlace, null, [], 0);
19361
+ for (let i = 0; i < surfaces.length; i++) {
19362
+ const surface = surfaces[i];
19363
+ if (!await replay(surface.path)) {
19364
+ stats.errors++;
19365
+ surface.actions.push({
19366
+ role: "",
19367
+ name: "",
19368
+ occurrence: 0,
19369
+ outcome: { kind: "error", message: LOST }
19370
+ });
19371
+ continue;
19372
+ }
19373
+ const home = await readPage(page);
19374
+ const seen = new Set(home.candidates.map(candidateSig));
19375
+ const work = home.candidates.map((c) => ({ c, via: [] }));
19376
+ const homeList = home.candidates.map(candidateSig).join("\n");
19377
+ let pristine = true;
19378
+ for (let w = 0; w < work.length; w++) {
19379
+ const { c, via } = work[w];
19380
+ const action = {
19381
+ role: c.role,
19382
+ name: c.name,
19383
+ occurrence: c.occurrence,
19384
+ ...c.href ? { href: c.href } : {},
19385
+ ...via.length > 0 ? { via } : {},
19386
+ outcome: { kind: "same" }
19387
+ };
19388
+ surface.actions.push(action);
19389
+ const reason = skipReason(c, origin, deny);
19390
+ if (reason) {
19391
+ action.outcome = { kind: "skipped", reason };
19392
+ stats.skipped++;
19393
+ continue;
19394
+ }
19395
+ if (surface.depth >= budget.max_depth || stats.actions_tried >= budget.max_actions) {
19396
+ action.outcome = { kind: "skipped", reason: "budget" };
19397
+ stats.skipped++;
19398
+ stats.truncated = true;
19399
+ continue;
19400
+ }
19401
+ stats.actions_tried++;
19402
+ takeBlocked();
19403
+ const click = {
19404
+ click: { role: c.role, name: c.name, occurrence: c.occurrence }
19405
+ };
19406
+ let outcome;
19407
+ let revealed = 0;
19408
+ let left = false;
19409
+ let lost = false;
19410
+ try {
19411
+ if (!pristine || via.length > 0) {
19412
+ if (!await replay([...surface.path, ...via])) {
19413
+ throw new Error(LOST);
19414
+ }
19415
+ }
19416
+ pristine = false;
19417
+ const before = await readPage(page);
19418
+ await markDocument(page);
19419
+ if (c.newTab && c.href) {
19420
+ await goto(c.href);
19421
+ } else {
19422
+ if (!await clickCandidate(page, c)) {
19423
+ throw new Error("control is gone");
19424
+ }
19425
+ await settle(page, settleMs);
19426
+ }
19427
+ const after = await readPage(page);
19428
+ if (originOf(after.url) !== origin) {
19429
+ outcome = { kind: "external", url: after.url };
19430
+ left = true;
19431
+ } else {
19432
+ const place = explorePlaceKey(after.url) ?? before.url;
19433
+ const sig = signatureOf(after);
19434
+ const sameDoc = !(c.newTab && c.href) && await isSameDocument(page);
19435
+ const beforePlace = explorePlaceKey(before.url);
19436
+ let key = byPlaceView.get(`${place}
19437
+ ${sig}`);
19438
+ let view = null;
19439
+ if (!key) {
19440
+ const inPage = sameDoc && place === beforePlace;
19441
+ view = inPage ? sig : null;
19442
+ key = exploreSurfaceKey(place, view);
19443
+ if (byKey.has(key)) byPlaceView.set(`${place}
19444
+ ${sig}`, key);
19445
+ }
19446
+ if (key === surface.key) {
19447
+ outcome = { kind: "same" };
19448
+ const list2 = after.candidates.map(candidateSig).join("\n");
19449
+ for (const n of after.candidates) {
19450
+ const s = candidateSig(n);
19451
+ if (seen.has(s)) continue;
19452
+ seen.add(s);
19453
+ work.push({ c: n, via: [...via, click] });
19454
+ revealed++;
19455
+ }
19456
+ if (list2 === homeList) pristine = true;
19457
+ } else {
19458
+ outcome = { kind: "surface", surface: key };
19459
+ if (!byKey.has(key)) {
19460
+ if (stats.surfaces >= budget.max_surfaces) {
19461
+ stats.truncated = true;
19462
+ } else {
19463
+ const path30 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
19464
+ addSurface(after, place, view, path30, surface.depth + 1);
19465
+ }
19466
+ }
19467
+ }
19468
+ }
19469
+ } catch (err) {
19470
+ const msg = err instanceof Error ? err.message : String(err);
19471
+ outcome = { kind: "error", message: msg.split("\n")[0].slice(0, 200) };
19472
+ stats.errors++;
19473
+ lost = msg === LOST;
19474
+ }
19475
+ action.outcome = outcome;
19476
+ if (revealed > 0) action.revealed = revealed;
19477
+ const writes = takeBlocked();
19478
+ if (writes.length > 0) {
19479
+ action.blocked_requests = [...new Set(writes)];
19480
+ stats.blocked_requests += writes.length;
19481
+ }
19482
+ if (left) pristine = false;
19483
+ if (lost) break;
19484
+ }
19485
+ }
19486
+ return { surfaces, stats };
19487
+ }
19488
+ var UNSAFE_NAME, UNSAFE_PATH, SAFE_METHODS, CLICK_TIMEOUT_MS, LOST, NAV_TIMEOUT_MS, READ_PAGE;
19489
+ var init_explore_crawl = __esm({
19490
+ "src/commands/explore-crawl.ts"() {
19491
+ "use strict";
19492
+ init_dist2();
19493
+ UNSAFE_NAME = new RegExp(
19494
+ `(?<![\\p{L}\\p{N}])(${[
19495
+ // English
19496
+ "delete",
19497
+ "remove",
19498
+ "destroy",
19499
+ "erase",
19500
+ "pay",
19501
+ "pay now",
19502
+ "purchase",
19503
+ "buy",
19504
+ "checkout",
19505
+ "place order",
19506
+ "order now",
19507
+ "send",
19508
+ "submit",
19509
+ "save",
19510
+ "confirm",
19511
+ "approve",
19512
+ "reject",
19513
+ "decline",
19514
+ "publish",
19515
+ "archive",
19516
+ "unsubscribe",
19517
+ "log ?out",
19518
+ "sign ?out",
19519
+ "reset",
19520
+ "transfer",
19521
+ "invite",
19522
+ "upload",
19523
+ "import",
19524
+ "deactivate",
19525
+ "close account",
19526
+ // Swedish
19527
+ "ta bort",
19528
+ "radera",
19529
+ "betala",
19530
+ "k\xF6p",
19531
+ "best\xE4ll",
19532
+ "skicka",
19533
+ "skicka in",
19534
+ "spara",
19535
+ "bekr\xE4fta",
19536
+ "godk\xE4nn",
19537
+ "avsl\xE5",
19538
+ "publicera",
19539
+ "arkivera",
19540
+ "logga ut",
19541
+ "\xE5terst\xE4ll",
19542
+ "\xF6verf\xF6r",
19543
+ "bjud in",
19544
+ "ladda upp"
19545
+ ].join("|")})(?![\\p{L}\\p{N}])`,
19546
+ "iu"
19547
+ );
19548
+ UNSAFE_PATH = /(^|\/)(log-?out|sign-?out|logga-?ut|delete|remove|destroy|unsubscribe)(\/|$)/i;
19549
+ SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
19550
+ CLICK_TIMEOUT_MS = 3e3;
19551
+ LOST = "could not return to surface";
19552
+ NAV_TIMEOUT_MS = 15e3;
19553
+ READ_PAGE = String.raw`(() => {
19554
+ const SELECTOR = [
19555
+ "a[href]", "button", "summary",
19556
+ 'input[type="button"]', 'input[type="submit"]',
19557
+ 'input[type="reset"]', 'input[type="image"]',
19558
+ '[role="button"]', '[role="link"]', '[role="tab"]',
19559
+ '[role="menuitem"]', '[role="menuitemradio"]', '[role="menuitemcheckbox"]',
19560
+ ].join(",");
19561
+ const clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
19562
+ const visible = (el) => {
19563
+ if (typeof el.checkVisibility === "function" &&
19564
+ !el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
19565
+ return false;
19566
+ }
19567
+ const r = el.getBoundingClientRect();
19568
+ return r.width > 0 && r.height > 0;
19569
+ };
19570
+ const textOf = (id) => clean(document.getElementById(id)?.innerText);
19571
+ const nameOf = (el) => {
19572
+ const label = clean(el.getAttribute("aria-label"));
19573
+ if (label) return label;
19574
+ const by = el.getAttribute("aria-labelledby");
19575
+ if (by) {
19576
+ const t = clean(by.split(/\s+/).map(textOf).join(" "));
19577
+ if (t) return t;
19578
+ }
19579
+ if (el instanceof HTMLInputElement) {
19580
+ return clean(el.value) || clean(el.alt) || clean(el.title);
19581
+ }
19582
+ const text = clean(el.innerText);
19583
+ if (text) return text.slice(0, 80);
19584
+ const img = el.querySelector("img[alt]");
19585
+ return clean(img?.getAttribute("alt")) || clean(el.title);
19586
+ };
19587
+ const roleOf = (el) =>
19588
+ el.getAttribute("role") || (el.tagName === "A" ? "link" : "button");
19589
+
19590
+ for (const el of document.querySelectorAll("[data-iamu-x]")) {
19591
+ el.removeAttribute("data-iamu-x");
19592
+ }
19593
+ const seen = new Map();
19594
+ const candidates = [];
19595
+ let index = 0;
19596
+ for (const el of document.querySelectorAll(SELECTOR)) {
19597
+ if (!visible(el)) continue;
19598
+ if (el.disabled || el.getAttribute("aria-disabled") === "true") continue;
19599
+ const role = roleOf(el);
19600
+ const name = nameOf(el);
19601
+ const sig = role + "\n" + name;
19602
+ const occurrence = seen.get(sig) ?? 0;
19603
+ seen.set(sig, occurrence + 1);
19604
+ let submit = false;
19605
+ if (el instanceof HTMLButtonElement) {
19606
+ // A button in a form submits unless it says type="button".
19607
+ submit = el.form
19608
+ ? el.type === "submit" || el.type === "reset"
19609
+ : el.getAttribute("type") === "submit";
19610
+ } else if (el instanceof HTMLInputElement) {
19611
+ submit = ["submit", "reset", "image"].includes(el.type);
19612
+ }
19613
+ const anchor = el instanceof HTMLAnchorElement ? el : null;
19614
+ el.setAttribute("data-iamu-x", String(index));
19615
+ candidates.push({
19616
+ index, role, name, occurrence,
19617
+ href: anchor ? anchor.href : null,
19618
+ download: anchor ? anchor.hasAttribute("download") : false,
19619
+ newTab: anchor ? anchor.target === "_blank" : false,
19620
+ submit,
19621
+ });
19622
+ index++;
19623
+ }
19624
+
19625
+ const headings = [];
19626
+ for (const el of document.querySelectorAll(
19627
+ 'h1, h2, [role="heading"][aria-level="1"], [role="heading"][aria-level="2"]',
19628
+ )) {
19629
+ if (!visible(el)) continue;
19630
+ const t = clean(el.innerText);
19631
+ if (t) headings.push(t.slice(0, 120));
19632
+ }
19633
+
19634
+ const fields = [];
19635
+ for (const f of document.querySelectorAll("input, select, textarea")) {
19636
+ if (["hidden", "submit", "button", "reset", "image"].includes(f.type)) continue;
19637
+ if (!visible(f)) continue;
19638
+ const forLabel = f.id
19639
+ ? document.querySelector('label[for="' + CSS.escape(f.id) + '"]')
19640
+ : null;
19641
+ const name =
19642
+ clean(f.getAttribute("aria-label")) ||
19643
+ clean(forLabel?.innerText) ||
19644
+ clean(f.closest("label")?.innerText) ||
19645
+ clean(f.placeholder) ||
19646
+ clean(f.name);
19647
+ if (name && !fields.includes(name)) fields.push(name.slice(0, 80));
19648
+ if (fields.length >= 40) break;
19649
+ }
19650
+
19651
+ return { url: location.href, title: clean(document.title), headings, fields, candidates };
19652
+ })()`;
19653
+ }
19654
+ });
19655
+
19656
+ // src/commands/explore.ts
19657
+ var explore_exports = {};
19658
+ __export(explore_exports, {
19659
+ exploreCoverageCommand: () => exploreCoverageCommand,
19660
+ exploreDiffCommand: () => exploreDiffCommand,
19661
+ exploreList: () => exploreList,
19662
+ exploreRun: () => exploreRun
19663
+ });
19664
+ import { promises as fs27 } from "fs";
19665
+ import path25 from "path";
19666
+ import chalk26 from "chalk";
19667
+ function fail2(message, json2) {
19668
+ if (json2) console.error(JSON.stringify({ error: message }));
19669
+ else console.error(chalk26.red(` ${message}`));
19670
+ process.exitCode = 1;
19671
+ }
19672
+ function positiveInt(raw, fallback, flag) {
19673
+ if (raw === void 0) return fallback;
19674
+ const n = Number(raw);
19675
+ if (!Number.isInteger(n) || n < 1) {
19676
+ throw new Error(`${flag} must be a positive whole number, got "${raw}"`);
19677
+ }
19678
+ return n;
19679
+ }
19680
+ async function testabilityTarget(projectRoot) {
19681
+ try {
19682
+ const raw = await fs27.readFile(
19683
+ path25.join(projectRoot, "iamu", "testability.md"),
19684
+ "utf8"
19685
+ );
19686
+ const { frontmatter } = parseFrontmatter(raw);
19687
+ return typeof frontmatter.target_url === "string" ? frontmatter.target_url : null;
19688
+ } catch {
19689
+ return null;
19690
+ }
19691
+ }
19692
+ async function openPage(opts) {
19693
+ const { chromium } = await import("playwright-core");
19694
+ const viewport = parseViewport(opts.viewport) ?? {
19695
+ width: 1280,
19696
+ height: 800
19697
+ };
19698
+ if (opts.attach) {
19699
+ const state = readBrowseState();
19700
+ if (!state) {
19701
+ throw new Error(
19702
+ "No browser to attach to \u2014 start one with `iamu browse start` and sign in first."
19703
+ );
19704
+ }
19705
+ const browser2 = await chromium.connectOverCDP(
19706
+ `http://127.0.0.1:${state.port}`
19707
+ );
19708
+ const context = browser2.contexts()[0] ?? await browser2.newContext();
19709
+ const page2 = await context.newPage();
19710
+ await page2.setViewportSize(viewport);
19711
+ return {
19712
+ page: page2,
19713
+ close: async () => {
19714
+ await page2.close().catch(() => {
19715
+ });
19716
+ await browser2.close().catch(() => {
19717
+ });
19718
+ }
19719
+ };
19720
+ }
19721
+ const chrome = findChrome();
19722
+ if (!chrome) {
19723
+ throw new Error(
19724
+ "No Chrome or Chromium found \u2014 install one or set IAMU_BROWSER."
19725
+ );
19726
+ }
19727
+ const browser = await chromium.launch({
19728
+ executablePath: chrome,
19729
+ headless: true,
19730
+ chromiumSandbox: !shouldDisableSandbox()
19731
+ });
19732
+ const page = await browser.newPage({ viewport, serviceWorkers: "block" });
19733
+ return { page, close: () => browser.close().catch(() => {
19734
+ }) };
19735
+ }
19736
+ async function exploreRun(url, opts) {
19737
+ const ctx = await resolveProject(opts);
19738
+ if (!ctx) return;
19739
+ const target = url ?? await testabilityTarget(ctx.projectRoot);
19740
+ if (!target) {
19741
+ fail2(
19742
+ "No URL to explore \u2014 pass one (iamu explore http://localhost:3000) or set target_url in iamu/testability.md.",
19743
+ opts.json
19744
+ );
19745
+ return;
19746
+ }
19747
+ let startUrl;
19748
+ let budget;
19749
+ let deny;
19750
+ try {
19751
+ startUrl = new URL(target).href;
19752
+ budget = {
19753
+ max_surfaces: positiveInt(
19754
+ opts.maxSurfaces,
19755
+ DEFAULTS.max_surfaces,
19756
+ "--max-surfaces"
19757
+ ),
19758
+ max_actions: positiveInt(
19759
+ opts.maxActions,
19760
+ DEFAULTS.max_actions,
19761
+ "--max-actions"
19762
+ ),
19763
+ max_depth: positiveInt(opts.maxDepth, DEFAULTS.max_depth, "--max-depth")
19764
+ };
19765
+ deny = (opts.deny ?? []).map((d) => new RegExp(d, "i"));
19766
+ } catch (err) {
19767
+ fail2(err.message, opts.json);
19768
+ return;
19769
+ }
19770
+ const quiet = opts.json || process.env.IAMU_QUIET === "1";
19771
+ if (!quiet) {
19772
+ console.error(chalk26.dim(` Exploring ${startUrl} (read-only)\u2026`));
19773
+ }
19774
+ const now = /* @__PURE__ */ new Date();
19775
+ let opened = null;
19776
+ let result;
19777
+ try {
19778
+ opened = await openPage(opts);
19779
+ result = await crawl(opened.page, {
19780
+ startUrl,
19781
+ budget,
19782
+ deny,
19783
+ log: quiet ? void 0 : (line) => console.error(chalk26.dim(line))
19784
+ });
19785
+ } catch (err) {
19786
+ fail2(err.message.split("\n")[0], opts.json);
19787
+ return;
19788
+ } finally {
19789
+ await opened?.close();
19790
+ }
19791
+ const build = detectBuild({ cwd: ctx.projectRoot });
19792
+ const map2 = {
19793
+ schema: 1,
19794
+ id: composeExploreId(now),
19795
+ created_at: now.toISOString(),
19796
+ target_url: startUrl,
19797
+ ...build ? { build: { ...build } } : {},
19798
+ budget,
19799
+ stats: result.stats,
19800
+ surfaces: result.surfaces
19801
+ };
19802
+ const file = await writeExploreMap(ctx.projectRoot, map2);
19803
+ const rel = path25.relative(process.cwd(), file);
19804
+ if (opts.json) {
19805
+ console.log(JSON.stringify({ id: map2.id, path: rel, stats: map2.stats }));
19806
+ return;
19807
+ }
19808
+ const s = map2.stats;
19809
+ console.log(
19810
+ `
19811
+ ${chalk26.bold(`${s.surfaces} surfaces`)} \xB7 ${s.actions_tried} controls tried \xB7 ${s.skipped} skipped \xB7 ${s.blocked_requests} writes blocked${s.errors ? ` \xB7 ${s.errors} errors` : ""}`
19812
+ );
19813
+ if (s.truncated) {
19814
+ console.log(
19815
+ chalk26.yellow(
19816
+ " Stopped by the budget before the product ran out \u2014 raise --max-surfaces / --max-actions / --max-depth to go further."
19817
+ )
19818
+ );
19819
+ }
19820
+ console.log(` Map: ${rel}`);
19821
+ console.log(chalk26.dim(" Next: iamu explore coverage"));
19822
+ }
19823
+ async function resolveMapId(projectRoot, id, offsetFromEnd = 1) {
19824
+ if (id && id !== "latest") return id;
19825
+ const ids = await listExploreMaps(projectRoot);
19826
+ return ids.at(-offsetFromEnd) ?? null;
19827
+ }
19828
+ async function exploreList(opts) {
19829
+ const ctx = await resolveProject(opts);
19830
+ if (!ctx) return;
19831
+ const rows = [];
19832
+ for (const id of await listExploreMaps(ctx.projectRoot)) {
19833
+ try {
19834
+ const m = await readExploreMap(ctx.projectRoot, id);
19835
+ rows.push({ id, target_url: m.target_url, surfaces: m.surfaces.length });
19836
+ } catch {
19837
+ }
19838
+ }
19839
+ if (opts.json) {
19840
+ console.log(JSON.stringify({ maps: rows }));
19841
+ return;
19842
+ }
19843
+ if (rows.length === 0) {
19844
+ console.log(" No maps yet. Run: iamu explore <url>");
19845
+ return;
19846
+ }
19847
+ for (const r of rows) {
19848
+ console.log(
19849
+ ` ${r.id} ${r.surfaces} surfaces ${chalk26.dim(r.target_url)}`
19850
+ );
19851
+ }
19852
+ }
19853
+ async function exploreCoverageCommand(mapId, opts) {
19854
+ const ctx = await resolveProject(opts);
19855
+ if (!ctx) return;
19856
+ const id = await resolveMapId(ctx.projectRoot, mapId);
19857
+ if (!id) {
19858
+ fail2("No map yet. Run: iamu explore <url>", opts.json);
19859
+ return;
19860
+ }
19861
+ let map2;
19862
+ try {
19863
+ map2 = await readExploreMap(ctx.projectRoot, id);
19864
+ } catch (err) {
19865
+ fail2(err.message, opts.json);
19866
+ return;
19867
+ }
19868
+ const cov = exploreCoverage(map2, await readAllRunSteps(ctx.projectRoot));
19869
+ if (opts.json) {
19870
+ console.log(JSON.stringify(cov));
19871
+ return;
19872
+ }
19873
+ console.log(
19874
+ `
19875
+ Map ${id} (${map2.surfaces.length} surfaces) against ${cov.runs} runs, ${cov.steps} steps`
19876
+ );
19877
+ console.log(
19878
+ ` ${chalk26.green(`${cov.reached} reached`)} \xB7 ${chalk26.yellow(`${cov.place_only} URL reached, view unclear`)} \xB7 ${chalk26.red(`${cov.unreached} never reached`)}`
19879
+ );
19880
+ const section = (title, rows, color) => {
19881
+ if (rows.length === 0) return;
19882
+ console.log(`
19883
+ ${color(title)}`);
19884
+ for (const r of rows) {
19885
+ console.log(
19886
+ ` ${r.key.padEnd(32)} ${r.label}${chalk26.dim(` (depth ${r.depth})`)}`
19887
+ );
19888
+ }
19889
+ };
19890
+ section(
19891
+ "Never reached by a run",
19892
+ cov.surfaces.filter((s) => s.status === "unreached"),
19893
+ chalk26.red
19894
+ );
19895
+ section(
19896
+ "URL reached, but no step names this view",
19897
+ cov.surfaces.filter((s) => s.status === "place_only"),
19898
+ chalk26.yellow
19899
+ );
19900
+ if (cov.run_only_places.length > 0) {
19901
+ console.log(`
19902
+ ${chalk26.cyan("Walked by runs, not in the map")}`);
19903
+ for (const p of cov.run_only_places) {
19904
+ console.log(
19905
+ ` ${p.place.padEnd(32)} ${p.steps} steps, ${p.runs.length} runs`
19906
+ );
19907
+ }
19908
+ }
19909
+ if (cov.runs === 0) {
19910
+ console.log(
19911
+ chalk26.dim(
19912
+ "\n No runs yet \u2014 every surface counts as never reached. Walk a scenario first (iamu-run)."
19913
+ )
19914
+ );
19915
+ }
19916
+ }
19917
+ async function exploreDiffCommand(a, b, opts) {
19918
+ const ctx = await resolveProject(opts);
19919
+ if (!ctx) return;
19920
+ const idA = a ?? await resolveMapId(ctx.projectRoot, void 0, 2);
19921
+ const idB = b ?? await resolveMapId(ctx.projectRoot, void 0, 1);
19922
+ if (!idA || !idB || idA === idB) {
19923
+ fail2(
19924
+ "Need two maps to compare \u2014 explore again (iamu explore <url>), or name both maps.",
19925
+ opts.json
19926
+ );
19927
+ return;
19928
+ }
19929
+ let mapA;
19930
+ let mapB;
19931
+ try {
19932
+ mapA = await readExploreMap(ctx.projectRoot, idA);
19933
+ mapB = await readExploreMap(ctx.projectRoot, idB);
19934
+ } catch (err) {
19935
+ fail2(err.message, opts.json);
19936
+ return;
19937
+ }
19938
+ const d = diffExploreMaps(mapA, mapB);
19939
+ if (opts.json) {
19940
+ console.log(JSON.stringify(d));
19941
+ return;
19942
+ }
19943
+ const pct = Math.round(d.surface_overlap * 100);
19944
+ console.log(`
19945
+ ${idA} \u2192 ${idB}`);
19946
+ console.log(
19947
+ ` ${chalk26.bold(`${pct}% of surfaces match`)} (${d.common} shared, ${d.only_a.length} only in the first, ${d.only_b.length} only in the second)`
19948
+ );
19949
+ if (d.only_a.length > 0) {
19950
+ console.log(`
19951
+ ${chalk26.red("Only in the first")}`);
19952
+ for (const s of d.only_a) console.log(` ${s.key.padEnd(32)} ${s.label}`);
19953
+ }
19954
+ if (d.only_b.length > 0) {
19955
+ console.log(`
19956
+ ${chalk26.green("Only in the second")}`);
19957
+ for (const s of d.only_b) console.log(` ${s.key.padEnd(32)} ${s.label}`);
19958
+ }
19959
+ if (d.changed_actions.length > 0) {
19960
+ console.log(
19961
+ `
19962
+ ${chalk26.yellow(`Controls differ on ${d.changed_actions.length} shared surfaces`)}`
19963
+ );
19964
+ for (const c of d.changed_actions) {
19965
+ console.log(` ${c.key} ${c.label}`);
19966
+ for (const x of c.only_a) console.log(chalk26.red(` - ${x}`));
19967
+ for (const x of c.only_b) console.log(chalk26.green(` + ${x}`));
19968
+ }
19969
+ }
19970
+ }
19971
+ var DEFAULTS;
19972
+ var init_explore = __esm({
19973
+ "src/commands/explore.ts"() {
19974
+ "use strict";
19975
+ init_dist2();
19976
+ init_build_info();
19977
+ init_browse();
19978
+ init_context4();
19979
+ init_explore_crawl();
19980
+ DEFAULTS = { max_surfaces: 40, max_actions: 300, max_depth: 6 };
19981
+ }
19982
+ });
19983
+
18709
19984
  // src/commands/completion.ts
18710
19985
  var completion_exports = {};
18711
19986
  __export(completion_exports, {
@@ -18902,7 +20177,7 @@ __export(instructions_walk_exports, {
18902
20177
  });
18903
20178
  import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
18904
20179
  import { join as join9 } from "path";
18905
- import chalk26 from "chalk";
20180
+ import chalk27 from "chalk";
18906
20181
  function selfCommand() {
18907
20182
  const q = (p) => /[\s"'$]/.test(p) ? JSON.stringify(p) : p;
18908
20183
  return `${q(process.execPath)} ${q(process.argv[1] ?? "iamu")}`;
@@ -19229,35 +20504,35 @@ async function instructionsWalk(opts) {
19229
20504
  return;
19230
20505
  }
19231
20506
  console.log();
19232
- console.log(chalk26.bold(" Walk instructions"));
19233
- console.log(` ${chalk26.dim("scenario:")} ${sfm.slug}`);
19234
- console.log(` ${chalk26.dim("persona:")} ${pfm.name} (${pfm.role})`);
19235
- console.log(` ${chalk26.dim("outline:")} ${ofm.title}`);
20507
+ console.log(chalk27.bold(" Walk instructions"));
20508
+ console.log(` ${chalk27.dim("scenario:")} ${sfm.slug}`);
20509
+ console.log(` ${chalk27.dim("persona:")} ${pfm.name} (${pfm.role})`);
20510
+ console.log(` ${chalk27.dim("outline:")} ${ofm.title}`);
19236
20511
  if (sfm.state?.description || sfm.state?.approximation) {
19237
20512
  console.log(
19238
- ` ${chalk26.dim("state:")} ${sfm.state.description ? sfm.state.description : sfm.state.approximation ?? ""}`
20513
+ ` ${chalk27.dim("state:")} ${sfm.state.description ? sfm.state.description : sfm.state.approximation ?? ""}`
19239
20514
  );
19240
20515
  }
19241
- if (targetUrl) console.log(` ${chalk26.dim("target:")} ${targetUrl}`);
20516
+ if (targetUrl) console.log(` ${chalk27.dim("target:")} ${targetUrl}`);
19242
20517
  console.log(
19243
- ` ${chalk26.dim("access:")} ${projectAccess || personaAccess ? `${projectAccess ? "project" : ""}${projectAccess && personaAccess ? " + " : ""}${personaAccess ? "persona" : ""}` : "not recorded (walker would run as whoever is signed in)"}`
20518
+ ` ${chalk27.dim("access:")} ${projectAccess || personaAccess ? `${projectAccess ? "project" : ""}${projectAccess && personaAccess ? " + " : ""}${personaAccess ? "persona" : ""}` : "not recorded (walker would run as whoever is signed in)"}`
19244
20519
  );
19245
20520
  console.log(
19246
- ` ${chalk26.dim("dismissed:")} ${dismissedFacts.length} findings to skip`
20521
+ ` ${chalk27.dim("dismissed:")} ${dismissedFacts.length} findings to skip`
19247
20522
  );
19248
20523
  console.log(
19249
- ` ${chalk26.dim("guidance:")} ${guidance.length} tester guidance rule${guidance.length === 1 ? "" : "s"}`
20524
+ ` ${chalk27.dim("guidance:")} ${guidance.length} tester guidance rule${guidance.length === 1 ? "" : "s"}`
19250
20525
  );
19251
20526
  if (revisit.length > 0) {
19252
20527
  console.log(
19253
- ` ${chalk26.dim("re-check:")} ${revisit.length} place${revisit.length === 1 ? "" : "s"} where fixes await a re-run`
20528
+ ` ${chalk27.dim("re-check:")} ${revisit.length} place${revisit.length === 1 ? "" : "s"} where fixes await a re-run`
19254
20529
  );
19255
20530
  }
19256
20531
  console.log(
19257
- ` ${chalk26.dim("methodology:")} ${methodology ? "loaded" : "missing (run iamu update)"}`
20532
+ ` ${chalk27.dim("methodology:")} ${methodology ? "loaded" : "missing (run iamu update)"}`
19258
20533
  );
19259
20534
  console.log();
19260
- console.log(chalk26.dim(" Use --json to get the full instruction bundle."));
20535
+ console.log(chalk27.dim(" Use --json to get the full instruction bundle."));
19261
20536
  console.log();
19262
20537
  }
19263
20538
  var SCREEN;
@@ -19283,8 +20558,8 @@ __export(new_finding_exports, {
19283
20558
  });
19284
20559
  import { randomUUID as randomUUID10 } from "crypto";
19285
20560
  import { access, readFile as readFile6 } from "fs/promises";
19286
- import path24 from "path";
19287
- import chalk27 from "chalk";
20561
+ import path26 from "path";
20562
+ import chalk28 from "chalk";
19288
20563
  async function newFinding(opts) {
19289
20564
  const ctx = await resolveProject(opts);
19290
20565
  if (!ctx) return;
@@ -19350,7 +20625,7 @@ async function newFinding(opts) {
19350
20625
  }
19351
20626
  try {
19352
20627
  await access(
19353
- path24.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
20628
+ path26.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
19354
20629
  );
19355
20630
  } catch {
19356
20631
  commandError(
@@ -19365,7 +20640,7 @@ async function newFinding(opts) {
19365
20640
  if (!scenarioSlug) {
19366
20641
  try {
19367
20642
  const manifestRaw = await readFile6(
19368
- path24.join(runDirPath, "walkthrough.json"),
20643
+ path26.join(runDirPath, "walkthrough.json"),
19369
20644
  "utf8"
19370
20645
  );
19371
20646
  const manifest = JSON.parse(manifestRaw);
@@ -19454,10 +20729,10 @@ async function newFinding(opts) {
19454
20729
  return;
19455
20730
  }
19456
20731
  console.log();
19457
- console.log(` ${symbols.success} Created finding: ${chalk27.cyan(slug)}`);
19458
- console.log(` ${chalk27.dim("path:")} ${record.path}`);
19459
- console.log(` ${chalk27.dim("id:")} ${id}`);
19460
- for (const w of warnings) console.log(` ${chalk27.yellow("!")} ${w}`);
20732
+ console.log(` ${symbols.success} Created finding: ${chalk28.cyan(slug)}`);
20733
+ console.log(` ${chalk28.dim("path:")} ${record.path}`);
20734
+ console.log(` ${chalk28.dim("id:")} ${id}`);
20735
+ for (const w of warnings) console.log(` ${chalk28.yellow("!")} ${w}`);
19461
20736
  console.log();
19462
20737
  } catch (err) {
19463
20738
  commandError(
@@ -19483,7 +20758,7 @@ __export(view_results_exports, {
19483
20758
  promptViewResults: () => promptViewResults
19484
20759
  });
19485
20760
  import { isCancel as isCancel4, select as select3 } from "@clack/prompts";
19486
- import chalk28 from "chalk";
20761
+ import chalk29 from "chalk";
19487
20762
  function launchCloudDashboard(config, runId) {
19488
20763
  const cloudRunId = runId ? toCloudRunUuid(runId) : void 0;
19489
20764
  const url = getCloudDashboardUrl(
@@ -19492,7 +20767,7 @@ function launchCloudDashboard(config, runId) {
19492
20767
  cloudRunId
19493
20768
  );
19494
20769
  console.log();
19495
- console.log(` ${symbols.success} Opening Iamu Cloud: ${chalk28.cyan(url)}
20770
+ console.log(` ${symbols.success} Opening Iamu Cloud: ${chalk29.cyan(url)}
19496
20771
  `);
19497
20772
  openBrowser(url);
19498
20773
  }
@@ -19560,9 +20835,9 @@ __export(evaluate_exports, {
19560
20835
  evaluate: () => evaluate,
19561
20836
  evaluateRecord: () => evaluateRecord
19562
20837
  });
19563
- import { promises as fs25 } from "fs";
19564
- import path25 from "path";
19565
- import chalk29 from "chalk";
20838
+ import { promises as fs28 } from "fs";
20839
+ import path27 from "path";
20840
+ import chalk30 from "chalk";
19566
20841
  import { z as z25 } from "zod";
19567
20842
  function formatIssues(error) {
19568
20843
  return error.issues.map(
@@ -19592,10 +20867,10 @@ async function evaluate(runId, opts) {
19592
20867
  const resolved = await resolveProjectAndRunDir(runId, opts);
19593
20868
  if (!resolved) return;
19594
20869
  const { projectRoot, runDirPath } = resolved;
19595
- const canonicalPath = path25.join(runDirPath, "canonical.json");
20870
+ const canonicalPath = path27.join(runDirPath, "canonical.json");
19596
20871
  let canonical;
19597
20872
  try {
19598
- const raw = await fs25.readFile(canonicalPath, "utf8");
20873
+ const raw = await fs28.readFile(canonicalPath, "utf8");
19599
20874
  canonical = JSON.parse(raw);
19600
20875
  } catch {
19601
20876
  commandError(
@@ -19651,15 +20926,15 @@ async function evaluate(runId, opts) {
19651
20926
  )
19652
20927
  };
19653
20928
  const prompt = composeJudgePrompt(input);
19654
- const promptFilePath = opts?.promptFile ?? path25.join(runDirPath, "judge_prompt.md");
19655
- await fs25.writeFile(promptFilePath, prompt, "utf8");
20929
+ const promptFilePath = opts?.promptFile ?? path27.join(runDirPath, "judge_prompt.md");
20930
+ await fs28.writeFile(promptFilePath, prompt, "utf8");
19656
20931
  if (opts?.json) {
19657
20932
  console.log(
19658
20933
  JSON.stringify(
19659
20934
  {
19660
20935
  ...input,
19661
- prompt_file: path25.relative(projectRoot, promptFilePath),
19662
- prompt_file_abs: path25.resolve(promptFilePath),
20936
+ prompt_file: path27.relative(projectRoot, promptFilePath),
20937
+ prompt_file_abs: path27.resolve(promptFilePath),
19663
20938
  judge_prompt: prompt
19664
20939
  },
19665
20940
  null,
@@ -19674,10 +20949,10 @@ async function evaluateRecord(runId, opts) {
19674
20949
  const resolved = await resolveProjectAndRunDir(runId, opts);
19675
20950
  if (!resolved) return;
19676
20951
  const { projectRoot, runDirPath } = resolved;
19677
- const canonicalPath = path25.join(runDirPath, "canonical.json");
20952
+ const canonicalPath = path27.join(runDirPath, "canonical.json");
19678
20953
  let canonical;
19679
20954
  try {
19680
- const raw = await fs25.readFile(canonicalPath, "utf8");
20955
+ const raw = await fs28.readFile(canonicalPath, "utf8");
19681
20956
  canonical = JSON.parse(raw);
19682
20957
  } catch {
19683
20958
  commandError(`No canonical.json for run: ${runId}`, opts);
@@ -19685,7 +20960,7 @@ async function evaluateRecord(runId, opts) {
19685
20960
  }
19686
20961
  let parsedFile;
19687
20962
  try {
19688
- const raw = await fs25.readFile(opts.file, "utf8");
20963
+ const raw = await fs28.readFile(opts.file, "utf8");
19689
20964
  parsedFile = JSON.parse(raw);
19690
20965
  } catch {
19691
20966
  commandError(`Cannot read verdicts file: ${opts.file}`, opts);
@@ -19742,8 +21017,8 @@ ${formatIssues(fileResult.error)}`,
19742
21017
  overall: inputVerdicts.overall
19743
21018
  });
19744
21019
  const reportContent = renderCanonicalMarkdown(canonicalForReport);
19745
- const verdictsPath = path25.join(runDirPath, "verdicts.json");
19746
- const reportPath = path25.join(runDirPath, "report.md");
21020
+ const verdictsPath = path27.join(runDirPath, "verdicts.json");
21021
+ const reportPath = path27.join(runDirPath, "report.md");
19747
21022
  await Promise.all([
19748
21023
  atomicWriteFile(
19749
21024
  verdictsPath,
@@ -19763,8 +21038,8 @@ ${formatIssues(fileResult.error)}`,
19763
21038
  JSON.stringify(
19764
21039
  {
19765
21040
  runId,
19766
- verdictsPath: path25.relative(projectRoot, verdictsPath),
19767
- reportPath: path25.relative(projectRoot, reportPath),
21041
+ verdictsPath: path27.relative(projectRoot, verdictsPath),
21042
+ reportPath: path27.relative(projectRoot, reportPath),
19768
21043
  verdictsCount: validatedVerdicts.length,
19769
21044
  linkedCount: linked.length,
19770
21045
  ...cloudInfo ? { cloud: cloudInfo } : {}
@@ -19777,18 +21052,18 @@ ${formatIssues(fileResult.error)}`,
19777
21052
  }
19778
21053
  console.log();
19779
21054
  console.log(
19780
- ` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${chalk29.cyan(runId)}`
21055
+ ` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${chalk30.cyan(runId)}`
19781
21056
  );
19782
21057
  if (linked.length > 0) {
19783
21058
  console.log(
19784
- ` ${chalk29.dim("linked:")} ${linked.length} finding${linked.length === 1 ? "" : "s"} to the human test${linked.length === 1 ? "" : "s"} citing ${linked.length === 1 ? "it" : "them"}`
21059
+ ` ${chalk30.dim("linked:")} ${linked.length} finding${linked.length === 1 ? "" : "s"} to the human test${linked.length === 1 ? "" : "s"} citing ${linked.length === 1 ? "it" : "them"}`
19785
21060
  );
19786
21061
  }
19787
21062
  console.log(
19788
- ` ${chalk29.dim("verdicts:")} ${path25.relative(projectRoot, verdictsPath)}`
21063
+ ` ${chalk30.dim("verdicts:")} ${path27.relative(projectRoot, verdictsPath)}`
19789
21064
  );
19790
21065
  console.log(
19791
- ` ${chalk29.dim("report:")} ${path25.relative(projectRoot, reportPath)}`
21066
+ ` ${chalk30.dim("report:")} ${path27.relative(projectRoot, reportPath)}`
19792
21067
  );
19793
21068
  printCloudRunInfo(cloudInfo);
19794
21069
  await promptViewResults(projectRoot, { ...opts, runId });
@@ -19852,9 +21127,9 @@ __export(complete_exports, {
19852
21127
  screenshotSizeWarning: () => screenshotSizeWarning,
19853
21128
  validateCanonicalWalkthrough: () => validateCanonicalWalkthrough
19854
21129
  });
19855
- import { promises as fs26 } from "fs";
19856
- import path26 from "path";
19857
- import chalk30 from "chalk";
21130
+ import { promises as fs29 } from "fs";
21131
+ import path28 from "path";
21132
+ import chalk31 from "chalk";
19858
21133
  async function complete(runId, opts) {
19859
21134
  const run = await loadRun(runId, opts);
19860
21135
  if (!run) return;
@@ -19864,7 +21139,7 @@ async function complete(runId, opts) {
19864
21139
  }
19865
21140
  if (!opts?.json) {
19866
21141
  console.log();
19867
- console.log(chalk30.bold(" Assembling canonical JSON\u2026"));
21142
+ console.log(chalk31.bold(" Assembling canonical JSON\u2026"));
19868
21143
  }
19869
21144
  const provenanceAudit = auditStepProvenance(
19870
21145
  await readStepsForRun(run.projectRoot, runId)
@@ -19877,8 +21152,8 @@ async function complete(runId, opts) {
19877
21152
  const walkerSummary = await readWalkerSummary(run.runDirPath);
19878
21153
  let judgeReported;
19879
21154
  try {
19880
- const verdictsRaw = await fs26.readFile(
19881
- path26.join(run.runDirPath, "verdicts.json"),
21155
+ const verdictsRaw = await fs29.readFile(
21156
+ path28.join(run.runDirPath, "verdicts.json"),
19882
21157
  "utf8"
19883
21158
  );
19884
21159
  const parsed = JSON.parse(verdictsRaw);
@@ -19930,16 +21205,16 @@ async function loadRun(runId, opts) {
19930
21205
  if (!resolved) return null;
19931
21206
  const { projectRoot, runDirPath } = resolved;
19932
21207
  try {
19933
- await fs26.access(runDirPath);
21208
+ await fs29.access(runDirPath);
19934
21209
  } catch {
19935
21210
  commandError(`Run not found: ${runId}`, opts);
19936
21211
  return null;
19937
21212
  }
19938
- const manifestPath = path26.join(runDirPath, "walkthrough.json");
21213
+ const manifestPath = path28.join(runDirPath, "walkthrough.json");
19939
21214
  let manifest;
19940
21215
  try {
19941
21216
  manifest = JSON.parse(
19942
- await fs26.readFile(manifestPath, "utf8")
21217
+ await fs29.readFile(manifestPath, "utf8")
19943
21218
  );
19944
21219
  } catch {
19945
21220
  commandError(`No walkthrough.json found for run: ${runId}`, opts);
@@ -19951,8 +21226,8 @@ async function reportAlreadyCompleted(run, opts) {
19951
21226
  const { runId, manifest } = run;
19952
21227
  if (opts?.json) {
19953
21228
  try {
19954
- const raw = await fs26.readFile(
19955
- path26.join(run.runDirPath, "canonical.json"),
21229
+ const raw = await fs29.readFile(
21230
+ path28.join(run.runDirPath, "canonical.json"),
19956
21231
  "utf8"
19957
21232
  );
19958
21233
  console.log(raw.trim());
@@ -19972,18 +21247,18 @@ async function reportAlreadyCompleted(run, opts) {
19972
21247
  return;
19973
21248
  }
19974
21249
  console.log();
19975
- console.log(chalk30.bold(" Run: ") + chalk30.cyan(runId));
19976
- console.log(` status ${chalk30.green("completed")}`);
21250
+ console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
21251
+ console.log(` status ${chalk31.green("completed")}`);
19977
21252
  if (manifest.completed_at) {
19978
- console.log(` completed ${chalk30.dim(manifest.completed_at)}`);
21253
+ console.log(` completed ${chalk31.dim(manifest.completed_at)}`);
19979
21254
  }
19980
21255
  const viewUrl = await pushedRunUrl(run.projectRoot, runId);
19981
21256
  if (viewUrl) {
19982
- console.log(` view ${chalk30.underline(viewUrl)}`);
21257
+ console.log(` view ${chalk31.underline(viewUrl)}`);
19983
21258
  }
19984
21259
  console.log();
19985
21260
  console.log(
19986
- chalk30.dim(
21261
+ chalk31.dim(
19987
21262
  " Already completed \u2014 nothing to do. Use --force to reassemble the report."
19988
21263
  )
19989
21264
  );
@@ -19999,13 +21274,13 @@ function reportStrictProvenanceFailure(audit, opts) {
19999
21274
  }
20000
21275
  console.error();
20001
21276
  console.error(
20002
- chalk30.red(` ${symbols.error} Strict provenance audit failed:`)
21277
+ chalk31.red(` ${symbols.error} Strict provenance audit failed:`)
20003
21278
  );
20004
21279
  for (const w of audit.warnings) {
20005
- console.error(chalk30.red(` \u2022 ${w}`));
21280
+ console.error(chalk31.red(` \u2022 ${w}`));
20006
21281
  }
20007
21282
  console.error(
20008
- chalk30.dim(
21283
+ chalk31.dim(
20009
21284
  " Run was flagged as synthetic / scripted. Remove --strict-provenance to complete anyway."
20010
21285
  )
20011
21286
  );
@@ -20046,41 +21321,41 @@ function completeJsonPayload(canonical, extras) {
20046
21321
  function printCompletionSummary(run, completedAt, assembled, extras, provenanceAudit) {
20047
21322
  const { runId, projectRoot, manifest } = run;
20048
21323
  const { canonical } = assembled;
20049
- const count = (n) => n === 0 ? chalk30.dim("0") : chalk30.cyan(String(n));
20050
- console.log(chalk30.bold(" Run: ") + chalk30.cyan(runId));
20051
- console.log(` scenario ${chalk30.dim(manifest.scenario_slug)}`);
20052
- console.log(` url ${chalk30.dim(manifest.target_url)}`);
21324
+ const count = (n) => n === 0 ? chalk31.dim("0") : chalk31.cyan(String(n));
21325
+ console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
21326
+ console.log(` scenario ${chalk31.dim(manifest.scenario_slug)}`);
21327
+ console.log(` url ${chalk31.dim(manifest.target_url)}`);
20053
21328
  if (manifest.environment) {
20054
- console.log(` env ${chalk30.dim(manifest.environment)}`);
21329
+ console.log(` env ${chalk31.dim(manifest.environment)}`);
20055
21330
  }
20056
21331
  if (manifest.build) {
20057
- console.log(` build ${chalk30.dim(formatBuild(manifest.build))}`);
21332
+ console.log(` build ${chalk31.dim(formatBuild(manifest.build))}`);
20058
21333
  }
20059
21334
  if (manifest.viewport) {
20060
- console.log(` device ${chalk30.dim(formatViewport(manifest.viewport))}`);
21335
+ console.log(` device ${chalk31.dim(formatViewport(manifest.viewport))}`);
20061
21336
  }
20062
- console.log(` status ${chalk30.green("completed")}`);
21337
+ console.log(` status ${chalk31.green("completed")}`);
20063
21338
  console.log(` findings ${count(canonical.findings.length)}`);
20064
21339
  console.log(` steps ${count(canonical.steps.length)}`);
20065
- console.log(` started ${chalk30.dim(manifest.created_at)}`);
20066
- console.log(` completed ${chalk30.dim(completedAt)}`);
21340
+ console.log(` started ${chalk31.dim(manifest.created_at)}`);
21341
+ console.log(` completed ${chalk31.dim(completedAt)}`);
20067
21342
  const models = manifest.models;
20068
21343
  if (models?.velocity_sec_per_step != null) {
20069
21344
  console.log(
20070
- ` velocity ${chalk30.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk30.dim(`(${models.total_duration_sec}s total)`)}`
21345
+ ` velocity ${chalk31.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk31.dim(`(${models.total_duration_sec}s total)`)}`
20071
21346
  );
20072
21347
  }
20073
21348
  if (models && (models.walker || models.judge || models.matcher)) {
20074
21349
  const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
20075
21350
  const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
20076
- console.log(` models ${chalk30.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
21351
+ console.log(` models ${chalk31.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
20077
21352
  }
20078
21353
  console.log();
20079
21354
  console.log(
20080
- ` ${chalk30.dim("canonical:")} ${chalk30.dim(path26.relative(projectRoot, assembled.canonicalPath))}`
21355
+ ` ${chalk31.dim("canonical:")} ${chalk31.dim(path28.relative(projectRoot, assembled.canonicalPath))}`
20081
21356
  );
20082
21357
  console.log(
20083
- ` ${chalk30.dim("report:")} ${chalk30.dim(path26.relative(projectRoot, assembled.reportPath))}`
21358
+ ` ${chalk31.dim("report:")} ${chalk31.dim(path28.relative(projectRoot, assembled.reportPath))}`
20084
21359
  );
20085
21360
  if (extras.viewportWarning) {
20086
21361
  console.log();
@@ -20088,7 +21363,7 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
20088
21363
  }
20089
21364
  if (extras.codeWarning) {
20090
21365
  console.log();
20091
- console.log(` ${chalk30.yellow("!")} ${extras.codeWarning}`);
21366
+ console.log(` ${chalk31.yellow("!")} ${extras.codeWarning}`);
20092
21367
  }
20093
21368
  if (extras.revisit) printRevisit(extras.revisit);
20094
21369
  if (assembled.verification) printVerification(assembled.verification);
@@ -20100,13 +21375,13 @@ function printProvenanceWarnings(audit) {
20100
21375
  if (audit.warnings.length === 0) return;
20101
21376
  console.log();
20102
21377
  console.log(
20103
- ` ${symbols.warning} ${chalk30.yellow("Step provenance audit flagged suspicious timing:")}`
21378
+ ` ${symbols.warning} ${chalk31.yellow("Step provenance audit flagged suspicious timing:")}`
20104
21379
  );
20105
21380
  for (const w of audit.warnings) {
20106
- console.log(` ${chalk30.dim("\u2022")} ${w}`);
21381
+ console.log(` ${chalk31.dim("\u2022")} ${w}`);
20107
21382
  }
20108
21383
  console.log(
20109
- chalk30.dim(
21384
+ chalk31.dim(
20110
21385
  " Run appears to be programmatically batch-generated rather than an interactive walkthrough."
20111
21386
  )
20112
21387
  );
@@ -20138,17 +21413,17 @@ function screenshotFitsViewport(width, expected) {
20138
21413
  }
20139
21414
  async function screenshotSizeWarning(runDirPath, viewport) {
20140
21415
  if (!viewport) return null;
20141
- const dir = path26.join(runDirPath, "screenshots");
21416
+ const dir = path28.join(runDirPath, "screenshots");
20142
21417
  let first;
20143
21418
  try {
20144
- first = (await fs26.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
21419
+ first = (await fs29.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
20145
21420
  } catch {
20146
21421
  return null;
20147
21422
  }
20148
21423
  if (!first) return null;
20149
21424
  let size = null;
20150
21425
  try {
20151
- const handle = await fs26.open(path26.join(dir, first), "r");
21426
+ const handle = await fs29.open(path28.join(dir, first), "r");
20152
21427
  try {
20153
21428
  const head = Buffer.alloc(24);
20154
21429
  const { bytesRead } = await handle.read(head, 0, 24, 0);
@@ -20165,7 +21440,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
20165
21440
  async function readWalkerSummary(runDirPath) {
20166
21441
  try {
20167
21442
  const data = JSON.parse(
20168
- await fs26.readFile(path26.join(runDirPath, "summary.json"), "utf8")
21443
+ await fs29.readFile(path28.join(runDirPath, "summary.json"), "utf8")
20169
21444
  );
20170
21445
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
20171
21446
  const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
@@ -20274,7 +21549,7 @@ function validateCanonicalWalkthrough(canonical) {
20274
21549
  async function applyRecordedVerdicts(canonical, runDirPath) {
20275
21550
  try {
20276
21551
  const recorded = JSON.parse(
20277
- await fs26.readFile(path26.join(runDirPath, "verdicts.json"), "utf8")
21552
+ await fs29.readFile(path28.join(runDirPath, "verdicts.json"), "utf8")
20278
21553
  );
20279
21554
  if (Array.isArray(recorded.verdicts)) {
20280
21555
  return applyVerdicts(canonical, recorded);
@@ -20284,7 +21559,7 @@ async function applyRecordedVerdicts(canonical, runDirPath) {
20284
21559
  return canonical;
20285
21560
  }
20286
21561
  async function persistCanonical(runDirPath, canonical) {
20287
- const canonicalPath = path26.join(runDirPath, "canonical.json");
21562
+ const canonicalPath = path28.join(runDirPath, "canonical.json");
20288
21563
  await atomicWriteFile(
20289
21564
  canonicalPath,
20290
21565
  `${JSON.stringify(canonical, null, 2)}
@@ -20293,7 +21568,7 @@ async function persistCanonical(runDirPath, canonical) {
20293
21568
  return canonicalPath;
20294
21569
  }
20295
21570
  async function persistReport(runDirPath, canonical) {
20296
- const reportPath = path26.join(runDirPath, "report.md");
21571
+ const reportPath = path28.join(runDirPath, "report.md");
20297
21572
  await atomicWriteFile(reportPath, renderCanonicalMarkdown(canonical));
20298
21573
  return reportPath;
20299
21574
  }
@@ -20393,17 +21668,17 @@ function placeLabel(p) {
20393
21668
  function printRevisit(r) {
20394
21669
  const missed = r.places.filter((p) => !p.reached);
20395
21670
  console.log();
20396
- const head = ` ${chalk30.bold("Re-check places:")} reached ${r.reached} of ${r.requested}`;
21671
+ const head = ` ${chalk31.bold("Re-check places:")} reached ${r.reached} of ${r.requested}`;
20397
21672
  if (missed.length === 0) {
20398
- console.log(`${head} ${chalk30.green("\u2713")}`);
21673
+ console.log(`${head} ${chalk31.green("\u2713")}`);
20399
21674
  return;
20400
21675
  }
20401
21676
  console.log(`${head} \u2014 not reached:`);
20402
21677
  for (const p of missed) {
20403
- console.log(` ${chalk30.yellow("\u25CB")} ${placeLabel(p)}`);
21678
+ console.log(` ${chalk31.yellow("\u25CB")} ${placeLabel(p)}`);
20404
21679
  }
20405
21680
  console.log(
20406
- chalk30.dim(
21681
+ chalk31.dim(
20407
21682
  " Fixes there stay awaiting a re-run until a walk gets there."
20408
21683
  )
20409
21684
  );
@@ -20411,7 +21686,7 @@ function printRevisit(r) {
20411
21686
  function printVerification(verification) {
20412
21687
  console.log();
20413
21688
  console.log(
20414
- chalk30.bold(" Since this scenario's last run") + chalk30.dim(` (${verification.baseline_run_id.slice(0, 30)}\u2026)`) + chalk30.bold(":")
21689
+ chalk31.bold(" Since this scenario's last run") + chalk31.dim(` (${verification.baseline_run_id.slice(0, 30)}\u2026)`) + chalk31.bold(":")
20415
21690
  );
20416
21691
  if (verification.resolved.length > 0) {
20417
21692
  console.log(
@@ -20420,7 +21695,7 @@ function printVerification(verification) {
20420
21695
  }
20421
21696
  if (verification.not_observed.length > 0) {
20422
21697
  console.log(
20423
- ` ${symbols.pending} ${verification.not_observed.length} not seen this run ${chalk30.dim("(not evidence of a fix)")}`
21698
+ ` ${symbols.pending} ${verification.not_observed.length} not seen this run ${chalk31.dim("(not evidence of a fix)")}`
20424
21699
  );
20425
21700
  }
20426
21701
  if (verification.persists.length > 0) {
@@ -20461,14 +21736,14 @@ __export(match_exports, {
20461
21736
  match: () => match,
20462
21737
  matchRecord: () => matchRecord
20463
21738
  });
20464
- import { promises as fs27 } from "fs";
20465
- import path27 from "path";
20466
- import chalk31 from "chalk";
21739
+ import { promises as fs30 } from "fs";
21740
+ import path29 from "path";
21741
+ import chalk32 from "chalk";
20467
21742
  async function completedManifest2(runDirPath, runId, opts) {
20468
21743
  let manifest;
20469
21744
  try {
20470
21745
  manifest = JSON.parse(
20471
- await fs27.readFile(path27.join(runDirPath, "walkthrough.json"), "utf8")
21746
+ await fs30.readFile(path29.join(runDirPath, "walkthrough.json"), "utf8")
20472
21747
  );
20473
21748
  } catch {
20474
21749
  commandError(`Run not found: ${runId}`, opts);
@@ -20497,12 +21772,12 @@ async function match(runId, opts) {
20497
21772
  );
20498
21773
  const needed = input.current.length > 0 && (input.prior.length > 0 || (input.guidance ?? []).length > 0);
20499
21774
  const prompt = needed ? composeMatchPrompt(input) : null;
20500
- const promptFilePath = opts?.promptFile ?? path27.join(runDirPath, "match_prompt.md");
21775
+ const promptFilePath = opts?.promptFile ?? path29.join(runDirPath, "match_prompt.md");
20501
21776
  if (prompt) {
20502
- await fs27.writeFile(promptFilePath, prompt, "utf8");
21777
+ await fs30.writeFile(promptFilePath, prompt, "utf8");
20503
21778
  } else {
20504
21779
  try {
20505
- await fs27.unlink(promptFilePath);
21780
+ await fs30.unlink(promptFilePath);
20506
21781
  } catch {
20507
21782
  }
20508
21783
  }
@@ -20512,8 +21787,8 @@ async function match(runId, opts) {
20512
21787
  {
20513
21788
  ...input,
20514
21789
  needed,
20515
- prompt_file: prompt ? path27.relative(ctx.projectRoot, promptFilePath) : null,
20516
- prompt_file_abs: prompt ? path27.resolve(promptFilePath) : null,
21790
+ prompt_file: prompt ? path29.relative(ctx.projectRoot, promptFilePath) : null,
21791
+ prompt_file_abs: prompt ? path29.resolve(promptFilePath) : null,
20517
21792
  match_prompt: prompt
20518
21793
  },
20519
21794
  null,
@@ -20524,7 +21799,7 @@ async function match(runId, opts) {
20524
21799
  }
20525
21800
  if (!prompt) {
20526
21801
  console.log(
20527
- chalk31.dim(
21802
+ chalk32.dim(
20528
21803
  " No earlier findings or tester guidance for this scenario to match against \u2014 nothing to do."
20529
21804
  )
20530
21805
  );
@@ -20540,7 +21815,7 @@ async function matchRecord(runId, opts) {
20540
21815
  if (!manifest) return;
20541
21816
  let answer;
20542
21817
  try {
20543
- answer = JSON.parse(await fs27.readFile(opts.file, "utf8"));
21818
+ answer = JSON.parse(await fs30.readFile(opts.file, "utf8"));
20544
21819
  } catch {
20545
21820
  commandError(`Cannot read matches file: ${opts.file}`, opts);
20546
21821
  return;
@@ -20581,17 +21856,17 @@ async function matchRecord(runId, opts) {
20581
21856
  }
20582
21857
  console.log();
20583
21858
  console.log(
20584
- ` ${chalk31.green("\u2713")} Matched ${result.linked} finding${result.linked === 1 ? "" : "s"} to earlier ones, ${result.new} first seen, ${result.rechecked_fixed} earlier re-checked fixed`
21859
+ ` ${chalk32.green("\u2713")} Matched ${result.linked} finding${result.linked === 1 ? "" : "s"} to earlier ones, ${result.new} first seen, ${result.rechecked_fixed} earlier re-checked fixed`
20585
21860
  );
20586
21861
  if (result.suppressed > 0) {
20587
21862
  console.log(
20588
- ` ${chalk31.yellow("!")} ${result.suppressed} finding${result.suppressed === 1 ? "" : "s"} match tester guidance (flagged with suppressed_by_guidance \u2014 dismiss them in triage if the rule holds)`
21863
+ ` ${chalk32.yellow("!")} ${result.suppressed} finding${result.suppressed === 1 ? "" : "s"} match tester guidance (flagged with suppressed_by_guidance \u2014 dismiss them in triage if the rule holds)`
20589
21864
  );
20590
21865
  }
20591
21866
  if (verification) printVerification(verification);
20592
21867
  console.log();
20593
21868
  console.log(
20594
- chalk31.dim(" Run `iamu sync` to push the linked findings to the cloud.")
21869
+ chalk32.dim(" Run `iamu sync` to push the linked findings to the cloud.")
20595
21870
  );
20596
21871
  console.log();
20597
21872
  }
@@ -20612,7 +21887,7 @@ __export(prune_exports, {
20612
21887
  prune: () => prune
20613
21888
  });
20614
21889
  import { confirm, isCancel as isCancel5 } from "@clack/prompts";
20615
- import chalk32 from "chalk";
21890
+ import chalk33 from "chalk";
20616
21891
  function formatBytes(bytes) {
20617
21892
  if (bytes <= 0) return "0 B";
20618
21893
  if (bytes < 1024) return `${bytes} B`;
@@ -20658,7 +21933,7 @@ async function prune(opts = {}) {
20658
21933
  });
20659
21934
  if (preview.prunedRuns.length === 0) {
20660
21935
  console.log(
20661
- chalk32.dim(" No runs match the pruning criteria. Nothing to prune.")
21936
+ chalk33.dim(" No runs match the pruning criteria. Nothing to prune.")
20662
21937
  );
20663
21938
  return preview;
20664
21939
  }
@@ -20667,7 +21942,7 @@ async function prune(opts = {}) {
20667
21942
  message: `Prune ${mode} ${preview.prunedRuns.length} run(s) and reclaim ${formatBytes(preview.totalBytesReclaimed)}?`
20668
21943
  });
20669
21944
  if (isCancel5(confirmed) || !confirmed) {
20670
- console.log(chalk32.yellow(" Pruning cancelled."));
21945
+ console.log(chalk33.yellow(" Pruning cancelled."));
20671
21946
  return;
20672
21947
  }
20673
21948
  }
@@ -20678,14 +21953,14 @@ async function prune(opts = {}) {
20678
21953
  }
20679
21954
  if (result.dryRun) {
20680
21955
  console.log(
20681
- chalk32.cyan(
21956
+ chalk33.cyan(
20682
21957
  `
20683
21958
  [DRY RUN] Would prune ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
20684
21959
  )
20685
21960
  );
20686
21961
  } else {
20687
21962
  console.log(
20688
- chalk32.bold(
21963
+ chalk33.bold(
20689
21964
  `
20690
21965
  ${symbols.success} Pruned ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
20691
21966
  )
@@ -20693,18 +21968,18 @@ async function prune(opts = {}) {
20693
21968
  }
20694
21969
  if (result.prunedRuns.length === 0) {
20695
21970
  console.log(
20696
- chalk32.dim(" No runs matched the pruning criteria. Nothing pruned.")
21971
+ chalk33.dim(" No runs matched the pruning criteria. Nothing pruned.")
20697
21972
  );
20698
21973
  } else {
20699
21974
  for (const run of result.prunedRuns) {
20700
21975
  const mode = run.fullyDeleted ? "removed" : "screenshots purged";
20701
21976
  const count = run.screenshotsDeleted > 0 ? `${run.screenshotsDeleted} screenshots, ` : "";
20702
21977
  console.log(
20703
- ` ${chalk32.dim("\u2022")} ${chalk32.bold(run.runId)} ${chalk32.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk32.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
21978
+ ` ${chalk33.dim("\u2022")} ${chalk33.bold(run.runId)} ${chalk33.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk33.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
20704
21979
  );
20705
21980
  }
20706
21981
  console.log(
20707
- chalk32.dim(
21982
+ chalk33.dim(
20708
21983
  `
20709
21984
  Total reclaimed: ${formatBytes(result.totalBytesReclaimed)} (${result.totalScreenshotsDeleted} screenshots deleted)
20710
21985
  `
@@ -20727,7 +22002,7 @@ init_context4();
20727
22002
  import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
20728
22003
  import { dirname as dirname5, resolve as resolve3 } from "path";
20729
22004
  import { fileURLToPath as fileURLToPath3 } from "url";
20730
- import chalk35 from "chalk";
22005
+ import chalk36 from "chalk";
20731
22006
  import { Command } from "commander";
20732
22007
 
20733
22008
  // src/commands/register-cloud.ts
@@ -20863,10 +22138,13 @@ function registerProjectCommands(program2) {
20863
22138
  await status2(opts);
20864
22139
  });
20865
22140
  program2.command("doctor").helpGroup("Project:").description(
20866
- "Diagnose environment: runtime, browser automation, skills, cloud"
22141
+ "Diagnose environment: runtime, browser automation, skills, cloud, and target testability"
22142
+ ).option(
22143
+ "-t, --target <url>",
22144
+ "Probe target URL health, reachability, and auth redirects"
20867
22145
  ).option("--json", "Output diagnostics as JSON").addHelpText(
20868
22146
  "after",
20869
- "\nExamples:\n $ iamu doctor\n $ iamu doctor --json"
22147
+ "\nExamples:\n $ iamu doctor\n $ iamu doctor --target http://localhost:3000\n $ iamu doctor --json"
20870
22148
  ).action(async (opts) => {
20871
22149
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
20872
22150
  await doctor2(opts);
@@ -20898,9 +22176,9 @@ function registerProjectCommands(program2) {
20898
22176
  "after",
20899
22177
  "\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
20900
22178
  ).action(
20901
- async (path28, opts) => {
22179
+ async (path30, opts) => {
20902
22180
  const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
20903
- await validate2(path28, opts);
22181
+ await validate2(path30, opts);
20904
22182
  }
20905
22183
  );
20906
22184
  program2.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
@@ -20939,6 +22217,54 @@ function registerProjectCommands(program2) {
20939
22217
  await newEntity2(type2, opts);
20940
22218
  }
20941
22219
  );
22220
+ const explore = program2.command("explore").helpGroup("Project:").description(
22221
+ "Map the running product with a read-only crawl; compare the map with runs (coverage) or with another map (diff)"
22222
+ );
22223
+ explore.command("run [url]", { isDefault: true }).description(
22224
+ "Crawl from <url> (default: target_url in iamu/testability.md) and write iamu/.cache/explore/<id>/map.json"
22225
+ ).option("--max-surfaces <n>", "Stop after this many surfaces (default 40)").option("--max-actions <n>", "Stop after this many clicks (default 300)").option(
22226
+ "--max-depth <n>",
22227
+ "Don't go more than this many clicks from the start (default 6)"
22228
+ ).option(
22229
+ "--deny <pattern...>",
22230
+ "Never click a control whose name or link matches (regex, case-insensitive)"
22231
+ ).option("--viewport <WxH>", "Page size, e.g. 390x844 (default 1280x800)").option(
22232
+ "--attach",
22233
+ "Crawl in a new tab of the `iamu browse` browser, keeping its sign-ins"
22234
+ ).option("--json", "Output as JSON").addHelpText(
22235
+ "after",
22236
+ `
22237
+ Read-only: controls that read like writes (delete, pay, send, save, log out \u2026),
22238
+ form submits, downloads and other origins are never clicked, nothing is typed,
22239
+ and every request but GET/HEAD/OPTIONS is blocked before it leaves the browser.
22240
+
22241
+ Examples:
22242
+ $ iamu explore http://localhost:3000
22243
+ $ iamu explore --attach --deny "^Admin"
22244
+ $ iamu explore coverage
22245
+ $ iamu explore diff`
22246
+ ).action(
22247
+ async (url, opts) => {
22248
+ const { exploreRun: exploreRun2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
22249
+ await exploreRun2(url, opts);
22250
+ }
22251
+ );
22252
+ explore.command("coverage [map]").description(
22253
+ "Surfaces of a map (default: latest) that no run reached, and places runs reached that the map lacks"
22254
+ ).option("--json", "Output as JSON").action(async (map2, opts) => {
22255
+ const { exploreCoverageCommand: exploreCoverageCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
22256
+ await exploreCoverageCommand2(map2, opts);
22257
+ });
22258
+ explore.command("diff [a] [b]").description("Compare two maps (default: the two latest)").option("--json", "Output as JSON").action(
22259
+ async (a, b, opts) => {
22260
+ const { exploreDiffCommand: exploreDiffCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
22261
+ await exploreDiffCommand2(a, b, opts);
22262
+ }
22263
+ );
22264
+ explore.command("list").description("List maps").option("--json", "Output as JSON").action(async (opts) => {
22265
+ const { exploreList: exploreList2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
22266
+ await exploreList2(opts);
22267
+ });
20942
22268
  program2.command("completion [shell]").helpGroup("Project:").description("Output shell completion script (bash, zsh, fish)").action(async (shell) => {
20943
22269
  const { completion: completion2 } = await Promise.resolve().then(() => (init_completion(), completion_exports));
20944
22270
  completion2(program2, shell);
@@ -21146,23 +22472,23 @@ function registerAllCommands(program2) {
21146
22472
 
21147
22473
  // src/ui/panic.ts
21148
22474
  init_logger();
21149
- import chalk33 from "chalk";
22475
+ import chalk34 from "chalk";
21150
22476
  var handlersRegistered = false;
21151
22477
  var customSigintHandler = null;
21152
22478
  function logFatalAndExit(title, errOrReason) {
21153
22479
  process.stderr.write("\x1B[?25h");
21154
22480
  if (isVerbose()) {
21155
- console.error(chalk33.red(`
22481
+ console.error(chalk34.red(`
21156
22482
  ${title}:`));
21157
22483
  console.error(
21158
22484
  errOrReason instanceof Error ? errOrReason.stack || errOrReason : errOrReason
21159
22485
  );
21160
22486
  } else {
21161
22487
  const msg = errOrReason instanceof Error ? errOrReason.message : String(errOrReason);
21162
- console.error(chalk33.red(`
22488
+ console.error(chalk34.red(`
21163
22489
  Fatal Error: ${msg}`));
21164
22490
  console.error(
21165
- chalk33.dim(
22491
+ chalk34.dim(
21166
22492
  " Run with --verbose or DEBUG=1 to view the full stack trace.\n"
21167
22493
  )
21168
22494
  );
@@ -21181,7 +22507,7 @@ function setupProcessHandlers() {
21181
22507
  }
21182
22508
  process.exit(130);
21183
22509
  } else {
21184
- console.error(chalk33.dim("\n Operation cancelled."));
22510
+ console.error(chalk34.dim("\n Operation cancelled."));
21185
22511
  process.exit(130);
21186
22512
  }
21187
22513
  });
@@ -21207,7 +22533,7 @@ function setupProcessHandlers() {
21207
22533
  import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
21208
22534
  import { homedir as homedir4 } from "os";
21209
22535
  import { join as join10 } from "path";
21210
- import chalk34 from "chalk";
22536
+ import chalk35 from "chalk";
21211
22537
  var CACHE_FILE = join10(homedir4(), ".iamu", "update-check.json");
21212
22538
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
21213
22539
  function isSemverGreater(latest, current) {
@@ -21260,7 +22586,7 @@ async function checkForUpdate(currentVersion) {
21260
22586
  function printUpdateNotice(currentVersion, latestVersion) {
21261
22587
  console.error();
21262
22588
  console.error(
21263
- ` ${chalk34.yellow("Update available:")} ${chalk34.dim(currentVersion)} \u2192 ${chalk34.green(latestVersion)} ${chalk34.dim("(run: npm i -g @iamu-dev/iamu)")}`
22589
+ ` ${chalk35.yellow("Update available:")} ${chalk35.dim(currentVersion)} \u2192 ${chalk35.green(latestVersion)} ${chalk35.dim("(run: npm i -g @iamu-dev/iamu)")}`
21264
22590
  );
21265
22591
  }
21266
22592
 
@@ -21305,7 +22631,7 @@ var program = new Command().name("iamu").description(
21305
22631
  }
21306
22632
  }
21307
22633
  if (root.color === false || process.env.NO_COLOR) {
21308
- chalk35.level = 0;
22634
+ chalk36.level = 0;
21309
22635
  }
21310
22636
  if (root.verbose) {
21311
22637
  process.env.IAMU_VERBOSE = "1";