@iamu-dev/iamu 0.8.0-beta.2 → 0.8.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -521,6 +521,7 @@ var init_finding = __esm({
521
521
  DISMISS_REASONS = [
522
522
  "not_relevant",
523
523
  "walker_error",
524
+ "out_of_scope",
524
525
  "duplicate"
525
526
  ];
526
527
  dismissReasonSchema = z9.enum(DISMISS_REASONS);
@@ -570,11 +571,12 @@ var init_finding = __esm({
570
571
  addressed_at: isoTimestamp.nullish(),
571
572
  /**
572
573
  * Why a `dismissed` finding was dismissed: `not_relevant` (by design,
573
- * out of scope), `walker_error` (the walker misread the product) or
574
- * `duplicate`. Absent means `not_relevant`. The first two also create
575
- * a tester guidance rule.
574
+ * out of scope), `walker_error` (the walker misread the product),
575
+ * `out_of_scope` or `duplicate`. Absent means `not_relevant`.
576
576
  */
577
577
  dismiss_reason: dismissReasonSchema.nullish(),
578
+ /** Finding slug or id this repeats when `dismiss_reason` is `duplicate`. */
579
+ duplicate_of: z9.string().nullish(),
578
580
  /**
579
581
  * The active tester guidance rule this finding matches, set by
580
582
  * `iamu match-record`. The finding is kept — the rule flagged it,
@@ -697,7 +699,11 @@ var init_guidance = __esm({
697
699
  "../../packages/protocol/dist/schemas/frontmatter/guidance.js"() {
698
700
  "use strict";
699
701
  init_common();
700
- GUIDANCE_KINDS = ["not_relevant", "walker_error"];
702
+ GUIDANCE_KINDS = [
703
+ "not_relevant",
704
+ "walker_error",
705
+ "out_of_scope"
706
+ ];
701
707
  guidanceKindSchema = z10.enum(GUIDANCE_KINDS);
702
708
  guidanceStatusSchema = z10.enum(["active", "retired"]);
703
709
  guidanceScopeSchema = z10.object({
@@ -1597,6 +1603,7 @@ var init_walkthrough_canonical = __esm({
1597
1603
  canonicalStepSchema = z22.object({
1598
1604
  id: z22.string(),
1599
1605
  step_number: z22.number().int(),
1606
+ timestamp: z22.string().optional(),
1600
1607
  url: z22.string().optional(),
1601
1608
  /** The walker's short name for the screen. */
1602
1609
  surface: z22.string().optional(),
@@ -5834,9 +5841,9 @@ function pidIsDead(rec) {
5834
5841
  async function isStale(lockPath, rec, staleMs, now) {
5835
5842
  if (rec === null) {
5836
5843
  try {
5837
- const stat3 = await fs6.stat(lockPath);
5838
- if (stat3.size === 0) {
5839
- const mtime = stat3.birthtimeMs && stat3.birthtimeMs > 0 ? stat3.birthtimeMs : stat3.mtimeMs;
5844
+ const stat4 = await fs6.stat(lockPath);
5845
+ if (stat4.size === 0) {
5846
+ const mtime = stat4.birthtimeMs && stat4.birthtimeMs > 0 ? stat4.birthtimeMs : stat4.mtimeMs;
5840
5847
  if (now - mtime <= staleMs) {
5841
5848
  return false;
5842
5849
  }
@@ -6837,13 +6844,13 @@ function fenceTracker() {
6837
6844
  };
6838
6845
  }
6839
6846
  function cleanName(raw, legacy) {
6840
- let s = raw.replace(/\s+#+\s*$/, "").replace(/[*_`]/g, "").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").trim();
6847
+ let s = raw.replace(/\\(["'])/g, "$1").replace(/\s+#+\s*$/, "").replace(/[*_`]/g, "").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").trim();
6841
6848
  if (legacy)
6842
6849
  s = s.replace(LEGACY_ID, "");
6843
- return s.replace(/[.:]+$/, "").trim();
6850
+ return s.replace(/[.:—–-]+$/, "").trim();
6844
6851
  }
6845
6852
  function normaliseRuleName(raw, legacy = false) {
6846
- return cleanName(raw, legacy).toLowerCase();
6853
+ return cleanName(raw, legacy).replace(/["'\\]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
6847
6854
  }
6848
6855
  function scanLines(text) {
6849
6856
  const inFence = fenceTracker();
@@ -6912,7 +6919,7 @@ function rulesInText(text, opts = {}) {
6912
6919
  continue;
6913
6920
  if (l.heading) {
6914
6921
  const { level, text: h } = l.heading;
6915
- if (level >= 2 && level <= 4) {
6922
+ if (level >= 2 && level <= 5) {
6916
6923
  let end2 = i + 1;
6917
6924
  while (end2 < to) {
6918
6925
  const next = lines[end2].heading;
@@ -6931,25 +6938,30 @@ function rulesInText(text, opts = {}) {
6931
6938
  headings.push(l.heading);
6932
6939
  continue;
6933
6940
  }
6934
- const item = LIST_ITEM.exec(l.text);
6935
- const rest = item ? l.text.slice(item[0].length) : l.text.trimStart();
6941
+ const unquoted = l.text.replace(/^\s*>\s?/, "");
6942
+ const item = LIST_ITEM.exec(unquoted);
6943
+ const rest = item ? unquoted.slice(item[0].length) : unquoted.trimStart();
6936
6944
  const bold = BOLD_LEAD.exec(rest);
6937
6945
  if (!bold)
6938
6946
  continue;
6939
6947
  const inner = bold[2].trim();
6940
- if (!/[.:]$/.test(inner) && !bold[3])
6948
+ const afterBold = rest.slice(bold[0].length);
6949
+ const hasStop = /[.:!?—–]$/.test(inner) || Boolean(bold[3]) || afterBold.trim() === "";
6950
+ if (!hasStop)
6941
6951
  continue;
6942
6952
  const prev = i === 0 ? null : lines[i - 1];
6943
- const startsParagraph = !prev || prev.text.trim() === "" || Boolean(prev.heading);
6953
+ const prevUnquoted = prev ? prev.text.replace(/^\s*>\s?/, "") : "";
6954
+ const startsParagraph = !prev || prevUnquoted.trim() === "" || Boolean(prev.heading);
6944
6955
  if (!item && !startsParagraph)
6945
6956
  continue;
6946
6957
  const indent = item ? item[1].length : 0;
6947
6958
  let end = i + 1;
6948
6959
  while (end < to) {
6949
6960
  const n = lines[end];
6950
- if (n.heading || !n.fenced && n.text.trim() === "")
6961
+ const nUnquoted = n.text.replace(/^\s*>\s?/, "");
6962
+ if (n.heading || !n.fenced && nUnquoted.trim() === "")
6951
6963
  break;
6952
- const nItem = LIST_ITEM.exec(n.text);
6964
+ const nItem = LIST_ITEM.exec(nUnquoted);
6953
6965
  if (item && nItem && nItem[1].length <= indent)
6954
6966
  break;
6955
6967
  end++;
@@ -6974,7 +6986,19 @@ function matchRuleName(rules, citation, legacy = false) {
6974
6986
  section: normaliseRuleName(citation.slice(0, at), legacy),
6975
6987
  name: normaliseRuleName(citation.slice(at + 1), legacy)
6976
6988
  } : null;
6977
- return rules.filter((r) => r.name.toLowerCase() === whole || scoped !== null && r.name.toLowerCase() === scoped.name && (r.section ?? "").toLowerCase() === scoped.section);
6989
+ const exact = rules.filter((r) => {
6990
+ const rName = normaliseRuleName(r.name, legacy);
6991
+ const rSec = normaliseRuleName(r.section ?? "", legacy);
6992
+ return rName === whole || scoped !== null && rName === scoped.name && rSec === scoped.section;
6993
+ });
6994
+ if (exact.length > 0)
6995
+ return exact;
6996
+ const leadOf = (s) => normaliseRuleName(s.split(/\s*(?::|—|–|\s-\s)\s*/)[0] ?? "", legacy);
6997
+ return rules.filter((r) => {
6998
+ const rLead = leadOf(r.name);
6999
+ const rSec = normaliseRuleName(r.section ?? "", legacy);
7000
+ return rLead.length > 0 && rLead === whole || scoped !== null && rLead.length > 0 && rLead === scoped.name && rSec === scoped.section;
7001
+ });
6978
7002
  }
6979
7003
  async function findRule(entries, citation) {
6980
7004
  const seen = /* @__PURE__ */ new Map();
@@ -7008,6 +7032,27 @@ async function citationFor(entries, match2) {
7008
7032
  const alone = await findRule(entries, { rule: match2.name, file: match2.file });
7009
7033
  return alone.length === 1 || !match2.section ? match2.name : `${match2.section} > ${match2.name}`;
7010
7034
  }
7035
+ async function canonicalRulesInFile(file) {
7036
+ if (file.kind !== "design")
7037
+ return [];
7038
+ let text;
7039
+ try {
7040
+ text = await fs10.readFile(file.abs, "utf8");
7041
+ } catch {
7042
+ return [];
7043
+ }
7044
+ const legacy = file.path === LEGACY_DESIGN_DOC;
7045
+ const rules = rulesInText(text, { legacy, heading: file.heading }) ?? [];
7046
+ const counts = /* @__PURE__ */ new Map();
7047
+ for (const r of rules) {
7048
+ const key = normaliseRuleName(r.name, legacy);
7049
+ counts.set(key, (counts.get(key) ?? 0) + 1);
7050
+ }
7051
+ return rules.map((r) => {
7052
+ const key = normaliseRuleName(r.name, legacy);
7053
+ return (counts.get(key) ?? 0) === 1 || !r.section ? r.name : `${r.section} > ${r.name}`;
7054
+ });
7055
+ }
7011
7056
  async function resolveRule(entries, citation) {
7012
7057
  const matches = await findRule(entries, citation);
7013
7058
  if (matches.length === 1)
@@ -7034,7 +7079,7 @@ var init_design_rules = __esm({
7034
7079
  LEGACY_ID = new RegExp(`^${LEGACY_ID_PATTERN}\\s+`);
7035
7080
  HEADING = /^(#{1,6})\s+(.+?)\s*$/;
7036
7081
  LIST_ITEM = /^(\s*)(?:[-*+]|\d+[.)])\s+/;
7037
- BOLD_LEAD = /^(\*\*|__)(.+?)\1\s*([.:])?/;
7082
+ BOLD_LEAD = /^(\*\*|__)(.+?)\1(?:\s*([.:—–]|\s-\s))?/;
7038
7083
  sameFile = (f, file) => {
7039
7084
  const want = file.trim().replace(/\\/g, "/");
7040
7085
  return f.entry === want || f.path === want;
@@ -7722,15 +7767,15 @@ async function entryProblem(projectRoot, entry) {
7722
7767
  if (!isInsideProjectRoot(projectRoot, abs)) {
7723
7768
  return `Path escapes project root: ${rel}`;
7724
7769
  }
7725
- let stat3;
7770
+ let stat4;
7726
7771
  try {
7727
- stat3 = await fs13.stat(abs);
7772
+ stat4 = await fs13.stat(abs);
7728
7773
  } catch {
7729
7774
  return `Not found: ${rel}`;
7730
7775
  }
7731
7776
  if (!heading)
7732
7777
  return null;
7733
- if (!stat3.isFile())
7778
+ if (!stat4.isFile())
7734
7779
  return `${rel} is a folder; a section needs a file`;
7735
7780
  const text = await fs13.readFile(abs, "utf8");
7736
7781
  if (sectionLines(text, heading))
@@ -8706,7 +8751,14 @@ async function isProposalStale(projectRoot, p) {
8706
8751
  }
8707
8752
  async function readDesignContext(projectRoot, opts = {}) {
8708
8753
  const state = await readDesignSources(projectRoot);
8709
- const files = selectDesignFiles(state.file.files, opts.urls).map((e) => resolveDesignFile(projectRoot, e));
8754
+ const files = await Promise.all(selectDesignFiles(state.file.files, opts.urls).map(async (e) => {
8755
+ const resolved = resolveDesignFile(projectRoot, e);
8756
+ if (resolved.kind === "design") {
8757
+ const rules = await canonicalRulesInFile(resolved);
8758
+ return rules.length > 0 ? { ...resolved, rules } : resolved;
8759
+ }
8760
+ return resolved;
8761
+ }));
8710
8762
  const proposals = [];
8711
8763
  for (const p of await listDesignProposals(projectRoot)) {
8712
8764
  proposals.push({
@@ -9635,7 +9687,7 @@ async function suppressionRules(projectRoot, outlineSlug) {
9635
9687
  if (!outlineSlug)
9636
9688
  return [];
9637
9689
  try {
9638
- return (await activeGuidanceFor(projectRoot, { outline: outlineSlug })).filter((r) => r.kind === "not_relevant").map((r) => ({ id: r.id, rule: r.rule }));
9690
+ return (await activeGuidanceFor(projectRoot, { outline: outlineSlug })).filter((r) => r.kind === "not_relevant" || r.kind === "out_of_scope").map((r) => ({ id: r.id, rule: r.rule }));
9639
9691
  } catch {
9640
9692
  return [];
9641
9693
  }
@@ -10536,6 +10588,63 @@ async function readDesignerIgnored(projectRoot) {
10536
10588
  return readForWrite(projectRoot).catch(() => []);
10537
10589
  }
10538
10590
  async function ignoreDesignerIssue(projectRoot, runId, issueId, opts = {}) {
10591
+ const reasonGiven = opts.reason?.trim() || null;
10592
+ const when = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
10593
+ const cascadeDismissSiblings = async (targetFm, note) => {
10594
+ const cid = targetFm.canonical_id;
10595
+ if (!cid)
10596
+ return;
10597
+ const { entities } = await listEntities(projectRoot, "finding").catch(() => ({ entities: [] }));
10598
+ for (const sibling of entities) {
10599
+ if (sibling.frontmatter.id === targetFm.id)
10600
+ continue;
10601
+ if (sibling.frontmatter.canonical_id !== cid)
10602
+ continue;
10603
+ if (sibling.frontmatter.triage_status === "dismissed")
10604
+ continue;
10605
+ await writeEntity(projectRoot, "finding", {
10606
+ ...sibling.frontmatter,
10607
+ triage_status: "dismissed",
10608
+ dismiss_reason: "not_relevant",
10609
+ triage_note: note,
10610
+ updated_at: when
10611
+ }, sibling.body);
10612
+ }
10613
+ };
10614
+ const trimmedRef = issueId.trim().toUpperCase();
10615
+ if (/^P\d+$/.test(trimmedRef)) {
10616
+ const refs2 = await fs21.readFile(designerRefsFile(runDir(projectRoot, runId)), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
10617
+ const findingId = refs2?.[trimmedRef];
10618
+ const pFile = findingId ? await readEntity(projectRoot, "finding", findingId).catch(() => null) : null;
10619
+ if (!pFile) {
10620
+ throw new DesignerRecordError("unknown_designer_finding", `${runId} has no designer finding ${issueId}`);
10621
+ }
10622
+ const already = pFile.frontmatter.triage_status === "dismissed";
10623
+ const note = reasonGiven ?? (pFile.frontmatter.triage_note?.trim() || "Dismissed via iamu designer-dismiss");
10624
+ if (!already || reasonGiven && reasonGiven !== pFile.frontmatter.triage_note) {
10625
+ await writeEntity(projectRoot, "finding", {
10626
+ ...pFile.frontmatter,
10627
+ triage_status: "dismissed",
10628
+ dismiss_reason: "not_relevant",
10629
+ triage_note: note,
10630
+ updated_at: when
10631
+ }, pFile.body);
10632
+ await cascadeDismissSiblings(pFile.frontmatter, note);
10633
+ }
10634
+ return {
10635
+ entry: {
10636
+ title: pFile.frontmatter.title ?? pFile.frontmatter.slug,
10637
+ screen: pFile.frontmatter.screen ?? null,
10638
+ what: pFile.frontmatter.what ?? "",
10639
+ severity: pFile.frontmatter.severity ?? void 0,
10640
+ reason: reasonGiven ?? pFile.frontmatter.triage_note ?? null,
10641
+ run: runId,
10642
+ issue: trimmedRef,
10643
+ at: when
10644
+ },
10645
+ already
10646
+ };
10647
+ }
10539
10648
  const record = await readDesignerRecord(projectRoot, runId);
10540
10649
  if (!record) {
10541
10650
  throw new DesignerRecordError("not_judged", `Run ${runId} has no designer's review to dismiss a finding of`);
@@ -10545,8 +10654,6 @@ async function ignoreDesignerIssue(projectRoot, runId, issueId, opts = {}) {
10545
10654
  if (!issue) {
10546
10655
  throw new DesignerRecordError("unknown_designer_finding", `${runId} has no designer finding ${issueId}`);
10547
10656
  }
10548
- const reasonGiven = opts.reason?.trim() || null;
10549
- const when = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
10550
10657
  const file = issue.finding_id ? await readEntity(projectRoot, "finding", issue.finding_id).catch(() => null) : null;
10551
10658
  if (file) {
10552
10659
  const already = file.frontmatter.triage_status === "dismissed";
@@ -10559,6 +10666,7 @@ async function ignoreDesignerIssue(projectRoot, runId, issueId, opts = {}) {
10559
10666
  triage_note: note,
10560
10667
  updated_at: when
10561
10668
  }, file.body);
10669
+ await cascadeDismissSiblings(file.frontmatter, note);
10562
10670
  }
10563
10671
  return {
10564
10672
  entry: {
@@ -10925,6 +11033,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
10925
11033
  return {
10926
11034
  id: `step-${stepNum}`,
10927
11035
  step_number: stepNum,
11036
+ ...typeof s.timestamp === "string" && s.timestamp ? { timestamp: s.timestamp } : {},
10928
11037
  screen_title: s.action,
10929
11038
  // Step files record the path relative to the run directory
10930
11039
  // (`screenshots/step-1.png`); older ones recorded the bare file
@@ -11423,7 +11532,7 @@ async function assembleWalkthroughBundle(projectRoot, runId, opts = {}) {
11423
11532
  scenarioIdForPush = manifest.scenario_slug;
11424
11533
  }
11425
11534
  }
11426
- const bundleSteps = steps2.map((s) => {
11535
+ const bundleSteps = steps2.map((s, idx) => {
11427
11536
  const shot = screenshots.get(s.step_number);
11428
11537
  const ref = shot ? {
11429
11538
  part_name: `screenshot_${s.step_number}`,
@@ -11431,7 +11540,7 @@ async function assembleWalkthroughBundle(projectRoot, runId, opts = {}) {
11431
11540
  content_type: shot.contentType
11432
11541
  } : null;
11433
11542
  return {
11434
- seq: s.step_number,
11543
+ seq: idx + 1,
11435
11544
  title: s.action,
11436
11545
  persona_reasoning: s.note ?? null,
11437
11546
  grounding: s.grounding ?? null,
@@ -17053,9 +17162,9 @@ function withSessionStateLock(filePath, fn) {
17053
17162
  const now = Date.now();
17054
17163
  if (rec === null) {
17055
17164
  try {
17056
- const stat3 = statSync(targetPath);
17057
- if (stat3.size === 0) {
17058
- const mtime = stat3.birthtimeMs && stat3.birthtimeMs > 0 ? stat3.birthtimeMs : stat3.mtimeMs;
17165
+ const stat4 = statSync(targetPath);
17166
+ if (stat4.size === 0) {
17167
+ const mtime = stat4.birthtimeMs && stat4.birthtimeMs > 0 ? stat4.birthtimeMs : stat4.mtimeMs;
17059
17168
  if (now - mtime <= staleMs)
17060
17169
  return false;
17061
17170
  }
@@ -18672,7 +18781,12 @@ async function buildSurfaceDossier(projectRoot, target, opts = {}) {
18672
18781
  seenDismissed.add(d.title);
18673
18782
  return true;
18674
18783
  });
18675
- const guidance = (await listEntities(projectRoot, "guidance")).entities.filter((g) => (g.frontmatter.status ?? "active") === "active").map((g) => g.body.trim().split(/\n\s*\n/)[0] ?? "").filter(Boolean);
18784
+ const guidance = (await listEntities(projectRoot, "guidance")).entities.filter((g) => (g.frontmatter.status ?? "active") === "active").map((g) => {
18785
+ const { rule, reason } = parseGuidanceBody(g.body);
18786
+ if (!rule)
18787
+ return "";
18788
+ return reason ? `${rule} (${reason})` : rule;
18789
+ }).filter(Boolean);
18676
18790
  const ids = new Set(allOnSurface.map((r) => r.fm.id));
18677
18791
  const directions = (await listDirections(projectRoot)).filter((d) => d.frontmatter.status !== "rejected" && d.frontmatter.absorbs.some((id) => ids.has(id))).map((d) => ({
18678
18792
  slug: d.frontmatter.slug,
@@ -20516,6 +20630,23 @@ async function buildRunDesignerInput(projectRoot, runId) {
20516
20630
  const ended = summary?.ended;
20517
20631
  const testability = testabilityPath(projectRoot);
20518
20632
  const pfm = persona?.frontmatter;
20633
+ const outlineSlug = outline?.frontmatter.slug ?? scenario?.frontmatter.outline ?? null;
20634
+ const guidanceRules = outlineSlug ? await activeGuidanceFor(projectRoot, { outline: outlineSlug }).catch(() => []) : (await listEntities(projectRoot, "guidance").catch(() => ({ entities: [] }))).entities.filter((g) => (g.frontmatter.status ?? "active") === "active").map((g) => {
20635
+ const { rule, reason: reason2 } = parseGuidanceBody(g.body);
20636
+ return { rule, reason: reason2 };
20637
+ }).filter((g) => Boolean(g.rule));
20638
+ const guidance = guidanceRules.map((r) => r.reason ? `${r.rule} (${r.reason})` : r.rule);
20639
+ const designFiles = await Promise.all(docs.map(async (f) => {
20640
+ const rules = f.kind === "design" ? f.rules ?? await canonicalRulesInFile(f) : void 0;
20641
+ return {
20642
+ entry: f.entry,
20643
+ kind: f.kind,
20644
+ abs: f.abs,
20645
+ heading: f.heading,
20646
+ applies_to: f.applies_to,
20647
+ ...rules && rules.length > 0 ? { rules } : {}
20648
+ };
20649
+ }));
20519
20650
  const bundle = {
20520
20651
  run_id: runId,
20521
20652
  project_root: projectRoot,
@@ -20540,13 +20671,7 @@ async function buildRunDesignerInput(projectRoot, runId) {
20540
20671
  ended: ended && typeof ended.note === "string" ? { reached: ended.reached === true, note: ended.note } : null
20541
20672
  } : null,
20542
20673
  design: docs.length > 0 || proposals.length > 0 ? {
20543
- files: docs.map((f) => ({
20544
- entry: f.entry,
20545
- kind: f.kind,
20546
- abs: f.abs,
20547
- heading: f.heading,
20548
- applies_to: f.applies_to
20549
- })),
20674
+ files: designFiles,
20550
20675
  proposals: proposals.map((p) => ({
20551
20676
  slug: p.slug,
20552
20677
  kind: p.kind,
@@ -20574,6 +20699,7 @@ async function buildRunDesignerInput(projectRoot, runId) {
20574
20699
  reason: e.reason
20575
20700
  }))
20576
20701
  ],
20702
+ guidance,
20577
20703
  facts: context?.facts ? designerFacts(context.facts) : null,
20578
20704
  setup_notes: {
20579
20705
  file: existsSync5(testability) ? testability : null,
@@ -20622,6 +20748,9 @@ var init_designer_input = __esm({
20622
20748
  init_advice_input();
20623
20749
  init_design_facts();
20624
20750
  init_design_proposals();
20751
+ init_design_rules();
20752
+ init_entity();
20753
+ init_guidance2();
20625
20754
  init_paths();
20626
20755
  init_review_issues();
20627
20756
  init_url_path();
@@ -20629,6 +20758,7 @@ var init_designer_input = __esm({
20629
20758
  init_designer_ignored();
20630
20759
  init_designer_ledger();
20631
20760
  init_walkthrough_canonical2();
20761
+ init_dist();
20632
20762
  LONG_VISIT = 6;
20633
20763
  FRAMES_PER_VISIT = 8;
20634
20764
  FRAMES_PER_RUN = 24;
@@ -22014,15 +22144,15 @@ function resolveApiUrl(configured) {
22014
22144
  assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
22015
22145
  return url;
22016
22146
  }
22017
- function parseStore(raw, path73) {
22147
+ function parseStore(raw, path74) {
22018
22148
  let parsed;
22019
22149
  try {
22020
22150
  parsed = JSON.parse(raw);
22021
22151
  } catch (err) {
22022
- throw new CredentialsFileError(path73, "not valid JSON", err);
22152
+ throw new CredentialsFileError(path74, "not valid JSON", err);
22023
22153
  }
22024
22154
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
22025
- throw new CredentialsFileError(path73, "expected a JSON object");
22155
+ throw new CredentialsFileError(path74, "expected a JSON object");
22026
22156
  }
22027
22157
  return parsed;
22028
22158
  }
@@ -22030,26 +22160,26 @@ function isNotFound(err) {
22030
22160
  return typeof err === "object" && err !== null && err.code === "ENOENT";
22031
22161
  }
22032
22162
  async function readCredentialsStore() {
22033
- const path73 = getCredentialsPath();
22163
+ const path74 = getCredentialsPath();
22034
22164
  let raw;
22035
22165
  try {
22036
- raw = await readFile(path73, "utf-8");
22166
+ raw = await readFile(path74, "utf-8");
22037
22167
  } catch (err) {
22038
22168
  if (isNotFound(err)) return {};
22039
22169
  warnUnreadableStoreOnce(err);
22040
22170
  throw err;
22041
22171
  }
22042
22172
  try {
22043
- return parseStore(raw, path73);
22173
+ return parseStore(raw, path74);
22044
22174
  } catch (err) {
22045
22175
  warnUnreadableStoreOnce(err);
22046
22176
  throw err;
22047
22177
  }
22048
22178
  }
22049
22179
  async function writeCredentialsStore(store) {
22050
- const path73 = getCredentialsPath();
22051
- await mkdir(dirname(path73), { recursive: true, mode: 448 });
22052
- await atomicWriteFile(path73, `${JSON.stringify(store, null, 2)}
22180
+ const path74 = getCredentialsPath();
22181
+ await mkdir(dirname(path74), { recursive: true, mode: 448 });
22182
+ await atomicWriteFile(path74, `${JSON.stringify(store, null, 2)}
22053
22183
  `, {
22054
22184
  mode: 384
22055
22185
  });
@@ -22077,16 +22207,16 @@ function resolveApiKey(apiUrl) {
22077
22207
  }
22078
22208
  }
22079
22209
  const normalized = url.replace(/\/+$/, "");
22080
- for (const path73 of candidatePaths) {
22210
+ for (const path74 of candidatePaths) {
22081
22211
  let raw;
22082
22212
  try {
22083
- raw = readFileSync2(path73, "utf-8");
22213
+ raw = readFileSync2(path74, "utf-8");
22084
22214
  } catch (err) {
22085
22215
  if (!isNotFound(err)) warnUnreadableStoreOnce(err);
22086
22216
  continue;
22087
22217
  }
22088
22218
  try {
22089
- const store = parseStore(raw, path73);
22219
+ const store = parseStore(raw, path74);
22090
22220
  const entry = store[url] ?? store[normalized] ?? store[`${normalized}/`];
22091
22221
  if (typeof entry !== "object" || entry === null) continue;
22092
22222
  const token = entry.token;
@@ -22127,12 +22257,12 @@ var init_credentials = __esm({
22127
22257
  * @param reason - What is wrong with it.
22128
22258
  * @param cause - Underlying parse error, if any.
22129
22259
  */
22130
- constructor(path73, reason, cause) {
22260
+ constructor(path74, reason, cause) {
22131
22261
  super(
22132
- `Credentials file ${path73} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
22262
+ `Credentials file ${path74} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
22133
22263
  {
22134
22264
  cause,
22135
- suggestedAction: `Delete ${path73} and run \`iamu auth login\`, or set IAMU_TOKEN.`
22265
+ suggestedAction: `Delete ${path74} and run \`iamu auth login\`, or set IAMU_TOKEN.`
22136
22266
  }
22137
22267
  );
22138
22268
  }
@@ -23027,8 +23157,8 @@ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
23027
23157
  import { fileURLToPath as fileURLToPath2 } from "url";
23028
23158
  import chalk6 from "chalk";
23029
23159
  function getVersion() {
23030
- if ("0.8.0-beta.2") {
23031
- return "0.8.0-beta.2";
23160
+ if ("0.8.0-beta.3") {
23161
+ return "0.8.0-beta.3";
23032
23162
  }
23033
23163
  try {
23034
23164
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -23140,16 +23270,16 @@ async function needsWrite(filePath, content) {
23140
23270
  return true;
23141
23271
  }
23142
23272
  }
23143
- function isCliWrittenSkill(path73) {
23273
+ function isCliWrittenSkill(path74) {
23144
23274
  try {
23145
- return /^generatedBy:/m.test(readFileSync6(path73, "utf-8"));
23275
+ return /^generatedBy:/m.test(readFileSync6(path74, "utf-8"));
23146
23276
  } catch {
23147
23277
  return false;
23148
23278
  }
23149
23279
  }
23150
- function isCliWrittenPrompt(path73) {
23280
+ function isCliWrittenPrompt(path74) {
23151
23281
  try {
23152
- const content = readFileSync6(path73, "utf-8");
23282
+ const content = readFileSync6(path74, "utf-8");
23153
23283
  return /^generatedBy:/m.test(content) || /^#\s+iamu:/m.test(content) || /^description:\s*.*iamu/im.test(content);
23154
23284
  } catch {
23155
23285
  return false;
@@ -23161,8 +23291,8 @@ function planSkillFiles(hosts, projectRoot, version) {
23161
23291
  for (const host of hosts) {
23162
23292
  if (pluginProvidesSkills(host, projectRoot)) {
23163
23293
  for (const name of skillTemplates.keys()) {
23164
- const path73 = join5(host.getSkillsDir(projectRoot), name, "SKILL.md");
23165
- if (isCliWrittenSkill(path73)) files.push({ host, path: path73, content: null });
23294
+ const path74 = join5(host.getSkillsDir(projectRoot), name, "SKILL.md");
23295
+ if (isCliWrittenSkill(path74)) files.push({ host, path: path74, content: null });
23166
23296
  }
23167
23297
  } else if (host.supportsSkills) {
23168
23298
  const skillsDir = host.getSkillsDir(projectRoot);
@@ -25192,13 +25322,13 @@ function git(args2, cwd) {
25192
25322
  }
25193
25323
  function hashFile(absPath) {
25194
25324
  const hash = createHash5("sha256");
25195
- const stat3 = lstatSync(absPath);
25196
- if (stat3.isSymbolicLink()) {
25325
+ const stat4 = lstatSync(absPath);
25326
+ if (stat4.isSymbolicLink()) {
25197
25327
  hash.update(`symlink:${readlinkSync(absPath)}`);
25198
25328
  return hash.digest("hex");
25199
25329
  }
25200
- if (!stat3.isFile()) {
25201
- hash.update(`${stat3.isDirectory() ? "dir" : "other"}`);
25330
+ if (!stat4.isFile()) {
25331
+ hash.update(`${stat4.isDirectory() ? "dir" : "other"}`);
25202
25332
  return hash.digest("hex");
25203
25333
  }
25204
25334
  const fd = openSync3(absPath, "r");
@@ -31459,12 +31589,20 @@ function awaitingSteps(projectRoot, findings2) {
31459
31589
  }
31460
31590
  function defaultRule(kind, findings2) {
31461
31591
  const titles = findings2.map((f) => f.frontmatter.title ?? f.frontmatter.ui_fact ?? f.slug).join("; ");
31462
- return kind === "walker_error" ? `Earlier walkers got this wrong: ${titles}` : `Don't report: ${titles}`;
31592
+ if (kind === "walker_error") {
31593
+ return `Earlier walkers got this wrong: ${titles}`;
31594
+ }
31595
+ if (kind === "out_of_scope") {
31596
+ return `Handled outside system; don't report: ${titles}`;
31597
+ }
31598
+ return `Don't report: ${titles}`;
31463
31599
  }
31464
31600
  async function resolveScope(projectRoot, flag, findings2, opts) {
31465
31601
  if (flag === "project") return {};
31602
+ const withScenario = findings2.filter((f) => Boolean(f.frontmatter.scenario));
31603
+ if (withScenario.length === 0) return {};
31466
31604
  const outlines = /* @__PURE__ */ new Set();
31467
- for (const f of findings2) {
31605
+ for (const f of withScenario) {
31468
31606
  const scenario = await safeReadEntity(
31469
31607
  projectRoot,
31470
31608
  "scenario",
@@ -31505,6 +31643,47 @@ async function resolveScope(projectRoot, flag, findings2, opts) {
31505
31643
  }
31506
31644
  return { capability };
31507
31645
  }
31646
+ async function resolveDesignerRefInTriage(projectRoot, ref) {
31647
+ const m = /^(?:([^#]+)#)?([DP]\d+)$/i.exec(ref.trim());
31648
+ if (!m) return null;
31649
+ const runPart = m[1]?.trim();
31650
+ const code = (m[2] ?? "").toUpperCase();
31651
+ const targetRunId = resolveRunId(projectRoot, runPart);
31652
+ if (!targetRunId) return null;
31653
+ if (code.startsWith("P")) {
31654
+ const refsRaw = await fs62.readFile(
31655
+ path59.join(runDir(projectRoot, targetRunId), DESIGNER_REFS_FILE),
31656
+ "utf8"
31657
+ ).catch(() => null);
31658
+ if (!refsRaw) return null;
31659
+ try {
31660
+ const parsed = JSON.parse(refsRaw);
31661
+ const findingId = parsed[code];
31662
+ if (findingId) {
31663
+ return await readEntity(
31664
+ projectRoot,
31665
+ "finding",
31666
+ findingId
31667
+ );
31668
+ }
31669
+ } catch {
31670
+ return null;
31671
+ }
31672
+ return null;
31673
+ }
31674
+ const rec = await readDesignerRecord(projectRoot, targetRunId).catch(
31675
+ () => null
31676
+ );
31677
+ const issue = rec?.issues.find((i) => i.id.toUpperCase() === code);
31678
+ if (issue?.finding_id) {
31679
+ return await readEntity(
31680
+ projectRoot,
31681
+ "finding",
31682
+ issue.finding_id
31683
+ );
31684
+ }
31685
+ return null;
31686
+ }
31508
31687
  async function triage(refs2, opts) {
31509
31688
  const ctx = await resolveProject(opts);
31510
31689
  if (!ctx) return;
@@ -31544,42 +31723,39 @@ async function triage(refs2, opts) {
31544
31723
  }
31545
31724
  reason = opts.reason;
31546
31725
  }
31547
- if (status2.data === "dismissed") {
31548
- reason ??= "not_relevant";
31549
- if (!opts.note?.trim()) {
31726
+ if (opts.duplicateOf !== void 0) {
31727
+ if (status2.data !== "dismissed") {
31728
+ commandError("--duplicate-of applies only to --status dismissed", {
31729
+ ...opts,
31730
+ code: "invalid_duplicate_of_usage",
31731
+ fix: `iamu triage ${refs2.join(" ")} --status dismissed --duplicate-of ${opts.duplicateOf}`
31732
+ });
31733
+ return;
31734
+ }
31735
+ if (reason && reason !== "duplicate") {
31550
31736
  commandError(
31551
- "Dismissing needs --note naming why (by design, out of scope, what the walker misread, duplicate of \u2026) \u2014 it teaches the next walk",
31737
+ `--duplicate-of requires --reason duplicate \u2014 got "${reason}"`,
31552
31738
  {
31553
31739
  ...opts,
31554
- code: "missing_dismiss_note",
31555
- fix: `iamu triage ${refs2.join(" ")} --status dismissed --note "by design"`
31740
+ code: "invalid_duplicate_of_usage",
31741
+ fix: `iamu triage ${refs2.join(" ")} --status dismissed --reason duplicate --duplicate-of ${opts.duplicateOf}`
31556
31742
  }
31557
31743
  );
31558
31744
  return;
31559
31745
  }
31746
+ reason = "duplicate";
31747
+ }
31748
+ if (status2.data === "dismissed") {
31749
+ reason ??= "not_relevant";
31560
31750
  }
31561
31751
  const isSweep = opts.sweep !== void 0 && opts.sweep !== false;
31562
31752
  const sweepQuery = typeof opts.sweep === "string" ? opts.sweep.trim() : "";
31563
31753
  if (isSweep && status2.data !== "dismissed" && status2.data !== "wont_fix") {
31564
- commandError(
31565
- "--sweep applies only when dismissing (--status dismissed or wont_fix)",
31566
- {
31567
- ...opts,
31568
- code: "invalid_sweep_usage",
31569
- fix: `iamu triage ${refs2.join(" ")} --status dismissed --note "by design" --sweep`
31570
- }
31571
- );
31572
- return;
31573
- }
31574
- if (isSweep && status2.data === "wont_fix" && !opts.note?.trim()) {
31575
- commandError(
31576
- "Dismissing with --sweep needs --note naming why \u2014 it teaches the whole project",
31577
- {
31578
- ...opts,
31579
- code: "missing_dismiss_note",
31580
- fix: `iamu triage ${refs2.join(" ")} --status wont_fix --note "by design" --sweep`
31581
- }
31582
- );
31754
+ commandError("--sweep applies only when dismissing (--status dismissed)", {
31755
+ ...opts,
31756
+ code: "invalid_sweep_usage",
31757
+ fix: `iamu triage ${refs2.join(" ")} --status dismissed --sweep`
31758
+ });
31583
31759
  return;
31584
31760
  }
31585
31761
  if (isSweep && opts.scope !== void 0) {
@@ -31622,6 +31798,9 @@ async function triage(refs2, opts) {
31622
31798
  "finding",
31623
31799
  ref
31624
31800
  );
31801
+ if (!entity) {
31802
+ entity = await resolveDesignerRefInTriage(projectRoot, ref);
31803
+ }
31625
31804
  } catch (err) {
31626
31805
  commandError(
31627
31806
  `Finding ${ref} does not validate: ${err instanceof Error ? err.message.split("\n")[0] : String(err)} \u2014 run \`iamu validate --all\``,
@@ -31698,29 +31877,25 @@ async function triage(refs2, opts) {
31698
31877
  }
31699
31878
  const allTargets = [...loaded, ...swept];
31700
31879
  const persona = allTargets.filter((f) => isPersonaFinding(f.frontmatter));
31701
- if (!isSweep && opts.rule !== void 0 && persona.length < loaded.length) {
31702
- commandError(
31703
- "--rule teaches the walker, and a designer finding is not the walker's: dismiss it without --rule (later designer's reviews are told not to raise it again)",
31704
- {
31705
- ...opts,
31706
- code: "rule_for_designer_finding",
31707
- fix: `iamu triage ${refs2.join(" ")} --status dismissed --note "<why>"`
31708
- }
31709
- );
31710
- return;
31711
- }
31712
31880
  const effectiveReason = reason ?? (isSweep ? "not_relevant" : null);
31713
- const teaches = (status2.data === "dismissed" || isSweep && status2.data === "wont_fix") && (effectiveReason === "not_relevant" || effectiveReason === "walker_error") && opts.guidance !== false && (persona.length > 0 || isSweep);
31881
+ const duplicateTarget = status2.data === "dismissed" && effectiveReason === "duplicate" ? opts.duplicateOf?.trim() || null : null;
31882
+ const teaches = (status2.data === "dismissed" || isSweep && status2.data === "wont_fix") && (effectiveReason === "not_relevant" || effectiveReason === "walker_error" || effectiveReason === "out_of_scope") && opts.guidance !== false && (persona.length > 0 || isSweep || opts.rule !== void 0);
31714
31883
  let scope = {};
31715
31884
  if (teaches && !isSweep) {
31716
- const resolved = await resolveScope(projectRoot, scopeFlag, persona, opts);
31885
+ const scopeTargets = persona.length > 0 ? persona : allTargets;
31886
+ const resolved = await resolveScope(
31887
+ projectRoot,
31888
+ scopeFlag,
31889
+ scopeTargets,
31890
+ opts
31891
+ );
31717
31892
  if (!resolved) return;
31718
31893
  scope = resolved;
31719
31894
  }
31720
31895
  let guidance = null;
31721
31896
  if (teaches && effectiveReason) {
31722
31897
  const ruleSources = allTargets.length > 0 ? allTargets : persona;
31723
- const ruleText = opts.rule?.trim() || (sweepQuery && ruleSources.length === 0 ? effectiveReason === "walker_error" ? `Earlier walkers got this wrong: ${sweepQuery}` : `Don't report: ${sweepQuery}` : defaultRule(effectiveReason, ruleSources));
31898
+ const ruleText = opts.rule?.trim() || (sweepQuery && ruleSources.length === 0 ? effectiveReason === "walker_error" ? `Earlier walkers got this wrong: ${sweepQuery}` : effectiveReason === "out_of_scope" ? `Handled outside system; don't report: ${sweepQuery}` : `Don't report: ${sweepQuery}` : defaultRule(effectiveReason, ruleSources));
31724
31899
  const rule = await createGuidanceRule(projectRoot, {
31725
31900
  kind: effectiveReason,
31726
31901
  rule: ruleText,
@@ -31761,7 +31936,8 @@ async function triage(refs2, opts) {
31761
31936
  updated_at: now,
31762
31937
  addressed_at: addressedAt(f.frontmatter),
31763
31938
  dismiss_reason: status2.data === "dismissed" ? effectiveReason : null,
31764
- ...isSweep && guidance ? { suppressed_by_guidance: guidance.id } : {}
31939
+ duplicate_of: duplicateTarget,
31940
+ ...(isSweep || !isPersonaFinding(f.frontmatter)) && guidance ? { suppressed_by_guidance: guidance.id } : {}
31765
31941
  };
31766
31942
  if (opts.note !== void 0) fm.triage_note = opts.note.trim();
31767
31943
  await writeEntity(projectRoot, "finding", fm, f.body);
@@ -31789,6 +31965,7 @@ async function triage(refs2, opts) {
31789
31965
  updated_at: now,
31790
31966
  addressed_at: addressedAt(c.frontmatter),
31791
31967
  dismiss_reason: status2.data === "dismissed" ? effectiveReason : null,
31968
+ duplicate_of: duplicateTarget,
31792
31969
  ...isSweep && guidance ? { suppressed_by_guidance: guidance.id } : {},
31793
31970
  ...opts.note !== void 0 ? { triage_note: opts.note.trim() } : {}
31794
31971
  },
@@ -32876,7 +33053,7 @@ var init_browse_driver = __esm({
32876
33053
  });
32877
33054
 
32878
33055
  // src/commands/step-numbering.ts
32879
- import { open, readdir, unlink } from "fs/promises";
33056
+ import { open, readdir, stat as stat2, unlink } from "fs/promises";
32880
33057
  import path61 from "path";
32881
33058
  function stepFileName(stepNumber2) {
32882
33059
  return `${String(stepNumber2).padStart(3, "0")}.json`;
@@ -32889,7 +33066,16 @@ async function peekNextStepNumber(stepsDir) {
32889
33066
  let max = 0;
32890
33067
  for (const f of files) {
32891
33068
  const m = /^(\d+)\.json$/.exec(f);
32892
- if (m) max = Math.max(max, Number(m[1]));
33069
+ if (!m) continue;
33070
+ const fullPath = path61.join(stepsDir, f);
33071
+ const st = await stat2(fullPath).catch(() => null);
33072
+ if (!st) continue;
33073
+ if (st.size === 0 && Date.now() - st.mtimeMs > STALE_RESERVATION_MS) {
33074
+ await unlink(fullPath).catch(() => {
33075
+ });
33076
+ continue;
33077
+ }
33078
+ max = Math.max(max, Number(m[1]));
32893
33079
  }
32894
33080
  return max + 1;
32895
33081
  }
@@ -32912,13 +33098,21 @@ async function reserveStepFile(stepsDir, limit) {
32912
33098
  };
32913
33099
  } catch (err) {
32914
33100
  if (err.code !== "EEXIST") throw err;
33101
+ const st = await stat2(stepPath).catch(() => null);
33102
+ if (st && st.size === 0 && Date.now() - st.mtimeMs > STALE_RESERVATION_MS) {
33103
+ await unlink(stepPath).catch(() => {
33104
+ });
33105
+ continue;
33106
+ }
32915
33107
  candidate = Math.max(candidate + 1, await peekNextStepNumber(stepsDir));
32916
33108
  }
32917
33109
  }
32918
33110
  }
33111
+ var STALE_RESERVATION_MS;
32919
33112
  var init_step_numbering = __esm({
32920
33113
  "src/commands/step-numbering.ts"() {
32921
33114
  "use strict";
33115
+ STALE_RESERVATION_MS = 2e3;
32922
33116
  }
32923
33117
  });
32924
33118
 
@@ -32930,7 +33124,7 @@ __export(step_exports, {
32930
33124
  recordStep: () => recordStep,
32931
33125
  stepCommand: () => stepCommand
32932
33126
  });
32933
- import { copyFile, mkdir as mkdir4, rename, stat as stat2 } from "fs/promises";
33127
+ import { copyFile, mkdir as mkdir4, rename, stat as stat3 } from "fs/promises";
32934
33128
  import path62 from "path";
32935
33129
  import chalk30 from "chalk";
32936
33130
  function parseMaxSteps(val) {
@@ -32960,9 +33154,10 @@ async function recordStep(input) {
32960
33154
  runId: input.runId
32961
33155
  });
32962
33156
  const { stepNumber: stepNumber2, stepPath } = reservation;
33157
+ let parsedStep;
33158
+ let finalScreenshotPath = null;
32963
33159
  try {
32964
33160
  let screenshotRef = null;
32965
- let finalScreenshotPath = null;
32966
33161
  if (shotSource) {
32967
33162
  const destName = `step-${stepNumber2}.png`;
32968
33163
  const destPath = path62.join(screenshotsDir, destName);
@@ -32978,7 +33173,7 @@ async function recordStep(input) {
32978
33173
  }
32979
33174
  screenshotRef = path62.posix.join("screenshots", destName);
32980
33175
  }
32981
- const parsedStep = runStepSchema.parse({
33176
+ parsedStep = runStepSchema.parse({
32982
33177
  step_number: stepNumber2,
32983
33178
  timestamp: input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
32984
33179
  action: input.action.trim(),
@@ -32990,33 +33185,33 @@ async function recordStep(input) {
32990
33185
  ...input.extra ? { extra: input.extra } : {}
32991
33186
  });
32992
33187
  await writeStep(runDirPath, parsedStep);
32993
- let cancelled = false;
32994
- try {
32995
- const liveRes = await sendLiveStep(
32996
- input.projectRoot,
32997
- input.runId,
32998
- parsedStep,
32999
- finalScreenshotPath
33000
- );
33001
- if (liveRes.cancelled) {
33002
- cancelled = true;
33003
- }
33004
- } catch {
33005
- }
33006
- return {
33007
- step: parsedStep,
33008
- stepPath,
33009
- screenshotPath: finalScreenshotPath,
33010
- cancelled
33011
- };
33012
33188
  } catch (err) {
33013
33189
  await reservation.release();
33014
33190
  throw err;
33015
33191
  }
33192
+ let cancelled = false;
33193
+ try {
33194
+ const liveRes = await sendLiveStep(
33195
+ input.projectRoot,
33196
+ input.runId,
33197
+ parsedStep,
33198
+ finalScreenshotPath
33199
+ );
33200
+ if (liveRes.cancelled) {
33201
+ cancelled = true;
33202
+ }
33203
+ } catch {
33204
+ }
33205
+ return {
33206
+ step: parsedStep,
33207
+ stepPath,
33208
+ screenshotPath: finalScreenshotPath,
33209
+ cancelled
33210
+ };
33016
33211
  }
33017
33212
  async function existingShot(shot) {
33018
33213
  const resolved = path62.resolve(shot);
33019
- const isFile = await stat2(resolved).then((s) => s.isFile()).catch(() => false);
33214
+ const isFile = await stat3(resolved).then((s) => s.isFile()).catch(() => false);
33020
33215
  if (!isFile) {
33021
33216
  throw new Error(
33022
33217
  `Screenshot not found: ${resolved} \u2014 save the screenshot first, or omit --shot.`
@@ -33352,6 +33547,7 @@ function chromeArgs(opts, port, headless, viewport) {
33352
33547
  `--window-size=${win.width},${win.height}`,
33353
33548
  "--no-first-run",
33354
33549
  "--no-default-browser-check",
33550
+ "--disable-features=Translate",
33355
33551
  "--password-store=basic",
33356
33552
  "--use-mock-keychain",
33357
33553
  ...headless ? ["--headless=new", "--disable-dev-shm-usage", "--disable-gpu"] : [],
@@ -38351,7 +38547,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
38351
38547
  const byKey = /* @__PURE__ */ new Map();
38352
38548
  const byPlaceView = /* @__PURE__ */ new Map();
38353
38549
  const signatureOf = (s) => exploreSignature(s.headings, s.title);
38354
- function addSurface(state, place, view, path73, depth) {
38550
+ function addSurface(state, place, view, path74, depth) {
38355
38551
  const key = exploreSurfaceKey(place, view);
38356
38552
  const surface = {
38357
38553
  key,
@@ -38363,7 +38559,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
38363
38559
  headings: state.headings,
38364
38560
  fields: state.fields,
38365
38561
  depth,
38366
- path: path73,
38562
+ path: path74,
38367
38563
  actions: []
38368
38564
  };
38369
38565
  surfaces.push(surface);
@@ -38381,9 +38577,9 @@ ${surface.signature}`, key);
38381
38577
  });
38382
38578
  await settle(page, settleMs);
38383
38579
  }
38384
- async function replay(path73) {
38385
- if (!("goto" in (path73[0] ?? {}))) await goto(opts.startUrl);
38386
- for (const step of path73) {
38580
+ async function replay(path74) {
38581
+ if (!("goto" in (path74[0] ?? {}))) await goto(opts.startUrl);
38582
+ for (const step of path74) {
38387
38583
  if ("goto" in step) {
38388
38584
  await goto(step.goto);
38389
38585
  } else {
@@ -38507,8 +38703,8 @@ ${sig}`, key);
38507
38703
  if (stats.surfaces >= budget.max_surfaces) {
38508
38704
  stats.truncated = true;
38509
38705
  } else {
38510
- const path73 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
38511
- addSurface(after, place, view, path73, surface.depth + 1);
38706
+ const path74 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
38707
+ addSurface(after, place, view, path74, surface.depth + 1);
38512
38708
  }
38513
38709
  }
38514
38710
  }
@@ -39433,7 +39629,9 @@ function guidanceLine(r) {
39433
39629
  return r.reason ? `- ${rule} (${r.reason.replace(/\s+/g, " ")})` : `- ${rule}`;
39434
39630
  }
39435
39631
  function guidanceSection(rules) {
39436
- const notRelevant = rules.filter((r) => r.kind === "not_relevant");
39632
+ const notRelevant = rules.filter(
39633
+ (r) => r.kind === "not_relevant" || r.kind === "out_of_scope"
39634
+ );
39437
39635
  const misreads = rules.filter((r) => r.kind === "walker_error");
39438
39636
  if (notRelevant.length === 0 && misreads.length === 0) return [];
39439
39637
  const out2 = ["", "Tester guidance \u2014 the team's decisions from earlier runs:"];
@@ -39520,7 +39718,8 @@ function composeWalkerPrompt(p) {
39520
39718
  "Squinting Guardrails:",
39521
39719
  "\u2022 If the exact target record/option is not available, apply the approximation strategy above to pick the closest plausible entity within the domain flow.",
39522
39720
  "\u2022 Squinting does NOT permit drifting to unrelated settings, account profiles, or off-target pages. Stay strictly within the scenario journey.",
39523
- "\u2022 If the system is completely empty or unseeded, and your persona role cannot create data, do NOT report false product bugs about the empty state. End the walk as blocked (below)."
39721
+ "\u2022 If the system is completely empty or unseeded, and your persona role cannot create data, do NOT report false product bugs about the empty state. End the walk as blocked (below).",
39722
+ "\u2022 If the expected state includes seeded defects inside an inspected subject (e.g. an embedded app preview, screenshot, submission, or failing check that your persona is supposed to review or flag), treat those defects as the subject data to act on \u2014 never report them as bugs in the host product itself."
39524
39723
  ].filter(Boolean) : [],
39525
39724
  ...p.untestableBoundaries && p.untestableBoundaries.length > 0 ? [
39526
39725
  "",
@@ -41097,6 +41296,235 @@ var init_designer = __esm({
41097
41296
  }
41098
41297
  });
41099
41298
 
41299
+ // src/commands/post-walk.ts
41300
+ var post_walk_exports = {};
41301
+ __export(post_walk_exports, {
41302
+ postWalk: () => postWalk
41303
+ });
41304
+ import { existsSync as existsSync23, promises as fs71 } from "fs";
41305
+ import path73 from "path";
41306
+ import chalk49 from "chalk";
41307
+ async function captureStageJson(fn) {
41308
+ const captured = [];
41309
+ const origLog = console.log;
41310
+ const origErr = console.error;
41311
+ console.log = (...args2) => {
41312
+ captured.push(args2.map(String).join(" "));
41313
+ };
41314
+ console.error = () => {
41315
+ };
41316
+ try {
41317
+ await fn();
41318
+ } finally {
41319
+ console.log = origLog;
41320
+ console.error = origErr;
41321
+ }
41322
+ const last = captured[captured.length - 1];
41323
+ if (!last) return null;
41324
+ try {
41325
+ return JSON.parse(last);
41326
+ } catch {
41327
+ return null;
41328
+ }
41329
+ }
41330
+ async function postWalk(runIdArg, opts = {}) {
41331
+ const ctx = await resolveProject(opts);
41332
+ if (!ctx) return;
41333
+ const { projectRoot } = ctx;
41334
+ const runId = resolveRunId(projectRoot, runIdArg ?? opts.run);
41335
+ if (!runId) {
41336
+ commandError(
41337
+ "Run ID is required. Pass <run-id>, specify --run <run-id>, or start a run first.",
41338
+ {
41339
+ ...opts,
41340
+ code: "missing_run_id",
41341
+ fix: "iamu status --json"
41342
+ }
41343
+ );
41344
+ return;
41345
+ }
41346
+ const runDirPath = resolveRunDir(projectRoot, runId, opts);
41347
+ if (!runDirPath) return;
41348
+ const manifestPath = path73.join(runDirPath, "walkthrough.json");
41349
+ let manifest;
41350
+ try {
41351
+ manifest = JSON.parse(
41352
+ await fs71.readFile(manifestPath, "utf8")
41353
+ );
41354
+ } catch {
41355
+ commandError(`Run not found: ${runId}`, {
41356
+ ...opts,
41357
+ code: "run_not_found",
41358
+ fix: "iamu status --json"
41359
+ });
41360
+ return;
41361
+ }
41362
+ if (manifest.status !== "completed") {
41363
+ await captureStageJson(
41364
+ () => complete(runId, { prompt: false, push: false, json: true })
41365
+ );
41366
+ if (process.exitCode && process.exitCode !== 0) return;
41367
+ manifest = JSON.parse(
41368
+ await fs71.readFile(manifestPath, "utf8")
41369
+ );
41370
+ }
41371
+ if (manifest.blocked) {
41372
+ const blockedResult = {
41373
+ run_id: runId,
41374
+ status: "blocked",
41375
+ blocked: manifest.blocked,
41376
+ recorded: [],
41377
+ stages: null
41378
+ };
41379
+ if (opts.json) {
41380
+ console.log(JSON.stringify(blockedResult, null, 2));
41381
+ } else {
41382
+ console.log(
41383
+ ` ${chalk49.yellow("!")} Run ${runId} is blocked: ${manifest.blocked.reason}`
41384
+ );
41385
+ }
41386
+ return;
41387
+ }
41388
+ const recordedNow = [];
41389
+ const matchesAnswerPath = path73.join(runDirPath, "matches.json");
41390
+ if (existsSync23(matchesAnswerPath)) {
41391
+ const res = await captureStageJson(
41392
+ () => matchRecord(runId, { file: matchesAnswerPath, json: true })
41393
+ );
41394
+ if (res && !process.exitCode) {
41395
+ recordedNow.push("match");
41396
+ await fs71.unlink(matchesAnswerPath).catch(() => {
41397
+ });
41398
+ }
41399
+ }
41400
+ const judgeAnswerPath = path73.join(runDirPath, "judge_answer.json");
41401
+ if (existsSync23(judgeAnswerPath)) {
41402
+ const res = await captureStageJson(
41403
+ () => evaluateRecord(runId, { file: judgeAnswerPath, json: true })
41404
+ );
41405
+ if (res && !process.exitCode) {
41406
+ recordedNow.push("evaluate");
41407
+ await fs71.unlink(judgeAnswerPath).catch(() => {
41408
+ });
41409
+ }
41410
+ }
41411
+ const adviseAnswerPath = path73.join(runDirPath, "advise_answer.json");
41412
+ if (!opts.critic && !existsSync23(path73.join(runDirPath, ADVICE_FILE)) && existsSync23(adviseAnswerPath)) {
41413
+ const res = await captureStageJson(
41414
+ () => adviseRecord(runId, { file: adviseAnswerPath, json: true })
41415
+ );
41416
+ if (res && !process.exitCode) {
41417
+ recordedNow.push("advise");
41418
+ await fs71.unlink(adviseAnswerPath).catch(() => {
41419
+ });
41420
+ }
41421
+ }
41422
+ const designerAnswerPath = path73.join(runDirPath, DESIGNER_ANSWER_FILE);
41423
+ if (!existingDesignerFile(runDirPath)) {
41424
+ if (opts.critic) {
41425
+ await captureStageJson(
41426
+ () => designerRecord(runId, { skipped: "--critic", json: true })
41427
+ );
41428
+ recordedNow.push("designer(skipped)");
41429
+ } else if (opts.ux === false) {
41430
+ await captureStageJson(
41431
+ () => designerRecord(runId, { skipped: "--no-ux", json: true })
41432
+ );
41433
+ recordedNow.push("designer(skipped)");
41434
+ } else if (existsSync23(designerAnswerPath)) {
41435
+ const res = await captureStageJson(
41436
+ () => designerRecord(runId, {
41437
+ file: designerAnswerPath,
41438
+ byOrchestrator: opts.byOrchestrator,
41439
+ json: true
41440
+ })
41441
+ );
41442
+ if (res && !process.exitCode) {
41443
+ recordedNow.push("designer");
41444
+ await fs71.unlink(designerAnswerPath).catch(() => {
41445
+ });
41446
+ }
41447
+ }
41448
+ }
41449
+ const matchInfo = await captureStageJson(() => match(runId, { json: true }));
41450
+ const evalRecorded = existsSync23(path73.join(runDirPath, "verdicts.json"));
41451
+ const evalInfo = evalRecorded ? null : await captureStageJson(() => evaluate(runId, { json: true }));
41452
+ const adviseRecorded = existsSync23(path73.join(runDirPath, ADVICE_FILE));
41453
+ const adviseInfo = opts.critic || adviseRecorded ? null : await captureStageJson(() => advise(runId, { json: true }));
41454
+ const designerRecorded = Boolean(existingDesignerFile(runDirPath));
41455
+ const designerInfo = opts.critic || opts.ux === false || designerRecorded ? null : await captureStageJson(() => designer(runId, { json: true }));
41456
+ const payload = {
41457
+ run_id: runId,
41458
+ status: "completed",
41459
+ recorded: recordedNow,
41460
+ stages: {
41461
+ match: {
41462
+ needed: Boolean(matchInfo?.needed),
41463
+ prompt_file: matchInfo?.prompt_file ?? null,
41464
+ prompt_file_abs: matchInfo?.prompt_file_abs ?? null,
41465
+ answer_file_abs: matchesAnswerPath,
41466
+ record_command: `iamu match-record ${runId} --file - <<'EOF'`
41467
+ },
41468
+ evaluate: {
41469
+ needed: !evalRecorded,
41470
+ recorded: evalRecorded,
41471
+ prompt_file: evalInfo?.prompt_file ?? null,
41472
+ prompt_file_abs: evalInfo?.prompt_file_abs ?? null,
41473
+ answer_file_abs: judgeAnswerPath,
41474
+ record_command: `iamu evaluate-record ${runId} --file - <<'EOF'`
41475
+ },
41476
+ advise: {
41477
+ needed: Boolean(adviseInfo?.needed),
41478
+ recorded: adviseRecorded,
41479
+ reason: opts.critic ? "--critic" : adviseInfo?.reason ?? null,
41480
+ prompt_file: adviseInfo?.prompt_file ?? null,
41481
+ prompt_file_abs: adviseInfo?.prompt_file_abs ?? null,
41482
+ answer_file_abs: adviseAnswerPath,
41483
+ record_command: `iamu advise-record ${runId} --file - <<'EOF'`
41484
+ },
41485
+ designer: {
41486
+ needed: Boolean(designerInfo?.needed),
41487
+ recorded: designerRecorded,
41488
+ reason: opts.critic ? "--critic" : opts.ux === false ? "--no-ux" : designerInfo?.reason ?? null,
41489
+ prompt_file: designerInfo?.prompt_file ?? null,
41490
+ prompt_file_abs: designerInfo?.prompt_file_abs ?? null,
41491
+ answer_file_abs: designerAnswerPath,
41492
+ key_screenshots: designerInfo?.key_screenshots ?? [],
41493
+ offer_template: Boolean(designerInfo?.offer_template),
41494
+ record_command: `iamu designer-record ${runId} --file - <<'EOF'`
41495
+ }
41496
+ }
41497
+ };
41498
+ if (opts.json) {
41499
+ console.log(JSON.stringify(payload, null, 2));
41500
+ return;
41501
+ }
41502
+ console.log();
41503
+ console.log(
41504
+ ` ${chalk49.green("\u2713")} Post-walk pipeline for ${chalk49.cyan(runId)}`
41505
+ );
41506
+ if (recordedNow.length > 0) {
41507
+ console.log(` Recorded: ${recordedNow.join(", ")}`);
41508
+ }
41509
+ for (const [name, s] of Object.entries(payload.stages)) {
41510
+ const state = "recorded" in s && s.recorded ? chalk49.green("recorded") : s.needed ? chalk49.yellow(`needed (${s.prompt_file})`) : chalk49.dim("not needed");
41511
+ console.log(` ${name}: ${state}`);
41512
+ }
41513
+ console.log();
41514
+ }
41515
+ var init_post_walk = __esm({
41516
+ "src/commands/post-walk.ts"() {
41517
+ "use strict";
41518
+ init_dist2();
41519
+ init_advise();
41520
+ init_complete();
41521
+ init_context4();
41522
+ init_designer();
41523
+ init_evaluate();
41524
+ init_match();
41525
+ }
41526
+ });
41527
+
41100
41528
  // src/commands/prune.ts
41101
41529
  var prune_exports = {};
41102
41530
  __export(prune_exports, {
@@ -41104,7 +41532,7 @@ __export(prune_exports, {
41104
41532
  prune: () => prune
41105
41533
  });
41106
41534
  import { confirm, isCancel as isCancel5 } from "@clack/prompts";
41107
- import chalk49 from "chalk";
41535
+ import chalk50 from "chalk";
41108
41536
  function formatBytes(bytes) {
41109
41537
  if (bytes <= 0) return "0 B";
41110
41538
  if (bytes < 1024) return `${bytes} B`;
@@ -41150,7 +41578,7 @@ async function prune(opts = {}) {
41150
41578
  });
41151
41579
  if (preview.prunedRuns.length === 0) {
41152
41580
  console.log(
41153
- chalk49.dim(" No runs match the pruning criteria. Nothing to prune.")
41581
+ chalk50.dim(" No runs match the pruning criteria. Nothing to prune.")
41154
41582
  );
41155
41583
  return preview;
41156
41584
  }
@@ -41159,7 +41587,7 @@ async function prune(opts = {}) {
41159
41587
  message: `Prune ${mode} ${preview.prunedRuns.length} run(s) and reclaim ${formatBytes(preview.totalBytesReclaimed)}?`
41160
41588
  });
41161
41589
  if (isCancel5(confirmed) || !confirmed) {
41162
- console.log(chalk49.yellow(" Pruning cancelled."));
41590
+ console.log(chalk50.yellow(" Pruning cancelled."));
41163
41591
  return;
41164
41592
  }
41165
41593
  }
@@ -41170,14 +41598,14 @@ async function prune(opts = {}) {
41170
41598
  }
41171
41599
  if (result.dryRun) {
41172
41600
  console.log(
41173
- chalk49.cyan(
41601
+ chalk50.cyan(
41174
41602
  `
41175
41603
  [DRY RUN] Would prune ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
41176
41604
  )
41177
41605
  );
41178
41606
  } else {
41179
41607
  console.log(
41180
- chalk49.bold(
41608
+ chalk50.bold(
41181
41609
  `
41182
41610
  ${symbols.success} Pruned ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
41183
41611
  )
@@ -41185,18 +41613,18 @@ async function prune(opts = {}) {
41185
41613
  }
41186
41614
  if (result.prunedRuns.length === 0) {
41187
41615
  console.log(
41188
- chalk49.dim(" No runs matched the pruning criteria. Nothing pruned.")
41616
+ chalk50.dim(" No runs matched the pruning criteria. Nothing pruned.")
41189
41617
  );
41190
41618
  } else {
41191
41619
  for (const run of result.prunedRuns) {
41192
41620
  const mode = run.fullyDeleted ? "removed" : "screenshots purged";
41193
41621
  const count = run.screenshotsDeleted > 0 ? `${run.screenshotsDeleted} screenshots, ` : "";
41194
41622
  console.log(
41195
- ` ${chalk49.dim("\u2022")} ${chalk49.bold(run.runId)} ${chalk49.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk49.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
41623
+ ` ${chalk50.dim("\u2022")} ${chalk50.bold(run.runId)} ${chalk50.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk50.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
41196
41624
  );
41197
41625
  }
41198
41626
  console.log(
41199
- chalk49.dim(
41627
+ chalk50.dim(
41200
41628
  `
41201
41629
  Total reclaimed: ${formatBytes(result.totalBytesReclaimed)} (${result.totalScreenshotsDeleted} screenshots deleted)
41202
41630
  `
@@ -41501,12 +41929,12 @@ function registerFindingsCommands(program) {
41501
41929
  await findings2(opts);
41502
41930
  }
41503
41931
  );
41504
- program.command("triage [findings...]").helpGroup("Findings:").description("Set findings' triage status").requiredOption(
41505
- "--status <status>",
41506
- "open, addressed, deferred, wont_fix or dismissed"
41507
- ).option("--note <text>", "Why \u2014 required when dismissing").option(
41932
+ program.command("triage [findings...]").helpGroup("Findings:").description("Set findings' triage status").requiredOption("--status <status>", "open, addressed or dismissed").option("--note <text>", "Optional triage note or reason").option(
41508
41933
  "--reason <reason>",
41509
- "Dismiss reason: not_relevant (default), walker_error or duplicate"
41934
+ "Dismiss reason: not_relevant (default), walker_error, out_of_scope or duplicate"
41935
+ ).option(
41936
+ "--duplicate-of <finding>",
41937
+ "Finding slug or id this repeats (implies --reason duplicate)"
41510
41938
  ).option(
41511
41939
  "--rule <text>",
41512
41940
  `Guidance rule text (default: "Don't report: <title>")`
@@ -41518,7 +41946,7 @@ function registerFindingsCommands(program) {
41518
41946
  "Project-wide sweep ('we never want X'): dismiss all matching open persona & designer findings across every scenario/surface and record a project-wide guidance rule"
41519
41947
  ).option("--no-guidance", "Dismiss without writing a tester guidance rule").option("--json", "Output as JSON").addHelpText(
41520
41948
  "after",
41521
- "\nDismissing as not_relevant or walker_error teaches the walker: it writes a\ntester guidance rule (skip it with --no-guidance)."
41949
+ "\nDismissing as not_relevant, walker_error, or out_of_scope teaches the walker: it writes a\ntester guidance rule (skip it with --no-guidance)."
41522
41950
  ).action(
41523
41951
  async (refs2, opts) => {
41524
41952
  const { triage: triage2 } = await Promise.resolve().then(() => (init_triage(), triage_exports));
@@ -41617,9 +42045,9 @@ function registerProjectCommands(program) {
41617
42045
  "after",
41618
42046
  "\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
41619
42047
  ).action(
41620
- async (path73, opts) => {
42048
+ async (path74, opts) => {
41621
42049
  const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
41622
- await validate2(path73, opts);
42050
+ await validate2(path74, opts);
41623
42051
  }
41624
42052
  );
41625
42053
  program.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
@@ -42061,6 +42489,20 @@ function registerWalkthroughCommands(program) {
42061
42489
  await complete2(runId, opts);
42062
42490
  }
42063
42491
  );
42492
+ program.command("post-walk [run-id]").helpGroup("Walkthrough:").description(
42493
+ "Complete a run, record staged answers, and prepare post-walk prompts"
42494
+ ).option("--run <run-id>", "Run ID (or pass as positional argument)").option("--critic", "Skip advisor and mark designer skipped (--critic)").option("--no-ux", "Mark designer's review skipped (--no-ux)").option(
42495
+ "--by-orchestrator",
42496
+ "Mark recorded designer answer as written by the orchestrator"
42497
+ ).option("--json", "Output post-walk status and prompt paths as JSON").addHelpText(
42498
+ "after",
42499
+ "\nExamples:\n $ iamu post-walk <run-id> --json\n $ iamu post-walk <run-id> --critic --json"
42500
+ ).action(
42501
+ async (runId, opts) => {
42502
+ const { postWalk: postWalk2 } = await Promise.resolve().then(() => (init_post_walk(), post_walk_exports));
42503
+ await postWalk2(runId, opts);
42504
+ }
42505
+ );
42064
42506
  program.command("match [run-id]").helpGroup("Walkthrough:").description(
42065
42507
  "Compose the prompt that links this run's findings to earlier ones"
42066
42508
  ).option("--run <run-id>", "Run ID (or pass as positional argument)").option("--json", "Output the match input bundle as JSON").option("--prompt-file <path>", "Write the generated prompt to a file").action(
@@ -42215,7 +42657,7 @@ function registerAllCommands(program) {
42215
42657
  // src/ui/panic.ts
42216
42658
  init_dist();
42217
42659
  init_logger();
42218
- import chalk50 from "chalk";
42660
+ import chalk51 from "chalk";
42219
42661
  var handlersRegistered = false;
42220
42662
  var customSigintHandler = null;
42221
42663
  function registerSigintCleanup(handler) {
@@ -42241,16 +42683,16 @@ function logFatalAndExit(title, errOrReason) {
42241
42683
  process.exit(1);
42242
42684
  }
42243
42685
  if (isVerbose()) {
42244
- console.error(chalk50.red(`
42686
+ console.error(chalk51.red(`
42245
42687
  ${title}:`));
42246
42688
  console.error(
42247
42689
  errOrReason instanceof Error ? errOrReason.stack || errOrReason : errOrReason
42248
42690
  );
42249
42691
  } else {
42250
- console.error(chalk50.red(`
42692
+ console.error(chalk51.red(`
42251
42693
  Fatal Error: ${msg}`));
42252
42694
  console.error(
42253
- chalk50.dim(
42695
+ chalk51.dim(
42254
42696
  " Run with --verbose or DEBUG=1 to view the full stack trace.\n"
42255
42697
  )
42256
42698
  );
@@ -42269,7 +42711,7 @@ function setupProcessHandlers() {
42269
42711
  }
42270
42712
  process.exit(130);
42271
42713
  } else {
42272
- console.error(chalk50.dim("\n Operation cancelled."));
42714
+ console.error(chalk51.dim("\n Operation cancelled."));
42273
42715
  process.exit(130);
42274
42716
  }
42275
42717
  });