@iamu-dev/iamu 0.4.0-beta.0 → 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1432,7 +1432,11 @@ var init_walkthrough_canonical = __esm({
1432
1432
  slug: z22.string(),
1433
1433
  persona_slug: z22.string(),
1434
1434
  outline_slug: z22.string(),
1435
- brief: z22.string()
1435
+ brief: z22.string(),
1436
+ /** Display names, so reports never show slugs. Absent on older runs. */
1437
+ persona_name: z22.string().optional(),
1438
+ persona_role: z22.string().optional(),
1439
+ outline_title: z22.string().optional()
1436
1440
  }).passthrough();
1437
1441
  canonicalTargetSchema = z22.object({
1438
1442
  url: z22.string(),
@@ -1466,6 +1470,10 @@ var init_walkthrough_canonical = __esm({
1466
1470
  }).passthrough();
1467
1471
  canonicalFindingSchema = z22.object({
1468
1472
  id: z22.string(),
1473
+ /** The finding's short title. Absent on older runs. */
1474
+ title: z22.string().nullable().optional(),
1475
+ /** The page it was seen on (the finding's own, else its step's). */
1476
+ page_url: z22.string().nullable().optional(),
1469
1477
  /** Stable across runs for pattern tracking — null until cross-run wiring lands. */
1470
1478
  canonical_id: z22.string().nullable(),
1471
1479
  type: findingTypeEnum,
@@ -1727,14 +1735,14 @@ Bad background: "Construction worker. Experienced in the field. Uses a smartphon
1727
1735
  - observation: An enhancement opportunity. Not friction, but a missed chance to help.
1728
1736
 
1729
1737
  Finding type pairing:
1730
- - helps \u2192 always observation severity. Record sparingly \u2014 only when the product does something genuinely supportive that's worth preserving.
1738
+ - helps \u2192 always observation severity. Record each one you meet: something the product does that this person would miss if it were gone.
1731
1739
  - hinders \u2192 critical, major, or minor.
1732
1740
  - missing \u2192 critical, major, or minor.
1733
1741
  - incorrect \u2192 critical or major only.
1734
1742
  - irrelevant \u2192 minor or observation.
1735
1743
 
1736
1744
  Do NOT record findings that amount to "this screen is clear and easy to use." The absence of friction is not a finding. Record helps only when the product actively supports the persona in a way that would be missed if removed.`;
1737
- HELPS_CALIBRATION = `Helps calibration: A helps finding records something the product does that actively supports the persona in a way that would be missed if removed. "The button is easy to find" is not a helps finding \u2014 that's baseline. "The system auto-saves my draft so I don't lose work when interrupted" IS a helps finding \u2014 removing it would hurt this persona. Record 1-2 helps per walkthrough at most.`;
1745
+ HELPS_CALIBRATION = `Helps calibration: A helps finding records something the product does that actively supports the persona in a way that would be missed if removed. "The button is easy to find" is not a helps finding \u2014 that's baseline. "The system auto-saves my draft so I don't lose work when interrupted" IS a helps finding \u2014 removing it would hurt this persona. Record every one you meet, with the same ui_fact and consequence as any finding; a full walk usually has several. They are what a later change must not break: whoever fixes the problems reads them to know what to protect.`;
1738
1746
  COGNITIVE_BOUNDING = `Cognitive bounding: Your reasoning ability exceeds this persona's. Their cognitive profile describes how they process information, what they're good at, and where they hit limits. Use it to generate authentic friction signals \u2014 not to lobotomize the walkthrough.
1739
1747
 
1740
1748
  The cognitive profile includes strengths AND limits. A construction worker who skims text may also have excellent spatial memory for page layouts. A teenager who's impatient may pattern-match UI conventions instantly. Apply both sides \u2014 strengths let the persona sail through some things; limits create friction at others.
@@ -1917,7 +1925,7 @@ var init_prompt_registry = __esm({
1917
1925
  },
1918
1926
  walkthrough: {
1919
1927
  id: "walkthroughPhilosophy",
1920
- version: "2.0.0",
1928
+ version: "2.1.0",
1921
1929
  compose: (opts) => composeWalkthroughPhilosophy(opts)
1922
1930
  },
1923
1931
  scenarioCraft: {
@@ -6848,413 +6856,196 @@ var init_revisit = __esm({
6848
6856
  }
6849
6857
  });
6850
6858
 
6851
- // ../../packages/local-store/dist/journey-diff.js
6852
- function isProblemFinding(type2) {
6853
- return PROBLEM_TYPES2.has(type2.toLowerCase());
6859
+ // ../../packages/local-store/dist/directions.js
6860
+ import { randomUUID as randomUUID5 } from "crypto";
6861
+ import { promises as fs12 } from "fs";
6862
+ import path9 from "path";
6863
+ import { z as z25 } from "zod";
6864
+ function directionsDir(projectRoot) {
6865
+ return path9.join(iamuDir(projectRoot), DIRECTIONS_DIR);
6854
6866
  }
6855
- function levenshteinDistance(a, b) {
6856
- if (a === b)
6857
- return 0;
6858
- if (!a.length)
6859
- return b.length;
6860
- if (!b.length)
6861
- return a.length;
6862
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
6863
- for (let i = 1; i <= a.length; i++) {
6864
- let prev = i;
6865
- for (let j = 1; j <= b.length; j++) {
6866
- const val = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], prev, row[j]) + 1;
6867
- row[j - 1] = prev;
6868
- prev = val;
6869
- }
6870
- row[b.length] = prev;
6871
- }
6872
- return row[b.length];
6867
+ function directionPath(projectRoot, slug) {
6868
+ return path9.join(directionsDir(projectRoot), `${slug}.md`);
6873
6869
  }
6874
- function normalizeAction(action) {
6875
- if (!action)
6876
- return "";
6877
- return action.toLowerCase().replace(/['"`]/g, "").replace(/[^\p{L}\p{N}\s/_-]+/gu, " ").replace(/\s+/g, " ").trim();
6870
+ function renderBody(texts) {
6871
+ return [
6872
+ "## Problem",
6873
+ "",
6874
+ texts.problem.trim(),
6875
+ "",
6876
+ "## Change",
6877
+ "",
6878
+ texts.change.trim(),
6879
+ "",
6880
+ "## Leading practice",
6881
+ "",
6882
+ texts.leading_practice.trim(),
6883
+ ""
6884
+ ].join("\n");
6878
6885
  }
6879
- function actionTokens(action) {
6880
- const norm = normalizeAction(action);
6881
- if (!norm)
6882
- return /* @__PURE__ */ new Set();
6883
- return new Set(norm.split(/\s+/).filter(Boolean));
6886
+ function section(body, heading) {
6887
+ const re = new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, "m");
6888
+ return re.exec(body)?.[1]?.trim() ?? "";
6884
6889
  }
6885
- function actionSimilarity(a, b) {
6886
- const normA = normalizeAction(a);
6887
- const normB = normalizeAction(b);
6888
- if (!normA && !normB)
6889
- return 1;
6890
- if (!normA || !normB)
6891
- return 0;
6892
- if (normA === normB)
6893
- return 1;
6894
- const tokensA = actionTokens(normA);
6895
- const tokensB = actionTokens(normB);
6896
- let intersection = 0;
6897
- for (const t of tokensA) {
6898
- if (tokensB.has(t))
6899
- intersection++;
6900
- }
6901
- if (intersection === 0) {
6902
- if (tokensA.size <= 1 && tokensB.size <= 1) {
6903
- const maxLen = Math.max(normA.length, normB.length);
6904
- const levDist = levenshteinDistance(normA, normB);
6905
- return levDist <= 2 && maxLen > 0 ? 1 - levDist / maxLen : 0;
6906
- }
6907
- return 0;
6908
- }
6909
- const union2 = tokensA.size + tokensB.size - intersection;
6910
- const jaccard = union2 > 0 ? intersection / union2 : 0;
6911
- const minSize = Math.min(tokensA.size, tokensB.size);
6912
- const subsetRatio = minSize > 0 ? intersection / minSize : 0;
6913
- if (normA.includes(normB) || normB.includes(normA) || subsetRatio === 1) {
6914
- return Math.max(0.85, 0.7 + 0.3 * jaccard);
6890
+ function parseDirection(raw, file) {
6891
+ const { frontmatter, body } = parseFrontmatter(raw);
6892
+ return {
6893
+ frontmatter: directionFrontmatterSchema.parse(frontmatter),
6894
+ texts: {
6895
+ problem: section(body, "Problem"),
6896
+ change: section(body, "Change"),
6897
+ leading_practice: section(body, "Leading practice")
6898
+ },
6899
+ path: file
6900
+ };
6901
+ }
6902
+ async function readDirection(projectRoot, slug) {
6903
+ const file = directionPath(projectRoot, slug);
6904
+ let raw;
6905
+ try {
6906
+ raw = await fs12.readFile(file, "utf8");
6907
+ } catch (err) {
6908
+ if (err.code === "ENOENT")
6909
+ return null;
6910
+ throw err;
6915
6911
  }
6916
- return 0.5 * jaccard + 0.5 * subsetRatio;
6912
+ return parseDirection(raw, file);
6917
6913
  }
6918
- function routeSimilarity(urlA, surfaceA, urlB, surfaceB) {
6919
- const normUrlA = normalizePlaceKey(urlA);
6920
- const normUrlB = normalizePlaceKey(urlB);
6921
- const hasUrlA = normUrlA !== null;
6922
- const hasUrlB = normUrlB !== null;
6923
- let urlScore = null;
6924
- if (hasUrlA && hasUrlB) {
6925
- if (normUrlA === normUrlB) {
6926
- urlScore = 1;
6927
- } else {
6928
- const pathA = normUrlA.split("?")[0];
6929
- const pathB = normUrlB.split("?")[0];
6930
- urlScore = pathA === pathB ? 0.8 : 0;
6931
- }
6914
+ async function listDirections(projectRoot) {
6915
+ let names2;
6916
+ try {
6917
+ names2 = await fs12.readdir(directionsDir(projectRoot));
6918
+ } catch (err) {
6919
+ if (err.code === "ENOENT")
6920
+ return [];
6921
+ throw err;
6932
6922
  }
6933
- const sA = surfaceA?.trim();
6934
- const sB = surfaceB?.trim();
6935
- let surfaceScore = null;
6936
- if (sA && sB) {
6937
- if (surfaceKey(sA) === surfaceKey(sB)) {
6938
- surfaceScore = 1;
6939
- } else if (surfaceMatches(sA, sB) || surfaceMatches(sB, sA)) {
6940
- surfaceScore = 0.8;
6941
- } else {
6942
- surfaceScore = 0;
6923
+ const out = [];
6924
+ for (const name of names2.sort()) {
6925
+ if (!name.endsWith(".md"))
6926
+ continue;
6927
+ const file = path9.join(directionsDir(projectRoot), name);
6928
+ try {
6929
+ out.push(parseDirection(await fs12.readFile(file, "utf8"), file));
6930
+ } catch {
6943
6931
  }
6944
6932
  }
6945
- if (urlScore !== null && surfaceScore !== null) {
6946
- if (urlScore >= 0.8 && surfaceScore >= 0.8)
6947
- return 1;
6948
- if (urlScore >= 0.8)
6949
- return 0.85;
6950
- if (surfaceScore >= 0.8)
6951
- return 0.75;
6952
- return 0;
6953
- }
6954
- if (urlScore !== null)
6955
- return urlScore;
6956
- if (surfaceScore !== null)
6957
- return surfaceScore;
6958
- return 0.5;
6933
+ return out.sort((a, b) => b.frontmatter.updated_at.localeCompare(a.frontmatter.updated_at));
6959
6934
  }
6960
- function stepSimilarity(a, b) {
6961
- const rSim = routeSimilarity(a.url, a.surface, b.url, b.surface);
6962
- const aSim = actionSimilarity(a.action, b.action);
6963
- if (aSim < 0.25 && a.action && b.action) {
6964
- return Math.min(0.2, rSim * 0.2);
6965
- }
6966
- if (rSim === 0) {
6967
- return aSim >= 0.8 ? aSim * 0.4 : 0.1;
6968
- }
6969
- return 0.5 * rSim + 0.5 * aSim;
6935
+ async function writeDirection(projectRoot, fm, texts) {
6936
+ const file = directionPath(projectRoot, fm.slug);
6937
+ const validated = directionFrontmatterSchema.parse(fm);
6938
+ await atomicWriteFile(file, serializeFrontmatter(validated, renderBody(texts)));
6939
+ return file;
6970
6940
  }
6971
- function findingMatchesStep(finding, stepNumber2) {
6972
- if (finding.linked_step_ids && finding.linked_step_ids.length > 0) {
6973
- return finding.linked_step_ids.some((id) => id === `step-${stepNumber2}` || id === String(stepNumber2));
6974
- }
6975
- if (finding.walkthrough_step_id) {
6976
- const raw = finding.walkthrough_step_id.trim();
6977
- if (raw === `step-${stepNumber2}` || raw === String(stepNumber2)) {
6978
- return true;
6979
- }
6941
+ async function upsertDirection(projectRoot, proposal, source, advisedBy, now = (/* @__PURE__ */ new Date()).toISOString()) {
6942
+ const existing = await readDirection(projectRoot, proposal.slug);
6943
+ const texts = {
6944
+ problem: proposal.problem,
6945
+ change: proposal.change,
6946
+ leading_practice: proposal.leading_practice
6947
+ };
6948
+ if (existing?.frontmatter.status === "rejected") {
6949
+ return { outcome: "previously_rejected" };
6980
6950
  }
6981
- return false;
6951
+ const prev = existing?.frontmatter;
6952
+ const fm = {
6953
+ id: prev?.id ?? randomUUID5(),
6954
+ slug: proposal.slug,
6955
+ title: proposal.title,
6956
+ status: prev?.status ?? "proposed",
6957
+ size: proposal.size,
6958
+ absorbs: union(prev?.absorbs ?? [], proposal.absorbs, (x) => x),
6959
+ reach: union(prev?.reach ?? [], proposal.reach, (r) => `${r.scenario}:${r.effect}`),
6960
+ verify: union(prev?.verify ?? [], proposal.verify, (x) => x),
6961
+ builds_on: union(prev?.builds_on ?? [], proposal.builds_on, (r) => `${r.path}#${r.symbol}`),
6962
+ seen_in_runs: union(prev?.seen_in_runs ?? [], [source], (x) => x),
6963
+ advised_by: advisedBy,
6964
+ created_at: prev?.created_at ?? now,
6965
+ updated_at: now
6966
+ };
6967
+ const file = await writeDirection(projectRoot, fm, texts);
6968
+ return {
6969
+ outcome: existing ? "updated" : "created",
6970
+ direction: { frontmatter: fm, texts, path: file }
6971
+ };
6982
6972
  }
6983
- function isStallNote(note) {
6984
- if (!note)
6985
- return false;
6986
- const lower = note.toLowerCase();
6987
- return STALL_KEYWORDS.some((kw) => lower.includes(kw));
6973
+ async function setDirectionStatus(projectRoot, slug, status2, now = (/* @__PURE__ */ new Date()).toISOString()) {
6974
+ const existing = await readDirection(projectRoot, slug);
6975
+ if (!existing)
6976
+ return null;
6977
+ const fm = { ...existing.frontmatter, status: status2, updated_at: now };
6978
+ await writeDirection(projectRoot, fm, existing.texts);
6979
+ return { ...existing, frontmatter: fm };
6988
6980
  }
6989
- function alignJourneySteps(scenario, baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings) {
6990
- const m = baselineSteps.length;
6991
- const n = currentSteps.length;
6992
- const GAP_PENALTY = -0.7;
6993
- const MATCH_THRESHOLD = 0.45;
6994
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
6995
- for (let i2 = 0; i2 <= m; i2++)
6996
- dp[i2][0] = i2 * GAP_PENALTY;
6997
- for (let j2 = 0; j2 <= n; j2++)
6998
- dp[0][j2] = j2 * GAP_PENALTY;
6999
- for (let i2 = 1; i2 <= m; i2++) {
7000
- for (let j2 = 1; j2 <= n; j2++) {
7001
- const sim = stepSimilarity(baselineSteps[i2 - 1], currentSteps[j2 - 1]);
7002
- const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
7003
- const match2 = dp[i2 - 1][j2 - 1] + matchScore;
7004
- const delP = dp[i2 - 1][j2] + GAP_PENALTY;
7005
- const insC = dp[i2][j2 - 1] + GAP_PENALTY;
7006
- dp[i2][j2] = Math.max(match2, delP, insC);
7007
- }
7008
- }
7009
- const alignedPairs = [];
7010
- let i = m;
7011
- let j = n;
7012
- while (i > 0 || j > 0) {
7013
- if (i > 0 && j > 0) {
7014
- const sim = stepSimilarity(baselineSteps[i - 1], currentSteps[j - 1]);
7015
- const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
7016
- const scoreFromMatch = dp[i - 1][j - 1] + matchScore;
7017
- if (Math.abs(dp[i][j] - scoreFromMatch) < 1e-6 && sim >= MATCH_THRESHOLD) {
7018
- alignedPairs.push({
7019
- baseline: baselineSteps[i - 1],
7020
- current: currentSteps[j - 1]
7021
- });
7022
- i--;
7023
- j--;
7024
- continue;
7025
- }
7026
- }
7027
- if (j > 0 && Math.abs(dp[i][j] - (dp[i][j - 1] + GAP_PENALTY)) < 1e-6) {
7028
- alignedPairs.push({
7029
- baseline: null,
7030
- current: currentSteps[j - 1]
7031
- });
7032
- j--;
7033
- } else if (i > 0 && Math.abs(dp[i][j] - (dp[i - 1][j] + GAP_PENALTY)) < 1e-6) {
7034
- alignedPairs.push({
7035
- baseline: baselineSteps[i - 1],
7036
- current: null
7037
- });
7038
- i--;
7039
- } else if (i > 0 && j > 0) {
7040
- alignedPairs.push({
7041
- baseline: baselineSteps[i - 1],
7042
- current: currentSteps[j - 1]
7043
- });
7044
- i--;
7045
- j--;
7046
- } else if (i > 0) {
7047
- alignedPairs.push({
7048
- baseline: baselineSteps[i - 1],
7049
- current: null
7050
- });
7051
- i--;
7052
- } else {
7053
- alignedPairs.push({
7054
- baseline: null,
7055
- current: currentSteps[j - 1]
7056
- });
7057
- j--;
7058
- }
7059
- }
7060
- alignedPairs.reverse();
7061
- const maxBaselineStepNum = baselineSteps.length > 0 ? Math.max(...baselineSteps.map((s) => s.step_number)) : -1;
7062
- const alignedSteps = [];
7063
- const stats = {
7064
- totalAlignedSteps: alignedPairs.length,
7065
- same: 0,
7066
- resolved: 0,
7067
- persists: 0,
7068
- regressions: 0,
7069
- stalledInBaseline: 0,
7070
- bypassed: 0,
7071
- newPath: 0
7072
- };
7073
- for (let idx = 0; idx < alignedPairs.length; idx++) {
7074
- const pair = alignedPairs[idx];
7075
- const bStep = pair.baseline;
7076
- const cStep = pair.current;
7077
- const bFindings = bStep ? baselineFindings.filter((f) => findingMatchesStep(f, bStep.step_number)) : [];
7078
- const cFindings = cStep ? currentFindings.filter((f) => findingMatchesStep(f, cStep.step_number)) : [];
7079
- const bProblems = bFindings.filter((f) => isProblemFinding(f.type));
7080
- const cProblems = cFindings.filter((f) => isProblemFinding(f.type));
7081
- let status2 = "same";
7082
- let summary = "Clean";
7083
- if (bStep && cStep) {
7084
- const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
7085
- if (bProblems.length > 0 && cProblems.length === 0) {
7086
- status2 = "resolved";
7087
- stats.resolved++;
7088
- const probDesc = bProblems[0].ui_fact || bProblems[0].title || bProblems[0].id;
7089
- summary = wasStall ? `Previous stall resolved \u2014 journey unblocked (${probDesc})` : `Resolved previous issue: ${probDesc}`;
7090
- } else if (bProblems.length > 0 && cProblems.length > 0) {
7091
- status2 = "persists";
7092
- stats.persists++;
7093
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7094
- summary = `Problem persists: ${probDesc}`;
7095
- } else if (bProblems.length === 0 && cProblems.length > 0) {
7096
- status2 = "regression";
7097
- stats.regressions++;
7098
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7099
- summary = `New issue reported: ${probDesc}`;
7100
- } else {
7101
- status2 = "same";
7102
- stats.same++;
7103
- summary = "Clean";
7104
- }
7105
- } else if (bStep && !cStep) {
7106
- const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
7107
- if (wasStall) {
7108
- status2 = "stalled";
7109
- stats.stalledInBaseline++;
7110
- const probDesc = bProblems[0]?.ui_fact || bProblems[0]?.title || "journey halted";
7111
- summary = `Stalled in baseline: ${probDesc}`;
7112
- } else {
7113
- status2 = "bypassed";
7114
- stats.bypassed++;
7115
- summary = "Bypassed in current run";
7116
- }
7117
- } else if (!bStep && cStep) {
7118
- if (cProblems.length > 0) {
7119
- status2 = "regression";
7120
- stats.regressions++;
7121
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7122
- summary = `New step with issue: ${probDesc}`;
7123
- } else {
7124
- status2 = "new_path";
7125
- stats.newPath++;
7126
- summary = "New path / unblocked progress";
7127
- }
7128
- }
7129
- alignedSteps.push({
7130
- index: idx + 1,
7131
- status: status2,
7132
- baselineStep: bStep,
7133
- currentStep: cStep,
7134
- baselineFindings: bFindings,
7135
- currentFindings: cFindings,
7136
- summary
7137
- });
7138
- }
7139
- return {
7140
- scenario,
7141
- baselineRunId,
7142
- currentRunId,
7143
- steps: alignedSteps,
7144
- stats
7145
- };
7146
- }
7147
- function toStepFindingInfo(f) {
7148
- return {
7149
- id: f.frontmatter.id,
7150
- title: f.frontmatter.title ?? f.slug,
7151
- type: f.frontmatter.type,
7152
- severity: f.frontmatter.severity,
7153
- ui_fact: f.frontmatter.ui_fact,
7154
- consequence: f.frontmatter.consequence,
7155
- canonical_id: f.frontmatter.canonical_id,
7156
- walkthrough_step_id: f.frontmatter.walkthrough_step_id
7157
- };
7158
- }
7159
- function toJourneyStepInfo(s) {
7160
- return {
7161
- step_number: s.step_number,
7162
- action: s.action,
7163
- url: s.url ?? void 0,
7164
- surface: s.surface ?? void 0,
7165
- note: s.note ?? void 0
7166
- };
7167
- }
7168
- function journeyStepRoute(step) {
7169
- return step.currentStep?.url && normalizePlaceKey(step.currentStep.url) || step.baselineStep?.url && normalizePlaceKey(step.baselineStep.url) || step.currentStep?.surface || step.baselineStep?.surface || "\u2014";
7170
- }
7171
- async function computeJourneyDiff(projectRoot, baselineRunId, currentRunId, scenarioSlug) {
7172
- const [bStepsRaw, bFindingsRaw, cStepsRaw, cFindingsRaw] = await Promise.all([
7173
- readStepsForRun(projectRoot, baselineRunId),
7174
- readFindingsForRun(projectRoot, baselineRunId),
7175
- readStepsForRun(projectRoot, currentRunId),
7176
- readFindingsForRun(projectRoot, currentRunId)
7177
- ]);
7178
- const baselineSteps = bStepsRaw.map(toJourneyStepInfo);
7179
- const baselineFindings = bFindingsRaw.map(toStepFindingInfo);
7180
- const currentSteps = cStepsRaw.map(toJourneyStepInfo);
7181
- const currentFindings = cFindingsRaw.map(toStepFindingInfo);
7182
- return alignJourneySteps(scenarioSlug ?? "", baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings);
7183
- }
7184
- function renderJourneyDiffMarkdown(diff) {
7185
- const lines = [
7186
- "### Chronological journey diff",
7187
- "",
7188
- `Baseline: \`${diff.baselineRunId}\` vs Current: \`${diff.currentRunId}\``
7189
- ];
7190
- const parts = [];
7191
- if (diff.stats.same > 0)
7192
- parts.push(`${diff.stats.same} unchanged`);
7193
- if (diff.stats.resolved > 0)
7194
- parts.push(`${diff.stats.resolved} resolved`);
7195
- if (diff.stats.persists > 0)
7196
- parts.push(`${diff.stats.persists} persists`);
7197
- if (diff.stats.regressions > 0)
7198
- parts.push(`${diff.stats.regressions} new issues`);
7199
- if (diff.stats.stalledInBaseline > 0)
7200
- parts.push(`${diff.stats.stalledInBaseline} stalled in baseline`);
7201
- if (diff.stats.bypassed > 0)
7202
- parts.push(`${diff.stats.bypassed} bypassed`);
7203
- if (diff.stats.newPath > 0)
7204
- parts.push(`${diff.stats.newPath} new path steps`);
7205
- if (parts.length > 0) {
7206
- lines.push(`- **Summary**: ${parts.join(", ")}`);
7207
- }
7208
- lines.push("", "| # | Route | Baseline (Run N-1) | Current (Run N) | Status | Notes |", "|---|-------|--------------------|-----------------|--------|-------|");
7209
- for (const step of diff.steps) {
7210
- const route = journeyStepRoute(step);
7211
- const baseAction = step.baselineStep ? `Step ${step.baselineStep.step_number}: ${escapeMarkdownCell(step.baselineStep.action)}` : "\u2014";
7212
- const currAction = step.currentStep ? `Step ${step.currentStep.step_number}: ${escapeMarkdownCell(step.currentStep.action)}` : "\u2014";
7213
- const statusBadge = `\`${step.status}\``;
7214
- const notes = escapeMarkdownCell(step.summary);
7215
- lines.push(`| ${step.index} | \`${escapeMarkdownCell(route)}\` | ${baseAction} | ${currAction} | ${statusBadge} | ${notes} |`);
7216
- }
7217
- return lines.join("\n");
7218
- }
7219
- function escapeMarkdownCell(text) {
7220
- return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
7221
- }
7222
- var PROBLEM_TYPES2, STALL_KEYWORDS;
7223
- var init_journey_diff = __esm({
7224
- "../../packages/local-store/dist/journey-diff.js"() {
6981
+ var DIRECTIONS_DIR, DIRECTION_STATUSES, directionStatusSchema, directionSizeSchema, directionReachSchema, codeRefSchema, directionFrontmatterSchema, union;
6982
+ var init_directions = __esm({
6983
+ "../../packages/local-store/dist/directions.js"() {
7225
6984
  "use strict";
7226
- init_revisit();
7227
- init_walkthrough_canonical2();
7228
- PROBLEM_TYPES2 = /* @__PURE__ */ new Set(["hinders", "missing", "incorrect"]);
7229
- STALL_KEYWORDS = [
7230
- "blocked",
7231
- "unable to proceed",
7232
- "stalled",
7233
- "halted",
7234
- "stuck",
7235
- "cannot proceed",
7236
- "could not proceed",
7237
- "failed to load",
7238
- "timed out"
6985
+ init_atomic();
6986
+ init_markdown();
6987
+ init_paths();
6988
+ DIRECTIONS_DIR = "directions";
6989
+ DIRECTION_STATUSES = [
6990
+ "proposed",
6991
+ "accepted",
6992
+ "done",
6993
+ "rejected"
7239
6994
  ];
6995
+ directionStatusSchema = z25.enum(DIRECTION_STATUSES);
6996
+ directionSizeSchema = z25.enum(["small", "medium", "large"]);
6997
+ directionReachSchema = z25.object({
6998
+ scenario: z25.string().min(1),
6999
+ effect: z25.enum(["helps", "risk"]),
7000
+ reason: z25.string().min(1)
7001
+ });
7002
+ codeRefSchema = z25.object({
7003
+ path: z25.string().min(1),
7004
+ symbol: z25.string().min(1)
7005
+ });
7006
+ directionFrontmatterSchema = z25.object({
7007
+ id: z25.string().uuid(),
7008
+ slug: z25.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
7009
+ title: z25.string().min(1),
7010
+ status: directionStatusSchema.default("proposed"),
7011
+ size: directionSizeSchema,
7012
+ /** Finding ids this direction would resolve. */
7013
+ absorbs: z25.array(z25.string().min(1)).default([]),
7014
+ /** Predicted effect on other scenarios — never evidence. */
7015
+ reach: z25.array(directionReachSchema).default([]),
7016
+ /** Scenarios to re-run once the direction lands. */
7017
+ verify: z25.array(z25.string().min(1)).default([]),
7018
+ builds_on: z25.array(codeRefSchema).default([]),
7019
+ /** Runs (or `project:<timestamp>`) whose analysis proposed it. */
7020
+ seen_in_runs: z25.array(z25.string().min(1)).default([]),
7021
+ advised_by: z25.string().regex(/^advisor@\S+$/),
7022
+ created_at: z25.string(),
7023
+ updated_at: z25.string()
7024
+ });
7025
+ union = (a, b, key) => {
7026
+ const seen = /* @__PURE__ */ new Map();
7027
+ for (const x of [...a, ...b])
7028
+ seen.set(key(x), x);
7029
+ return [...seen.values()];
7030
+ };
7240
7031
  }
7241
7032
  });
7242
7033
 
7243
7034
  // ../../packages/local-store/dist/walkthrough-canonical.js
7244
- import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
7245
- import { promises as fs12 } from "fs";
7246
- import path9 from "path";
7035
+ import { createHash as createHash2, randomUUID as randomUUID6 } from "crypto";
7036
+ import { promises as fs13 } from "fs";
7037
+ import path10 from "path";
7247
7038
  async function readStepsForRun(projectRoot, runId) {
7248
7039
  const runDirPath = runDir(projectRoot, runId);
7249
- const stepsDir = path9.join(runDirPath, "steps");
7040
+ const stepsDir = path10.join(runDirPath, "steps");
7250
7041
  try {
7251
- const names2 = await fs12.readdir(stepsDir);
7042
+ const names2 = await fs13.readdir(stepsDir);
7252
7043
  const jsonFiles = names2.filter((n) => n.endsWith(".json")).sort();
7253
7044
  if (jsonFiles.length > 0) {
7254
7045
  const out2 = [];
7255
7046
  for (const name of jsonFiles) {
7256
7047
  try {
7257
- const raw2 = await fs12.readFile(path9.join(stepsDir, name), "utf8");
7048
+ const raw2 = await fs13.readFile(path10.join(stepsDir, name), "utf8");
7258
7049
  const parsed = JSON.parse(raw2);
7259
7050
  if (parsed.step_number == null) {
7260
7051
  const numFromName = Number.parseInt(name.replace(/\.json$/, ""), 10);
@@ -7268,10 +7059,10 @@ async function readStepsForRun(projectRoot, runId) {
7268
7059
  }
7269
7060
  } catch {
7270
7061
  }
7271
- const stepsPath = path9.join(runDirPath, "steps.jsonl");
7062
+ const stepsPath = path10.join(runDirPath, "steps.jsonl");
7272
7063
  let raw;
7273
7064
  try {
7274
- raw = await fs12.readFile(stepsPath, "utf8");
7065
+ raw = await fs13.readFile(stepsPath, "utf8");
7275
7066
  } catch (err) {
7276
7067
  if (err.code === "ENOENT")
7277
7068
  return [];
@@ -7296,7 +7087,7 @@ async function readFindingsForRun(projectRoot, runId) {
7296
7087
  const findingsDir = entityDirPath(projectRoot, "finding");
7297
7088
  let names2;
7298
7089
  try {
7299
- names2 = await fs12.readdir(findingsDir);
7090
+ names2 = await fs13.readdir(findingsDir);
7300
7091
  } catch (err) {
7301
7092
  if (err.code === "ENOENT")
7302
7093
  return [];
@@ -7306,8 +7097,8 @@ async function readFindingsForRun(projectRoot, runId) {
7306
7097
  for (const name of names2) {
7307
7098
  if (!name.endsWith(".md") || name.startsWith("."))
7308
7099
  continue;
7309
- const filePath = path9.join(findingsDir, name);
7310
- const raw = await fs12.readFile(filePath, "utf8");
7100
+ const filePath = path10.join(findingsDir, name);
7101
+ const raw = await fs13.readFile(filePath, "utf8");
7311
7102
  try {
7312
7103
  const { frontmatter, body } = parseFrontmatter(raw);
7313
7104
  const fm = frontmatter;
@@ -7338,7 +7129,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7338
7129
  const findingsDir = entityDirPath(projectRoot, "finding");
7339
7130
  let allNames;
7340
7131
  try {
7341
- allNames = await fs12.readdir(findingsDir);
7132
+ allNames = await fs13.readdir(findingsDir);
7342
7133
  } catch {
7343
7134
  allNames = [];
7344
7135
  }
@@ -7346,7 +7137,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7346
7137
  if (!name.endsWith(".md") || name.startsWith("."))
7347
7138
  continue;
7348
7139
  try {
7349
- const raw = await fs12.readFile(path9.join(findingsDir, name), "utf8");
7140
+ const raw = await fs13.readFile(path10.join(findingsDir, name), "utf8");
7350
7141
  const { frontmatter } = parseFrontmatter(raw);
7351
7142
  const fm = frontmatter;
7352
7143
  if (!fm.scenario || fm.scenario !== scenarioSlug)
@@ -7357,14 +7148,14 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7357
7148
  if (fm.canonical_id) {
7358
7149
  priorIndex.set(fp, fm.canonical_id);
7359
7150
  } else if (!priorIndex.has(fp)) {
7360
- priorIndex.set(fp, randomUUID5());
7151
+ priorIndex.set(fp, randomUUID6());
7361
7152
  }
7362
7153
  } catch {
7363
7154
  }
7364
7155
  }
7365
7156
  for (const f of currentFindings) {
7366
7157
  const fp = findingFingerprint(f.frontmatter.scenario, f.frontmatter.type, f.frontmatter.ui_fact);
7367
- const canonicalId = f.frontmatter.canonical_id ?? priorIndex.get(fp) ?? randomUUID5();
7158
+ const canonicalId = f.frontmatter.canonical_id ?? priorIndex.get(fp) ?? randomUUID6();
7368
7159
  result.set(f.frontmatter.id, canonicalId);
7369
7160
  if (!f.frontmatter.canonical_id) {
7370
7161
  try {
@@ -7372,9 +7163,9 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7372
7163
  for (const name of allNames) {
7373
7164
  if (!name.endsWith(".md") || name.startsWith("."))
7374
7165
  continue;
7375
- const candidatePath = path9.join(findingsDir, name);
7166
+ const candidatePath = path10.join(findingsDir, name);
7376
7167
  try {
7377
- const candidateRaw = await fs12.readFile(candidatePath, "utf8");
7168
+ const candidateRaw = await fs13.readFile(candidatePath, "utf8");
7378
7169
  const { frontmatter: cFm } = parseFrontmatter(candidateRaw);
7379
7170
  if (cFm.id === f.frontmatter.id) {
7380
7171
  targetPath = candidatePath;
@@ -7385,7 +7176,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7385
7176
  }
7386
7177
  if (targetPath) {
7387
7178
  await withFileLock(targetPath, async () => {
7388
- const raw = await fs12.readFile(targetPath, "utf8");
7179
+ const raw = await fs13.readFile(targetPath, "utf8");
7389
7180
  const updated = raw.replace(/^(---\n[\s\S]*?)(---\n)/m, `$1canonical_id: ${canonicalId}
7390
7181
  $2`);
7391
7182
  if (updated !== raw) {
@@ -7410,7 +7201,7 @@ function extractRecommendationFromBody(body) {
7410
7201
  }
7411
7202
  async function readAnalysis(projectRoot, runId) {
7412
7203
  try {
7413
- const raw = JSON.parse(await fs12.readFile(path9.join(runDir(projectRoot, runId), "advice.json"), "utf8"));
7204
+ const raw = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, runId), "advice.json"), "utf8"));
7414
7205
  return {
7415
7206
  advisor: `advisor@${raw.advisor?.version ?? "unknown"}`,
7416
7207
  directions: (raw.directions ?? []).map((d) => ({
@@ -7440,9 +7231,13 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7440
7231
  } : null;
7441
7232
  const outlineEntity = scenario ? await readEntity(projectRoot, "outline", scenario.outline) : null;
7442
7233
  const outline = outlineEntity ? {
7234
+ title: outlineEntity.frontmatter.title,
7443
7235
  human_tests: outlineEntity.frontmatter.human_tests ?? []
7444
7236
  } : null;
7237
+ const personaEntity = scenario?.persona ? await readEntity(projectRoot, "persona", scenario.persona).catch(() => null) : null;
7238
+ const personaName = personaEntity?.frontmatter.name ?? null;
7445
7239
  const steps = await readStepsForRun(projectRoot, manifest.run_id);
7240
+ const history = await scenarioRunHistory(projectRoot, manifest.scenario_slug, manifest.run_id);
7446
7241
  const findings2 = await readFindingsForRun(projectRoot, manifest.run_id);
7447
7242
  const analysis = await readAnalysis(projectRoot, manifest.run_id);
7448
7243
  const canonicalSteps = steps.map((s, idx) => {
@@ -7454,7 +7249,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7454
7249
  // Step files record the path relative to the run directory
7455
7250
  // (`screenshots/step-1.png`); older ones recorded the bare file
7456
7251
  // name. Normalize on the file name so the prefix is never doubled.
7457
- screenshot_ref: s.screenshot ? path9.join("screenshots", path9.basename(s.screenshot)) : null,
7252
+ screenshot_ref: s.screenshot ? path10.join("screenshots", path10.basename(s.screenshot)) : null,
7458
7253
  persona_reasoning: s.note,
7459
7254
  grounding: s.grounding ?? null,
7460
7255
  action: s.action,
@@ -7462,9 +7257,15 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7462
7257
  ...typeof s.surface === "string" && s.surface ? { surface: s.surface } : {}
7463
7258
  };
7464
7259
  });
7260
+ const checks = await runChecks(projectRoot, manifest, canonicalSteps, history.previousStartedAt);
7465
7261
  const canonicalMap = await resolveCanonicalIds(projectRoot, manifest.scenario_slug, manifest.run_id, findings2);
7262
+ const stepUrl2 = new Map(canonicalSteps.map((st) => [st.id, st.url ?? null]));
7466
7263
  const canonicalFindings = findings2.map((f) => ({
7467
7264
  id: f.frontmatter.id,
7265
+ // Additive keys (D3): the short title and the page it was seen on,
7266
+ // so reports never fall back to ids or bare step numbers.
7267
+ title: f.frontmatter.title ?? null,
7268
+ page_url: f.frontmatter.page_url ?? (f.frontmatter.walkthrough_step_id ? stepUrl2.get(f.frontmatter.walkthrough_step_id) : null) ?? null,
7468
7269
  canonical_id: canonicalMap.get(f.frontmatter.id) ?? null,
7469
7270
  type: f.frontmatter.type,
7470
7271
  severity: f.frontmatter.severity ?? null,
@@ -7496,7 +7297,10 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7496
7297
  slug: manifest.scenario_slug,
7497
7298
  persona_slug: scenario?.persona ?? "",
7498
7299
  outline_slug: scenario?.outline ?? "",
7499
- brief: scenario?.brief ?? ""
7300
+ brief: scenario?.brief ?? "",
7301
+ ...personaName ? { persona_name: personaName } : {},
7302
+ ...personaEntity?.frontmatter.role ? { persona_role: personaEntity.frontmatter.role } : {},
7303
+ ...outline?.title ? { outline_title: outline.title } : {}
7500
7304
  },
7501
7305
  target: {
7502
7306
  url: manifest.target_url,
@@ -7522,7 +7326,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7522
7326
  steps: canonicalSteps,
7523
7327
  human_tests: humanTests,
7524
7328
  human_test_overall: {
7525
- question: outline ? `Would ${scenario?.persona ?? "the persona"} come back and use this voluntarily?` : "Would the persona come back and use this voluntarily?",
7329
+ question: outline ? `Would ${personaName ?? scenario?.persona ?? "the persona"} come back and use this voluntarily?` : "Would the persona come back and use this voluntarily?",
7526
7330
  answer: null,
7527
7331
  reasoning: null
7528
7332
  },
@@ -7533,21 +7337,65 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7533
7337
  // Consumers MUST preserve unknown keys (per design D3).
7534
7338
  ...inputs.what_worked && inputs.what_worked.length > 0 ? { what_worked: inputs.what_worked } : {},
7535
7339
  ...inputs.open_questions && inputs.open_questions.length > 0 ? { open_questions: inputs.open_questions } : {},
7340
+ ...inputs.setup_notes && inputs.setup_notes.length > 0 ? { setup_notes: inputs.setup_notes } : {},
7536
7341
  ...manifest.blocked ? { blocked: manifest.blocked } : {},
7537
- ...analysis ? { analysis } : {}
7342
+ ...analysis ? { analysis } : {},
7343
+ run_number: history.runNumber,
7344
+ ...checks ? { checks } : {}
7538
7345
  };
7539
7346
  }
7540
- async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, currentFindings) {
7541
- const allRuns = await listRunsInternal(projectRoot);
7542
- let baselineRunId = null;
7543
- let baselineManifest = null;
7544
- for (let i = allRuns.length - 1; i >= 0; i--) {
7545
- const rid = allRuns[i];
7546
- if (rid === currentRunId)
7347
+ async function runChecks(projectRoot, manifest, steps, previousStartedAt) {
7348
+ const places = manifest.revisit ?? [];
7349
+ const fixes = [];
7350
+ if (places.length > 0) {
7351
+ const { entities } = await listEntities(projectRoot, "finding");
7352
+ const byId = new Map(entities.map((e) => [e.frontmatter.id, e.frontmatter]));
7353
+ for (const place of revisitCoverage(places, steps).places) {
7354
+ for (const id of place.finding_ids) {
7355
+ const f = byId.get(id);
7356
+ if (!f || f.type === "helps")
7357
+ continue;
7358
+ fixes.push({
7359
+ canonical_id: f.canonical_id ?? null,
7360
+ title: f.title ?? f.slug,
7361
+ fix: f.triage_note ?? null,
7362
+ page: place.url,
7363
+ reached: place.reached
7364
+ });
7365
+ }
7366
+ }
7367
+ }
7368
+ const directions = previousStartedAt ? (await listDirections(projectRoot).catch(() => [])).filter((d) => d.frontmatter.status === "done" && d.frontmatter.verify.includes(manifest.scenario_slug) && d.frontmatter.updated_at > previousStartedAt).map((d) => d.frontmatter.title) : [];
7369
+ return fixes.length > 0 || directions.length > 0 ? { fixes, directions } : null;
7370
+ }
7371
+ async function scenarioRunHistory(projectRoot, scenarioSlug, runId) {
7372
+ let runNumber = 1;
7373
+ let previousStartedAt = null;
7374
+ for (const rid of await listRunsInternal(projectRoot)) {
7375
+ if (rid >= runId)
7376
+ continue;
7377
+ try {
7378
+ const m = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, rid), "walkthrough.json"), "utf8"));
7379
+ if (m.scenario_slug === scenarioSlug && m.status === "completed") {
7380
+ runNumber++;
7381
+ previousStartedAt = m.created_at;
7382
+ }
7383
+ } catch {
7384
+ }
7385
+ }
7386
+ return { runNumber, previousStartedAt };
7387
+ }
7388
+ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, currentFindings) {
7389
+ const allRuns = await listRunsInternal(projectRoot);
7390
+ let baselineRunId = null;
7391
+ let baselineManifest = null;
7392
+ for (let i = allRuns.length - 1; i >= 0; i--) {
7393
+ const rid = allRuns[i];
7394
+ if (rid === currentRunId)
7547
7395
  continue;
7548
7396
  try {
7549
- const mPath = path9.join(runDir(projectRoot, rid), "walkthrough.json");
7550
- const raw = await fs12.readFile(mPath, "utf8");
7397
+ const mPath = path10.join(runDir(projectRoot, rid), "walkthrough.json");
7398
+ const raw = await fs13.readFile(mPath, "utf8");
7551
7399
  const m = JSON.parse(raw);
7552
7400
  if (m.scenario_slug === scenarioSlug && m.status === "completed" && !m.blocked) {
7553
7401
  baselineRunId = rid;
@@ -7569,15 +7417,21 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7569
7417
  const uncertain = [];
7570
7418
  const matches = await readMatches(projectRoot, currentRunId);
7571
7419
  const recheckedFixed = new Set((matches?.rechecked_fixed ?? []).map((r) => r.canonical_id));
7420
+ const recheckOf = new Map((matches?.rechecked_fixed ?? []).map((r) => [r.canonical_id, r]));
7421
+ const matchNote = new Map((matches?.matches ?? []).filter((m) => m.canonical_id && m.reasoning).map((m) => [m.canonical_id, m.reasoning]));
7422
+ const recurrence = new Map(currentFindings.filter((f) => f.canonical_id).map((f) => [f.canonical_id, f]));
7572
7423
  if (recheckedFixed.size > 0) {
7573
7424
  const prior = await priorCanonicals(projectRoot, scenarioSlug, currentRunId);
7574
7425
  for (const p of prior) {
7575
7426
  if (!recheckedFixed.has(p.canonical_id))
7576
7427
  continue;
7428
+ const recheck = recheckOf.get(p.canonical_id);
7577
7429
  resolved.push({
7578
7430
  canonical_id: p.canonical_id,
7579
7431
  title: p.title,
7580
- type: p.type
7432
+ type: p.type,
7433
+ ...recheck?.reasoning ? { note: recheck.reasoning } : {},
7434
+ ...recheck ? { step_ids: recheck.step_ids } : {}
7581
7435
  });
7582
7436
  await setCanonicalFixStatus(projectRoot, scenarioSlug, p.canonical_id, "verified_resolved");
7583
7437
  }
@@ -7594,11 +7448,20 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7594
7448
  });
7595
7449
  continue;
7596
7450
  }
7451
+ const fixedBefore = bf.frontmatter.triage_status === "addressed";
7597
7452
  if (currentCanonicalIds.has(cid)) {
7453
+ const note = matchNote.get(cid);
7598
7454
  persists.push({
7599
7455
  canonical_id: cid,
7600
7456
  title: bf.frontmatter.title ?? bf.frontmatter.slug,
7601
- type: bf.frontmatter.type
7457
+ type: bf.frontmatter.type,
7458
+ ...note ? { note } : {},
7459
+ step_ids: recurrence.get(cid)?.linked_step_ids ?? [],
7460
+ ...fixedBefore ? { fixed_before: true } : {},
7461
+ ...bf.frontmatter.severity && recurrence.get(cid)?.severity ? {
7462
+ severity_before: bf.frontmatter.severity,
7463
+ severity_now: recurrence.get(cid)?.severity
7464
+ } : {}
7602
7465
  });
7603
7466
  await updateFixStatus(projectRoot, bf.frontmatter.slug, "verified_persists");
7604
7467
  } else if (recheckedFixed.has(cid)) {
@@ -7606,7 +7469,8 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7606
7469
  notObserved.push({
7607
7470
  canonical_id: cid,
7608
7471
  title: bf.frontmatter.title ?? bf.frontmatter.slug,
7609
- type: bf.frontmatter.type
7472
+ type: bf.frontmatter.type,
7473
+ ...fixedBefore ? { fixed_before: true } : {}
7610
7474
  });
7611
7475
  }
7612
7476
  }
@@ -7628,9 +7492,9 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7628
7492
  async function updateFixStatus(projectRoot, slug, status2) {
7629
7493
  try {
7630
7494
  const findingsDir = entityDirPath(projectRoot, "finding");
7631
- const filePath = path9.join(findingsDir, `${slug}.md`);
7495
+ const filePath = path10.join(findingsDir, `${slug}.md`);
7632
7496
  await withFileLock(filePath, async () => {
7633
- const raw = await fs12.readFile(filePath, "utf8");
7497
+ const raw = await fs13.readFile(filePath, "utf8");
7634
7498
  const updated = raw.replace(/^(fix_status:\s*).*$/m, `$1${status2}`);
7635
7499
  if (updated !== raw) {
7636
7500
  await atomicWriteFile(filePath, updated);
@@ -7644,210 +7508,19 @@ fix_status: ${status2}`);
7644
7508
  }
7645
7509
  }
7646
7510
  async function listRunsInternal(projectRoot) {
7647
- const dir = path9.join(projectRoot, "iamu", ".runs");
7511
+ const dir = path10.join(projectRoot, "iamu", ".runs");
7648
7512
  try {
7649
- const names2 = await fs12.readdir(dir);
7513
+ const names2 = await fs13.readdir(dir);
7650
7514
  return names2.filter((n) => !n.startsWith(".")).sort();
7651
7515
  } catch {
7652
7516
  return [];
7653
7517
  }
7654
7518
  }
7655
- function renderCanonicalMarkdown(canonical) {
7656
- const lines = [
7657
- "<!-- GENERATED: do not edit by hand. Regen via complete_walkthrough or local-store renderer. -->",
7658
- "",
7659
- `# Walkthrough report: ${canonical.scenario.slug}`,
7660
- "",
7661
- ...canonical.blocked ? [
7662
- "> **Blocked \u2014 the walk could not start as it was set up.**",
7663
- `> ${canonical.blocked.reason}`,
7664
- ">",
7665
- "> This run is not compared with earlier runs and proves nothing about earlier findings. Fix the setup (the scenario's starting state or the persona's access) and run it again.",
7666
- ""
7667
- ] : [],
7668
- `- Run ID: ${canonical.run_id}`,
7669
- `- Target URL: ${canonical.target.url}`
7670
- ];
7671
- if (canonical.environment) {
7672
- lines.push(`- Environment: ${canonical.environment}`);
7673
- }
7674
- if (canonical.build) {
7675
- const commit = canonical.build.git_sha?.slice(0, 7);
7676
- const buildDetails = [
7677
- canonical.build.label,
7678
- canonical.build.git_branch,
7679
- canonical.build.git_dirty && canonical.build.worktree_hash ? `${commit ?? "HEAD"} + uncommitted changes (${canonical.build.worktree_hash.slice(0, 6)}\u2026)` : commit,
7680
- canonical.build.ci?.provider
7681
- ].filter(Boolean).join(" \xB7 ");
7682
- if (buildDetails) {
7683
- lines.push(`- Build: ${buildDetails}`);
7684
- }
7685
- }
7686
- if (canonical.viewport) {
7687
- lines.push(`- Viewport: ${canonical.viewport.device} (${canonical.viewport.width}\xD7${canonical.viewport.height})`);
7688
- }
7689
- lines.push(`- Started: ${canonical.provenance.started_at}`, `- Completed: ${canonical.provenance.completed_at}`, `- Walker prompt: ${canonical.provenance.walker_prompt.id}@${canonical.provenance.walker_prompt.version}`, `- Findings: ${canonical.findings.length}`, `- Steps: ${canonical.steps.length}`);
7690
- const models = canonical.models;
7691
- if (models) {
7692
- const parts = [
7693
- models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
7694
- models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
7695
- models.matcher ? `Matcher: ${models.matcher}` : ""
7696
- ].filter(Boolean);
7697
- if (parts.length > 0) {
7698
- lines.push(`- Models: ${parts.join(" \xB7 ")}`);
7699
- }
7700
- if (typeof models.velocity_sec_per_step === "number") {
7701
- lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
7702
- }
7703
- }
7704
- const what = canonical.what_worked;
7705
- if (what && what.length > 0) {
7706
- lines.push("", "## What worked");
7707
- for (const w of what)
7708
- lines.push(`- ${w}`);
7709
- }
7710
- const open2 = canonical.open_questions;
7711
- if (open2 && open2.length > 0) {
7712
- lines.push("", "## Open questions");
7713
- for (const q of open2)
7714
- lines.push(`- ${q}`);
7715
- }
7716
- const verification = canonical.verification;
7717
- if (verification) {
7718
- lines.push("", "## Since this scenario's last run", "", `Baseline: ${verification.baseline_run_id}`);
7719
- const group = (label, items, note = "") => {
7720
- if (items.length === 0)
7721
- return;
7722
- lines.push("", `**${label}** (${items.length})${note}`);
7723
- for (const i of items)
7724
- lines.push(`- ${i.title}`);
7725
- };
7726
- group("Resolved", verification.resolved, " \u2014 re-checked in this run");
7727
- group("Persists", verification.persists);
7728
- group("New since that run", verification.new_findings);
7729
- group("Not seen this run", verification.not_observed, " \u2014 not evidence of a fix");
7730
- group("Uncertain", verification.uncertain);
7731
- }
7732
- const diff = canonical.journey_diff;
7733
- if (diff) {
7734
- lines.push("", renderJourneyDiffMarkdown(diff));
7735
- }
7736
- lines.push("", "## Findings");
7737
- if (canonical.findings.length === 0) {
7738
- lines.push("", "_No findings recorded._");
7739
- } else {
7740
- const severityRank = ["critical", "major", "minor", "observation"];
7741
- const rank = (value, order) => {
7742
- const i = order.indexOf(value ?? "");
7743
- return i === -1 ? order.length : i;
7744
- };
7745
- const typeRank = ["incorrect", "hinders", "missing", "irrelevant", "helps"];
7746
- const ordered = [...canonical.findings].sort((a, b) => rank(a.severity, severityRank) - rank(b.severity, severityRank) || rank(a.type, typeRank) - rank(b.type, typeRank));
7747
- for (const f of ordered) {
7748
- lines.push("", `### ${f.type} / ${f.severity ?? "unranked"} \u2014 ${f.id}`);
7749
- if (f.ui_fact === null || f.consequence === null) {
7750
- lines.push("", "> _Legacy finding: predates the UI-fact / consequence depth gate._");
7751
- }
7752
- if (f.ui_fact)
7753
- lines.push("", "**UI fact.** ", f.ui_fact);
7754
- if (f.consequence)
7755
- lines.push("", "**Consequence.** ", f.consequence);
7756
- if (f.recommendation)
7757
- lines.push("", "**Recommendation.** ", f.recommendation);
7758
- for (const stepId of f.linked_step_ids) {
7759
- const step = canonical.steps.find((st) => st.id === stepId);
7760
- const label = step ? `Step ${step.step_number}` : stepId;
7761
- lines.push("", step?.screenshot_ref ? `**Where.** ${label}
7762
-
7763
- ![${label}](${step.screenshot_ref})` : `**Where.** ${label}`);
7764
- }
7765
- if (f.linked_human_test_id) {
7766
- lines.push("", `Human test: ${f.linked_human_test_id}`);
7767
- }
7768
- }
7769
- }
7770
- const analysis = canonical.analysis;
7771
- if (analysis && analysis.directions.length > 0) {
7772
- lines.push("", "## Directions", "", `_Structural changes proposed by ${analysis.advisor} after the walk. Reach is a prediction until the listed scenarios are re-run._`);
7773
- for (const d of analysis.directions) {
7774
- lines.push("", `### ${d.title} (${d.size})`, "", d.change);
7775
- lines.push("", `Absorbs ${d.absorbs.length} finding${d.absorbs.length === 1 ? "" : "s"}.`);
7776
- const helps = d.reach.filter((r) => r.effect === "helps");
7777
- const risks = d.reach.filter((r) => r.effect === "risk");
7778
- for (const r of helps)
7779
- lines.push(`- Likely helps ${r.scenario}: ${r.reason}`);
7780
- for (const r of risks)
7781
- lines.push(`- Could hurt ${r.scenario}: ${r.reason}`);
7782
- if (d.verify.length > 0)
7783
- lines.push("", `Re-run to verify: ${d.verify.join(", ")}`);
7784
- }
7785
- }
7786
- if (analysis && analysis.observations.length > 0) {
7787
- lines.push("", "## Also worth knowing", "");
7788
- for (const o of analysis.observations) {
7789
- const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
7790
- lines.push(`- **${label}: ${o.title}.** ${o.detail}`);
7791
- if (o.suggested_scenario) {
7792
- lines.push(` Suggested scenario: ${o.suggested_scenario}`);
7793
- }
7794
- }
7795
- }
7796
- lines.push("", "## Human tests");
7797
- if (canonical.human_tests.length === 0) {
7798
- lines.push("", "_No human tests on the outline._");
7799
- } else {
7800
- for (const ht of canonical.human_tests) {
7801
- lines.push("", `- **${ht.question}**`);
7802
- if (ht.verdict === null) {
7803
- if (canonical.blocked) {
7804
- lines.push(` - Verdict: _skipped (walk blocked)_`);
7805
- } else {
7806
- lines.push(` - Verdict: _pending judge_ (walker does not self-grade)`);
7807
- }
7808
- } else {
7809
- lines.push(` - Verdict: **${ht.verdict}**`);
7810
- if (ht.reasoning)
7811
- lines.push(` - Reasoning: ${ht.reasoning}`);
7812
- }
7813
- }
7814
- }
7815
- lines.push("", "## Human test overall", "", `> ${canonical.human_test_overall.question}`);
7816
- if (canonical.human_test_overall.answer === null) {
7817
- if (canonical.blocked) {
7818
- lines.push("", "_Skipped (walk blocked)._");
7819
- } else {
7820
- lines.push("", "_Pending judge stage._");
7821
- }
7822
- } else {
7823
- lines.push("", canonical.human_test_overall.answer);
7824
- if (canonical.human_test_overall.reasoning)
7825
- lines.push("", canonical.human_test_overall.reasoning);
7826
- }
7827
- lines.push("", "## Steps");
7828
- if (canonical.steps.length === 0) {
7829
- lines.push("", "_No steps recorded._");
7830
- } else {
7831
- for (const s of canonical.steps) {
7832
- lines.push("", `### ${s.step_number}. ${s.screen_title ?? "(untitled)"} \u2014 ${s.id}`);
7833
- const where = [s.surface, s.url].filter(Boolean).join(" \u2014 ");
7834
- if (where)
7835
- lines.push("", `Page: ${where}`);
7836
- if (s.persona_reasoning)
7837
- lines.push("", `> ${s.persona_reasoning.replace(/\n/g, "\n> ")}`);
7838
- if (s.grounding)
7839
- lines.push("", `Grounding: ${s.grounding}`);
7840
- if (s.screenshot_ref)
7841
- lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
7842
- }
7843
- }
7844
- return `${lines.join("\n")}
7845
- `;
7846
- }
7847
7519
  var init_walkthrough_canonical2 = __esm({
7848
7520
  "../../packages/local-store/dist/walkthrough-canonical.js"() {
7849
7521
  "use strict";
7850
- init_journey_diff();
7522
+ init_revisit();
7523
+ init_directions();
7851
7524
  init_atomic();
7852
7525
  init_file_lock();
7853
7526
  init_dist();
@@ -7869,8 +7542,8 @@ __export(walkthrough_mirror_exports, {
7869
7542
  walkthroughBundleSchema: () => walkthroughBundleSchema
7870
7543
  });
7871
7544
  import { createHash as createHash3 } from "crypto";
7872
- import { promises as fs13 } from "fs";
7873
- import path10 from "path";
7545
+ import { promises as fs14 } from "fs";
7546
+ import path11 from "path";
7874
7547
  function toCloudRunUuid(runId) {
7875
7548
  if (UUID_RE.test(runId))
7876
7549
  return runId.toLowerCase();
@@ -7920,18 +7593,18 @@ function buildPushUrl(cloudApiUrl, cloudProjectId) {
7920
7593
  return `${base}/api/projects/${cloudProjectId}/walkthroughs/push`;
7921
7594
  }
7922
7595
  async function readLocalManifest(projectRoot, runId) {
7923
- const p = path10.join(runDir(projectRoot, runId), "walkthrough.json");
7596
+ const p = path11.join(runDir(projectRoot, runId), "walkthrough.json");
7924
7597
  try {
7925
- const raw = await fs13.readFile(p, "utf8");
7598
+ const raw = await fs14.readFile(p, "utf8");
7926
7599
  return JSON.parse(raw);
7927
7600
  } catch {
7928
7601
  return null;
7929
7602
  }
7930
7603
  }
7931
7604
  async function readLocalReport(projectRoot, runId) {
7932
- const p = path10.join(runDir(projectRoot, runId), "report.md");
7605
+ const p = path11.join(runDir(projectRoot, runId), "report.md");
7933
7606
  try {
7934
- return await fs13.readFile(p, "utf8");
7607
+ return await fs14.readFile(p, "utf8");
7935
7608
  } catch (err) {
7936
7609
  if (err.code === "ENOENT")
7937
7610
  return "";
@@ -7939,10 +7612,10 @@ async function readLocalReport(projectRoot, runId) {
7939
7612
  }
7940
7613
  }
7941
7614
  async function readScreenshotBytes(projectRoot, runId) {
7942
- const dir = path10.join(runDir(projectRoot, runId), SCREENSHOTS_DIR);
7615
+ const dir = path11.join(runDir(projectRoot, runId), SCREENSHOTS_DIR);
7943
7616
  let names2;
7944
7617
  try {
7945
- names2 = await fs13.readdir(dir);
7618
+ names2 = await fs14.readdir(dir);
7946
7619
  } catch (err) {
7947
7620
  if (err.code === "ENOENT")
7948
7621
  return /* @__PURE__ */ new Map();
@@ -7958,15 +7631,15 @@ async function readScreenshotBytes(projectRoot, runId) {
7958
7631
  const contentType = EXT_TO_CONTENT_TYPE[ext === ".jpeg" ? ".jpeg" : ext];
7959
7632
  if (!contentType)
7960
7633
  continue;
7961
- const bytes = await fs13.readFile(path10.join(dir, name));
7634
+ const bytes = await fs14.readFile(path11.join(dir, name));
7962
7635
  out.set(seq2, { bytes, filename: name, contentType });
7963
7636
  }
7964
7637
  return out;
7965
7638
  }
7966
7639
  async function readVerdictsJson(projectRoot, runId) {
7967
- const filePath = path10.join(runDir(projectRoot, runId), "verdicts.json");
7640
+ const filePath = path11.join(runDir(projectRoot, runId), "verdicts.json");
7968
7641
  try {
7969
- const raw = await fs13.readFile(filePath, "utf-8");
7642
+ const raw = await fs14.readFile(filePath, "utf-8");
7970
7643
  return JSON.parse(raw);
7971
7644
  } catch {
7972
7645
  return null;
@@ -7975,9 +7648,9 @@ async function readVerdictsJson(projectRoot, runId) {
7975
7648
  async function readProvenance(projectRoot, runId, verdicts) {
7976
7649
  if (verdicts?.provenance)
7977
7650
  return verdicts.provenance;
7978
- const filePath = path10.join(runDir(projectRoot, runId), "canonical.json");
7651
+ const filePath = path11.join(runDir(projectRoot, runId), "canonical.json");
7979
7652
  try {
7980
- const raw = await fs13.readFile(filePath, "utf-8");
7653
+ const raw = await fs14.readFile(filePath, "utf-8");
7981
7654
  const data = JSON.parse(raw);
7982
7655
  return data.provenance ?? null;
7983
7656
  } catch {
@@ -7986,7 +7659,7 @@ async function readProvenance(projectRoot, runId, verdicts) {
7986
7659
  }
7987
7660
  async function readCanonicalExtras(projectRoot, runId) {
7988
7661
  try {
7989
- const data = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, runId), "canonical.json"), "utf-8"));
7662
+ const data = JSON.parse(await fs14.readFile(path11.join(runDir(projectRoot, runId), "canonical.json"), "utf-8"));
7990
7663
  const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
7991
7664
  const verification = bundleVerificationSchema.safeParse(data.verification);
7992
7665
  let blocked;
@@ -8735,7 +8408,7 @@ var init_refs = __esm({
8735
8408
  });
8736
8409
 
8737
8410
  // ../../packages/local-store/dist/sync/types.js
8738
- import { z as z25 } from "zod";
8411
+ import { z as z26 } from "zod";
8739
8412
  function toRef(d) {
8740
8413
  return { type: d.type, id: d.id, label: d.label };
8741
8414
  }
@@ -8800,17 +8473,17 @@ var init_types = __esm({
8800
8473
  DEFAULT_TIMEOUT_MS4 = 3e4;
8801
8474
  MAX_BATCH_BYTES = 6 * 1024 * 1024;
8802
8475
  BASE_HASH_BYTES = 96;
8803
- importConflictSchema = z25.object({
8804
- type: z25.string(),
8805
- id: z25.string(),
8806
- base_hash: z25.string().nullish(),
8807
- cloud_hash: z25.string(),
8808
- cloud_updated_at: z25.string().nullish(),
8809
- cloud_changed_by: z25.string().nullish()
8476
+ importConflictSchema = z26.object({
8477
+ type: z26.string(),
8478
+ id: z26.string(),
8479
+ base_hash: z26.string().nullish(),
8480
+ cloud_hash: z26.string(),
8481
+ cloud_updated_at: z26.string().nullish(),
8482
+ cloud_changed_by: z26.string().nullish()
8810
8483
  });
8811
- importResponseSchema = z25.object({
8812
- imported: z25.number().int().min(0),
8813
- conflicts: z25.array(importConflictSchema).optional()
8484
+ importResponseSchema = z26.object({
8485
+ imported: z26.number().int().min(0),
8486
+ conflicts: z26.array(importConflictSchema).optional()
8814
8487
  });
8815
8488
  }
8816
8489
  });
@@ -9061,7 +8734,7 @@ var init_push = __esm({
9061
8734
  });
9062
8735
 
9063
8736
  // ../../packages/local-store/dist/sync/context.js
9064
- import { promises as fs14 } from "fs";
8737
+ import { promises as fs15 } from "fs";
9065
8738
  async function syncContext(target) {
9066
8739
  const { projectRoot } = target;
9067
8740
  const summary = zeroContext();
@@ -9069,7 +8742,7 @@ async function syncContext(target) {
9069
8742
  const conflicts = [];
9070
8743
  let raw;
9071
8744
  try {
9072
- raw = await fs14.readFile(contextPath(projectRoot), "utf8");
8745
+ raw = await fs15.readFile(contextPath(projectRoot), "utf8");
9073
8746
  } catch (err) {
9074
8747
  if (err.code === "ENOENT")
9075
8748
  return { summary, errors, conflicts };
@@ -9150,7 +8823,7 @@ var init_context3 = __esm({
9150
8823
  // ../../packages/local-store/dist/git-branches.js
9151
8824
  import { execFile } from "child_process";
9152
8825
  import { realpathSync } from "fs";
9153
- import path11 from "path";
8826
+ import path12 from "path";
9154
8827
  function defaultGitExec(cwd, args, input) {
9155
8828
  return new Promise((resolve3) => {
9156
8829
  let settled = false;
@@ -9209,7 +8882,7 @@ async function otherBranchesWithFiles(cwd, absPaths, opts = {}) {
9209
8882
  return out;
9210
8883
  return null;
9211
8884
  }
9212
- const top = path11.resolve(topRes.stdout.trim());
8885
+ const top = path12.resolve(topRes.stdout.trim());
9213
8886
  if (!top)
9214
8887
  return out;
9215
8888
  const refsRes = await git2(cwd, [
@@ -9247,8 +8920,8 @@ async function otherBranchesWithFiles(cwd, absPaths, opts = {}) {
9247
8920
  } catch {
9248
8921
  }
9249
8922
  const rel = absPaths.map((abs) => {
9250
- const fromCwd = path11.relative(cwd, abs);
9251
- return path11.relative(top, path11.resolve(base, fromCwd)).split(path11.sep).join("/");
8923
+ const fromCwd = path12.relative(cwd, abs);
8924
+ return path12.relative(top, path12.resolve(base, fromCwd)).split(path12.sep).join("/");
9252
8925
  });
9253
8926
  const queries = [];
9254
8927
  const lines = [];
@@ -9286,8 +8959,8 @@ var init_git_branches = __esm({
9286
8959
  });
9287
8960
 
9288
8961
  // ../../packages/local-store/dist/deletions.js
9289
- import { promises as fs15 } from "fs";
9290
- import path12 from "path";
8962
+ import { promises as fs16 } from "fs";
8963
+ import path13 from "path";
9291
8964
  function describeDeletionCount(type2, n) {
9292
8965
  const nouns = {
9293
8966
  persona: ["persona", "personas"],
@@ -9314,7 +8987,7 @@ function summarizeDeletions(items) {
9314
8987
  }
9315
8988
  async function readPushedEntities(projectRoot) {
9316
8989
  try {
9317
- const raw = await fs15.readFile(pushedEntitiesPath(projectRoot), "utf8");
8990
+ const raw = await fs16.readFile(pushedEntitiesPath(projectRoot), "utf8");
9318
8991
  const parsed = JSON.parse(raw);
9319
8992
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9320
8993
  return {};
@@ -9353,11 +9026,11 @@ async function rememberPushedEntities(projectRoot, known) {
9353
9026
  }
9354
9027
  async function readLenient(projectRoot, relPath) {
9355
9028
  try {
9356
- const raw = await fs15.readFile(path12.join(projectRoot, relPath), "utf8");
9029
+ const raw = await fs16.readFile(path13.join(projectRoot, relPath), "utf8");
9357
9030
  const fm = parseFrontmatter(raw).frontmatter;
9358
9031
  if (typeof fm.id !== "string" || fm.id.length === 0)
9359
9032
  return null;
9360
- const slug = typeof fm.slug === "string" ? fm.slug : path12.basename(relPath, ".md");
9033
+ const slug = typeof fm.slug === "string" ? fm.slug : path13.basename(relPath, ".md");
9361
9034
  const run = typeof fm.run === "string" ? fm.run : void 0;
9362
9035
  return { id: fm.id, slug, run };
9363
9036
  } catch {
@@ -9779,7 +9452,7 @@ var init_deletions2 = __esm({
9779
9452
  });
9780
9453
 
9781
9454
  // ../../packages/local-store/dist/sync/scan.js
9782
- import { promises as fs16 } from "fs";
9455
+ import { promises as fs17 } from "fs";
9783
9456
  function blockedRef(refs, blocked) {
9784
9457
  if (!refs)
9785
9458
  return null;
@@ -9875,7 +9548,7 @@ async function scanPending(projectRoot) {
9875
9548
  continue;
9876
9549
  let bytes;
9877
9550
  try {
9878
- bytes = await fs16.readFile(entity.absPath, "utf8");
9551
+ bytes = await fs17.readFile(entity.absPath, "utf8");
9879
9552
  } catch {
9880
9553
  continue;
9881
9554
  }
@@ -9936,7 +9609,7 @@ var init_scan = __esm({
9936
9609
  });
9937
9610
 
9938
9611
  // ../../packages/local-store/dist/sync/walkthroughs.js
9939
- import { promises as fs17 } from "fs";
9612
+ import { promises as fs18 } from "fs";
9940
9613
  import nodePath from "path";
9941
9614
  async function syncWalkthroughs(projectRoot, deps, unpushableScenarios = /* @__PURE__ */ new Set()) {
9942
9615
  const summary = zeroWalkthroughs();
@@ -9964,7 +9637,7 @@ async function syncWalkthroughs(projectRoot, deps, unpushableScenarios = /* @__P
9964
9637
  const manifestPath = nodePath.join(runDir(projectRoot, runId), "walkthrough.json");
9965
9638
  let manifestRaw;
9966
9639
  try {
9967
- manifestRaw = await fs17.readFile(manifestPath, "utf8");
9640
+ manifestRaw = await fs18.readFile(manifestPath, "utf8");
9968
9641
  } catch {
9969
9642
  continue;
9970
9643
  }
@@ -10303,8 +9976,8 @@ var init_disconnect = __esm({
10303
9976
  });
10304
9977
 
10305
9978
  // ../../packages/local-store/dist/status.js
10306
- import { promises as fs18 } from "fs";
10307
- import path13 from "path";
9979
+ import { promises as fs19 } from "fs";
9980
+ import path14 from "path";
10308
9981
  async function countType(projectRoot, type2) {
10309
9982
  const result = await listEntities(projectRoot, type2);
10310
9983
  const contentHashes = /* @__PURE__ */ new Map();
@@ -10314,7 +9987,7 @@ async function countType(projectRoot, type2) {
10314
9987
  if (typeof id !== "string" || typeof slug !== "string")
10315
9988
  continue;
10316
9989
  try {
10317
- const raw = await fs18.readFile(entity.absPath, "utf8");
9990
+ const raw = await fs19.readFile(entity.absPath, "utf8");
10318
9991
  contentHashes.set(id, hashNormalizedContent(raw));
10319
9992
  } catch {
10320
9993
  }
@@ -10408,7 +10081,7 @@ async function countContext(projectRoot, opts = {
10408
10081
  return base;
10409
10082
  let pending = true;
10410
10083
  try {
10411
- const bytes = await fs18.readFile(contextPath(projectRoot), "utf8");
10084
+ const bytes = await fs19.readFile(contextPath(projectRoot), "utf8");
10412
10085
  const hash = hashNormalizedContent(bytes);
10413
10086
  pending = opts.lastPush["context:root"] !== hash;
10414
10087
  } catch {
@@ -10427,10 +10100,10 @@ async function countWalkthroughs(projectRoot, opts = { cloudLinked: false, lastP
10427
10100
  let pushed = 0;
10428
10101
  let pending = 0;
10429
10102
  for (const runId of valid) {
10430
- const manifestPath = path13.join(projectRoot, "iamu", ".runs", runId, "walkthrough.json");
10103
+ const manifestPath = path14.join(projectRoot, "iamu", ".runs", runId, "walkthrough.json");
10431
10104
  let completed = false;
10432
10105
  try {
10433
- const raw = await fs18.readFile(manifestPath, "utf8");
10106
+ const raw = await fs19.readFile(manifestPath, "utf8");
10434
10107
  const manifest = JSON.parse(raw);
10435
10108
  completed = manifest.status === "completed";
10436
10109
  } catch {
@@ -10450,7 +10123,7 @@ async function getProjectStatus(projectRoot, opts = {}) {
10450
10123
  const config = await readConfig(projectRoot);
10451
10124
  const iamuPath = iamuDir(projectRoot);
10452
10125
  try {
10453
- await fs18.access(iamuPath);
10126
+ await fs19.access(iamuPath);
10454
10127
  } catch {
10455
10128
  throw new Error(`status: iamu/ directory missing at ${iamuPath}`);
10456
10129
  }
@@ -10632,22 +10305,22 @@ var init_status = __esm({
10632
10305
  });
10633
10306
 
10634
10307
  // ../../packages/local-store/dist/prune.js
10635
- import { promises as fs19 } from "fs";
10636
- import path14 from "path";
10308
+ import { promises as fs20 } from "fs";
10309
+ import path15 from "path";
10637
10310
  async function getDirectoryStats(dirPath) {
10638
10311
  let bytes = 0;
10639
10312
  let count = 0;
10640
10313
  try {
10641
- const entries = await fs19.readdir(dirPath, { withFileTypes: true });
10314
+ const entries = await fs20.readdir(dirPath, { withFileTypes: true });
10642
10315
  for (const entry of entries) {
10643
- const fullPath = path14.join(dirPath, entry.name);
10316
+ const fullPath = path15.join(dirPath, entry.name);
10644
10317
  if (entry.isDirectory()) {
10645
10318
  const sub = await getDirectoryStats(fullPath);
10646
10319
  bytes += sub.bytes;
10647
10320
  count += sub.count;
10648
10321
  } else if (entry.isFile()) {
10649
10322
  try {
10650
- const s = await fs19.stat(fullPath);
10323
+ const s = await fs20.stat(fullPath);
10651
10324
  bytes += s.size;
10652
10325
  count += 1;
10653
10326
  } catch {
@@ -10663,12 +10336,12 @@ async function getScreenshotsStats(screenshotsDir) {
10663
10336
  let count = 0;
10664
10337
  const paths = [];
10665
10338
  try {
10666
- const entries = await fs19.readdir(screenshotsDir, { withFileTypes: true });
10339
+ const entries = await fs20.readdir(screenshotsDir, { withFileTypes: true });
10667
10340
  for (const entry of entries) {
10668
10341
  if (entry.isFile() && (entry.name.endsWith(".png") || entry.name.endsWith(".jpg") || entry.name.endsWith(".jpeg") || entry.name.endsWith(".webp"))) {
10669
- const fullPath = path14.join(screenshotsDir, entry.name);
10342
+ const fullPath = path15.join(screenshotsDir, entry.name);
10670
10343
  try {
10671
- const s = await fs19.stat(fullPath);
10344
+ const s = await fs20.stat(fullPath);
10672
10345
  bytes += s.size;
10673
10346
  count += 1;
10674
10347
  paths.push(fullPath);
@@ -10688,8 +10361,8 @@ async function pruneRuns(projectRoot, opts = {}) {
10688
10361
  let timestamp2 = null;
10689
10362
  let scenarioSlug;
10690
10363
  try {
10691
- const manifestPath = path14.join(dirPath, "walkthrough.json");
10692
- const raw = await fs19.readFile(manifestPath, "utf-8");
10364
+ const manifestPath = path15.join(dirPath, "walkthrough.json");
10365
+ const raw = await fs20.readFile(manifestPath, "utf-8");
10693
10366
  const parsed = JSON.parse(raw);
10694
10367
  if (parsed.scenario_slug)
10695
10368
  scenarioSlug = parsed.scenario_slug;
@@ -10701,7 +10374,7 @@ async function pruneRuns(projectRoot, opts = {}) {
10701
10374
  }
10702
10375
  if (!timestamp2 || Number.isNaN(timestamp2.getTime())) {
10703
10376
  try {
10704
- const st = await fs19.stat(dirPath);
10377
+ const st = await fs20.stat(dirPath);
10705
10378
  timestamp2 = st.mtime;
10706
10379
  } catch {
10707
10380
  timestamp2 = /* @__PURE__ */ new Date();
@@ -10736,12 +10409,12 @@ async function pruneRuns(projectRoot, opts = {}) {
10736
10409
  let totalScreenshotsDeleted = 0;
10737
10410
  let totalBytesReclaimed = 0;
10738
10411
  for (const c of candidates) {
10739
- const screenshotsDir = path14.join(c.dirPath, "screenshots");
10412
+ const screenshotsDir = path15.join(c.dirPath, "screenshots");
10740
10413
  const shotStats = await getScreenshotsStats(screenshotsDir);
10741
10414
  if (opts.screenshotsOnly) {
10742
10415
  if (!opts.dryRun) {
10743
10416
  for (const shotPath of shotStats.paths) {
10744
- await fs19.unlink(shotPath).catch(() => {
10417
+ await fs20.unlink(shotPath).catch(() => {
10745
10418
  });
10746
10419
  }
10747
10420
  }
@@ -10758,7 +10431,7 @@ async function pruneRuns(projectRoot, opts = {}) {
10758
10431
  } else {
10759
10432
  const dirStats = await getDirectoryStats(c.dirPath);
10760
10433
  if (!opts.dryRun) {
10761
- await fs19.rm(c.dirPath, { recursive: true, force: true });
10434
+ await fs20.rm(c.dirPath, { recursive: true, force: true });
10762
10435
  }
10763
10436
  prunedRuns.push({
10764
10437
  runId: c.runId,
@@ -10790,8 +10463,8 @@ var init_prune = __esm({
10790
10463
 
10791
10464
  // ../../packages/local-store/dist/explore-map.js
10792
10465
  import { randomBytes as randomBytes2 } from "crypto";
10793
- import { promises as fs20 } from "fs";
10794
- import path15 from "path";
10466
+ import { promises as fs21 } from "fs";
10467
+ import path16 from "path";
10795
10468
  function templateToken(token) {
10796
10469
  if (token === "")
10797
10470
  return token;
@@ -10870,7 +10543,7 @@ function fnv1a(s) {
10870
10543
  return h.toString(16).padStart(8, "0");
10871
10544
  }
10872
10545
  function exploreDir(projectRoot) {
10873
- return path15.join(cacheDir(projectRoot), "explore");
10546
+ return path16.join(cacheDir(projectRoot), "explore");
10874
10547
  }
10875
10548
  function composeExploreId(now, randomHex) {
10876
10549
  const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
@@ -10882,17 +10555,17 @@ function isValidExploreId(id) {
10882
10555
  function mapPath(projectRoot, id) {
10883
10556
  if (!isValidExploreId(id))
10884
10557
  throw new Error(`Not an explore map id: ${id}`);
10885
- return path15.join(exploreDir(projectRoot), id, "map.json");
10558
+ return path16.join(exploreDir(projectRoot), id, "map.json");
10886
10559
  }
10887
10560
  async function writeExploreMap(projectRoot, map2) {
10888
10561
  const file = mapPath(projectRoot, map2.id);
10889
- await fs20.mkdir(path15.dirname(file), { recursive: true });
10562
+ await fs21.mkdir(path16.dirname(file), { recursive: true });
10890
10563
  await atomicWriteFile(file, `${JSON.stringify(map2, null, 2)}
10891
10564
  `);
10892
10565
  return file;
10893
10566
  }
10894
10567
  async function readExploreMap(projectRoot, id) {
10895
- const raw = await fs20.readFile(mapPath(projectRoot, id), "utf8");
10568
+ const raw = await fs21.readFile(mapPath(projectRoot, id), "utf8");
10896
10569
  const map2 = JSON.parse(raw);
10897
10570
  if (map2?.schema !== EXPLORE_MAP_SCHEMA || !Array.isArray(map2.surfaces)) {
10898
10571
  throw new Error(`Unreadable explore map: ${id}`);
@@ -10901,7 +10574,7 @@ async function readExploreMap(projectRoot, id) {
10901
10574
  }
10902
10575
  async function listExploreMaps(projectRoot) {
10903
10576
  try {
10904
- const entries = await fs20.readdir(exploreDir(projectRoot), {
10577
+ const entries = await fs21.readdir(exploreDir(projectRoot), {
10905
10578
  withFileTypes: true
10906
10579
  });
10907
10580
  return entries.filter((e) => e.isDirectory() && isValidExploreId(e.name)).map((e) => e.name).sort();
@@ -11002,10 +10675,10 @@ async function readAllRunSteps(projectRoot) {
11002
10675
  function matchSurfaceCapabilities(place, capabilities) {
11003
10676
  const normPlace = place.length > 1 ? place.replace(/\/+$/, "") : place;
11004
10677
  const matchedCaps = /* @__PURE__ */ new Map();
11005
- for (const cap of capabilities) {
11006
- if (!cap.paths || cap.paths.length === 0)
10678
+ for (const cap2 of capabilities) {
10679
+ if (!cap2.paths || cap2.paths.length === 0)
11007
10680
  continue;
11008
- for (const rawPath of cap.paths) {
10681
+ for (const rawPath of cap2.paths) {
11009
10682
  if (!rawPath)
11010
10683
  continue;
11011
10684
  const cleanPath = rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : rawPath;
@@ -11016,9 +10689,9 @@ function matchSurfaceCapabilities(place, capabilities) {
11016
10689
  isMatch = normPlace === cleanPath || normPlace.startsWith(`${cleanPath}/`) || normPlace.startsWith(`${cleanPath}?`) || normPlace.startsWith(`${cleanPath}#`);
11017
10690
  }
11018
10691
  if (isMatch) {
11019
- const prevLen = matchedCaps.get(cap.slug) ?? -1;
10692
+ const prevLen = matchedCaps.get(cap2.slug) ?? -1;
11020
10693
  if (cleanPath.length > prevLen) {
11021
- matchedCaps.set(cap.slug, cleanPath.length);
10694
+ matchedCaps.set(cap2.slug, cleanPath.length);
11022
10695
  }
11023
10696
  }
11024
10697
  }
@@ -11121,94 +10794,831 @@ function groupUnassignedByPrefix(surfaces) {
11121
10794
  g.examples.push(pathOnly);
11122
10795
  }
11123
10796
  }
11124
- return [...groups.values()].sort((a, b) => b.surfaces - a.surfaces || a.prefix.localeCompare(b.prefix));
10797
+ return [...groups.values()].sort((a, b) => b.surfaces - a.surfaces || a.prefix.localeCompare(b.prefix));
10798
+ }
10799
+ async function readCapabilityCoverage(projectRoot, coverage) {
10800
+ const [capsResult, outlinesResult] = await Promise.all([
10801
+ listEntities(projectRoot, "capability"),
10802
+ listEntities(projectRoot, "outline")
10803
+ ]);
10804
+ const caps = capsResult.entities.map((e) => ({
10805
+ slug: e.frontmatter.slug,
10806
+ title: e.frontmatter.title,
10807
+ area: e.frontmatter.area ?? null,
10808
+ paths: e.frontmatter.paths ?? []
10809
+ }));
10810
+ const outlines = outlinesResult.entities.map((e) => ({
10811
+ primary_capability: e.frontmatter.primary_capability,
10812
+ additional_capabilities: e.frontmatter.additional_capabilities ?? []
10813
+ }));
10814
+ return summarizeCapabilityCoverage(coverage.surfaces, caps, outlines);
10815
+ }
10816
+ function actionSignature(a) {
10817
+ 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;
10818
+ return `${a.role} "${a.name}" \u2192 ${where}`;
10819
+ }
10820
+ function diffExploreMaps(a, b) {
10821
+ const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
10822
+ const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
10823
+ const ref = (s) => ({
10824
+ key: s.key,
10825
+ label: s.label
10826
+ });
10827
+ const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
10828
+ const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
10829
+ const changed = [];
10830
+ let common2 = 0;
10831
+ for (const sa of a.surfaces) {
10832
+ const sb = bByKey.get(sa.key);
10833
+ if (!sb)
10834
+ continue;
10835
+ common2++;
10836
+ const setA = new Set(sa.actions.map(actionSignature));
10837
+ const setB = new Set(sb.actions.map(actionSignature));
10838
+ const inA = [...setA].filter((x) => !setB.has(x)).sort();
10839
+ const inB = [...setB].filter((x) => !setA.has(x)).sort();
10840
+ if (inA.length > 0 || inB.length > 0) {
10841
+ changed.push({ ...ref(sa), only_a: inA, only_b: inB });
10842
+ }
10843
+ }
10844
+ const union2 = common2 + onlyA.length + onlyB.length;
10845
+ return {
10846
+ a: a.id,
10847
+ b: b.id,
10848
+ common: common2,
10849
+ only_a: onlyA,
10850
+ only_b: onlyB,
10851
+ surface_overlap: union2 === 0 ? 1 : common2 / union2,
10852
+ changed_actions: changed
10853
+ };
10854
+ }
10855
+ var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
10856
+ var init_explore_map = __esm({
10857
+ "../../packages/local-store/dist/explore-map.js"() {
10858
+ "use strict";
10859
+ init_atomic();
10860
+ init_entity();
10861
+ init_paths();
10862
+ init_revisit();
10863
+ init_runs();
10864
+ init_walkthrough_canonical2();
10865
+ EXPLORE_MAP_SCHEMA = 1;
10866
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10867
+ DATE = /^\d{4}-\d{2}-\d{2}$/;
10868
+ MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
10869
+ }
10870
+ });
10871
+
10872
+ // ../../packages/local-store/dist/journey-diff.js
10873
+ function isProblemFinding(type2) {
10874
+ return PROBLEM_TYPES2.has(type2.toLowerCase());
10875
+ }
10876
+ function levenshteinDistance(a, b) {
10877
+ if (a === b)
10878
+ return 0;
10879
+ if (!a.length)
10880
+ return b.length;
10881
+ if (!b.length)
10882
+ return a.length;
10883
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
10884
+ for (let i = 1; i <= a.length; i++) {
10885
+ let prev = i;
10886
+ for (let j = 1; j <= b.length; j++) {
10887
+ const val = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], prev, row[j]) + 1;
10888
+ row[j - 1] = prev;
10889
+ prev = val;
10890
+ }
10891
+ row[b.length] = prev;
10892
+ }
10893
+ return row[b.length];
10894
+ }
10895
+ function normalizeAction(action) {
10896
+ if (!action)
10897
+ return "";
10898
+ return action.toLowerCase().replace(/['"`]/g, "").replace(/[^\p{L}\p{N}\s/_-]+/gu, " ").replace(/\s+/g, " ").trim();
10899
+ }
10900
+ function actionTokens(action) {
10901
+ const norm = normalizeAction(action);
10902
+ if (!norm)
10903
+ return /* @__PURE__ */ new Set();
10904
+ return new Set(norm.split(/\s+/).filter(Boolean));
10905
+ }
10906
+ function actionSimilarity(a, b) {
10907
+ const normA = normalizeAction(a);
10908
+ const normB = normalizeAction(b);
10909
+ if (!normA && !normB)
10910
+ return 1;
10911
+ if (!normA || !normB)
10912
+ return 0;
10913
+ if (normA === normB)
10914
+ return 1;
10915
+ const tokensA = actionTokens(normA);
10916
+ const tokensB = actionTokens(normB);
10917
+ let intersection = 0;
10918
+ for (const t of tokensA) {
10919
+ if (tokensB.has(t))
10920
+ intersection++;
10921
+ }
10922
+ if (intersection === 0) {
10923
+ if (tokensA.size <= 1 && tokensB.size <= 1) {
10924
+ const maxLen = Math.max(normA.length, normB.length);
10925
+ const levDist = levenshteinDistance(normA, normB);
10926
+ return levDist <= 2 && maxLen > 0 ? 1 - levDist / maxLen : 0;
10927
+ }
10928
+ return 0;
10929
+ }
10930
+ const union2 = tokensA.size + tokensB.size - intersection;
10931
+ const jaccard = union2 > 0 ? intersection / union2 : 0;
10932
+ const minSize = Math.min(tokensA.size, tokensB.size);
10933
+ const subsetRatio = minSize > 0 ? intersection / minSize : 0;
10934
+ if (normA.includes(normB) || normB.includes(normA) || subsetRatio === 1) {
10935
+ return Math.max(0.85, 0.7 + 0.3 * jaccard);
10936
+ }
10937
+ return 0.5 * jaccard + 0.5 * subsetRatio;
10938
+ }
10939
+ function routeSimilarity(urlA, surfaceA, urlB, surfaceB) {
10940
+ const normUrlA = normalizePlaceKey(urlA);
10941
+ const normUrlB = normalizePlaceKey(urlB);
10942
+ const hasUrlA = normUrlA !== null;
10943
+ const hasUrlB = normUrlB !== null;
10944
+ let urlScore = null;
10945
+ if (hasUrlA && hasUrlB) {
10946
+ if (normUrlA === normUrlB) {
10947
+ urlScore = 1;
10948
+ } else {
10949
+ const pathA = normUrlA.split("?")[0];
10950
+ const pathB = normUrlB.split("?")[0];
10951
+ urlScore = pathA === pathB ? 0.8 : 0;
10952
+ }
10953
+ }
10954
+ const sA = surfaceA?.trim();
10955
+ const sB = surfaceB?.trim();
10956
+ let surfaceScore = null;
10957
+ if (sA && sB) {
10958
+ if (surfaceKey(sA) === surfaceKey(sB)) {
10959
+ surfaceScore = 1;
10960
+ } else if (surfaceMatches(sA, sB) || surfaceMatches(sB, sA)) {
10961
+ surfaceScore = 0.8;
10962
+ } else {
10963
+ surfaceScore = 0;
10964
+ }
10965
+ }
10966
+ if (urlScore !== null && surfaceScore !== null) {
10967
+ if (urlScore >= 0.8 && surfaceScore >= 0.8)
10968
+ return 1;
10969
+ if (urlScore >= 0.8)
10970
+ return 0.85;
10971
+ if (surfaceScore >= 0.8)
10972
+ return 0.75;
10973
+ return 0;
10974
+ }
10975
+ if (urlScore !== null)
10976
+ return urlScore;
10977
+ if (surfaceScore !== null)
10978
+ return surfaceScore;
10979
+ return 0.5;
10980
+ }
10981
+ function stepSimilarity(a, b) {
10982
+ const rSim = routeSimilarity(a.url, a.surface, b.url, b.surface);
10983
+ const aSim = actionSimilarity(a.action, b.action);
10984
+ if (aSim < 0.25 && a.action && b.action) {
10985
+ return Math.min(0.2, rSim * 0.2);
10986
+ }
10987
+ if (rSim === 0) {
10988
+ return aSim >= 0.8 ? aSim * 0.4 : 0.1;
10989
+ }
10990
+ return 0.5 * rSim + 0.5 * aSim;
10991
+ }
10992
+ function findingMatchesStep(finding, stepNumber2) {
10993
+ if (finding.linked_step_ids && finding.linked_step_ids.length > 0) {
10994
+ return finding.linked_step_ids.some((id) => id === `step-${stepNumber2}` || id === String(stepNumber2));
10995
+ }
10996
+ if (finding.walkthrough_step_id) {
10997
+ const raw = finding.walkthrough_step_id.trim();
10998
+ if (raw === `step-${stepNumber2}` || raw === String(stepNumber2)) {
10999
+ return true;
11000
+ }
11001
+ }
11002
+ return false;
11003
+ }
11004
+ function isStallNote(note) {
11005
+ if (!note)
11006
+ return false;
11007
+ const lower = note.toLowerCase();
11008
+ return STALL_KEYWORDS.some((kw) => lower.includes(kw));
11009
+ }
11010
+ function alignJourneySteps(scenario, baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings) {
11011
+ const m = baselineSteps.length;
11012
+ const n = currentSteps.length;
11013
+ const GAP_PENALTY = -0.7;
11014
+ const MATCH_THRESHOLD = 0.45;
11015
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
11016
+ for (let i2 = 0; i2 <= m; i2++)
11017
+ dp[i2][0] = i2 * GAP_PENALTY;
11018
+ for (let j2 = 0; j2 <= n; j2++)
11019
+ dp[0][j2] = j2 * GAP_PENALTY;
11020
+ for (let i2 = 1; i2 <= m; i2++) {
11021
+ for (let j2 = 1; j2 <= n; j2++) {
11022
+ const sim = stepSimilarity(baselineSteps[i2 - 1], currentSteps[j2 - 1]);
11023
+ const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
11024
+ const match2 = dp[i2 - 1][j2 - 1] + matchScore;
11025
+ const delP = dp[i2 - 1][j2] + GAP_PENALTY;
11026
+ const insC = dp[i2][j2 - 1] + GAP_PENALTY;
11027
+ dp[i2][j2] = Math.max(match2, delP, insC);
11028
+ }
11029
+ }
11030
+ const alignedPairs = [];
11031
+ let i = m;
11032
+ let j = n;
11033
+ while (i > 0 || j > 0) {
11034
+ if (i > 0 && j > 0) {
11035
+ const sim = stepSimilarity(baselineSteps[i - 1], currentSteps[j - 1]);
11036
+ const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
11037
+ const scoreFromMatch = dp[i - 1][j - 1] + matchScore;
11038
+ if (Math.abs(dp[i][j] - scoreFromMatch) < 1e-6 && sim >= MATCH_THRESHOLD) {
11039
+ alignedPairs.push({
11040
+ baseline: baselineSteps[i - 1],
11041
+ current: currentSteps[j - 1]
11042
+ });
11043
+ i--;
11044
+ j--;
11045
+ continue;
11046
+ }
11047
+ }
11048
+ if (j > 0 && Math.abs(dp[i][j] - (dp[i][j - 1] + GAP_PENALTY)) < 1e-6) {
11049
+ alignedPairs.push({
11050
+ baseline: null,
11051
+ current: currentSteps[j - 1]
11052
+ });
11053
+ j--;
11054
+ } else if (i > 0 && Math.abs(dp[i][j] - (dp[i - 1][j] + GAP_PENALTY)) < 1e-6) {
11055
+ alignedPairs.push({
11056
+ baseline: baselineSteps[i - 1],
11057
+ current: null
11058
+ });
11059
+ i--;
11060
+ } else if (i > 0 && j > 0) {
11061
+ alignedPairs.push({
11062
+ baseline: baselineSteps[i - 1],
11063
+ current: currentSteps[j - 1]
11064
+ });
11065
+ i--;
11066
+ j--;
11067
+ } else if (i > 0) {
11068
+ alignedPairs.push({
11069
+ baseline: baselineSteps[i - 1],
11070
+ current: null
11071
+ });
11072
+ i--;
11073
+ } else {
11074
+ alignedPairs.push({
11075
+ baseline: null,
11076
+ current: currentSteps[j - 1]
11077
+ });
11078
+ j--;
11079
+ }
11080
+ }
11081
+ alignedPairs.reverse();
11082
+ const maxBaselineStepNum = baselineSteps.length > 0 ? Math.max(...baselineSteps.map((s) => s.step_number)) : -1;
11083
+ const alignedSteps = [];
11084
+ const stats = {
11085
+ totalAlignedSteps: alignedPairs.length,
11086
+ same: 0,
11087
+ resolved: 0,
11088
+ persists: 0,
11089
+ regressions: 0,
11090
+ stalledInBaseline: 0,
11091
+ bypassed: 0,
11092
+ newPath: 0
11093
+ };
11094
+ for (let idx = 0; idx < alignedPairs.length; idx++) {
11095
+ const pair = alignedPairs[idx];
11096
+ const bStep = pair.baseline;
11097
+ const cStep = pair.current;
11098
+ const bFindings = bStep ? baselineFindings.filter((f) => findingMatchesStep(f, bStep.step_number)) : [];
11099
+ const cFindings = cStep ? currentFindings.filter((f) => findingMatchesStep(f, cStep.step_number)) : [];
11100
+ const bProblems = bFindings.filter((f) => isProblemFinding(f.type));
11101
+ const cProblems = cFindings.filter((f) => isProblemFinding(f.type));
11102
+ let status2 = "same";
11103
+ let summary = "Clean";
11104
+ if (bStep && cStep) {
11105
+ const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
11106
+ if (bProblems.length > 0 && cProblems.length === 0) {
11107
+ status2 = "resolved";
11108
+ stats.resolved++;
11109
+ const probDesc = bProblems[0].ui_fact || bProblems[0].title || bProblems[0].id;
11110
+ summary = wasStall ? `Previous stall resolved \u2014 journey unblocked (${probDesc})` : `Resolved previous issue: ${probDesc}`;
11111
+ } else if (bProblems.length > 0 && cProblems.length > 0) {
11112
+ status2 = "persists";
11113
+ stats.persists++;
11114
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11115
+ summary = `Problem persists: ${probDesc}`;
11116
+ } else if (bProblems.length === 0 && cProblems.length > 0) {
11117
+ status2 = "regression";
11118
+ stats.regressions++;
11119
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11120
+ summary = `New issue reported: ${probDesc}`;
11121
+ } else {
11122
+ status2 = "same";
11123
+ stats.same++;
11124
+ summary = "Clean";
11125
+ }
11126
+ } else if (bStep && !cStep) {
11127
+ const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
11128
+ if (wasStall) {
11129
+ status2 = "stalled";
11130
+ stats.stalledInBaseline++;
11131
+ const probDesc = bProblems[0]?.ui_fact || bProblems[0]?.title || "journey halted";
11132
+ summary = `Stalled in baseline: ${probDesc}`;
11133
+ } else {
11134
+ status2 = "bypassed";
11135
+ stats.bypassed++;
11136
+ summary = "Bypassed in current run";
11137
+ }
11138
+ } else if (!bStep && cStep) {
11139
+ if (cProblems.length > 0) {
11140
+ status2 = "regression";
11141
+ stats.regressions++;
11142
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11143
+ summary = `New step with issue: ${probDesc}`;
11144
+ } else {
11145
+ status2 = "new_path";
11146
+ stats.newPath++;
11147
+ summary = "New path / unblocked progress";
11148
+ }
11149
+ }
11150
+ alignedSteps.push({
11151
+ index: idx + 1,
11152
+ status: status2,
11153
+ baselineStep: bStep,
11154
+ currentStep: cStep,
11155
+ baselineFindings: bFindings,
11156
+ currentFindings: cFindings,
11157
+ summary
11158
+ });
11159
+ }
11160
+ return {
11161
+ scenario,
11162
+ baselineRunId,
11163
+ currentRunId,
11164
+ steps: alignedSteps,
11165
+ stats
11166
+ };
11167
+ }
11168
+ function toStepFindingInfo(f) {
11169
+ return {
11170
+ id: f.frontmatter.id,
11171
+ title: f.frontmatter.title ?? f.slug,
11172
+ type: f.frontmatter.type,
11173
+ severity: f.frontmatter.severity,
11174
+ ui_fact: f.frontmatter.ui_fact,
11175
+ consequence: f.frontmatter.consequence,
11176
+ canonical_id: f.frontmatter.canonical_id,
11177
+ walkthrough_step_id: f.frontmatter.walkthrough_step_id
11178
+ };
11179
+ }
11180
+ function toJourneyStepInfo(s) {
11181
+ return {
11182
+ step_number: s.step_number,
11183
+ action: s.action,
11184
+ url: s.url ?? void 0,
11185
+ surface: s.surface ?? void 0,
11186
+ note: s.note ?? void 0
11187
+ };
11188
+ }
11189
+ function journeyStepRoute(step) {
11190
+ return step.currentStep?.url && normalizePlaceKey(step.currentStep.url) || step.baselineStep?.url && normalizePlaceKey(step.baselineStep.url) || step.currentStep?.surface || step.baselineStep?.surface || "\u2014";
11191
+ }
11192
+ async function computeJourneyDiff(projectRoot, baselineRunId, currentRunId, scenarioSlug) {
11193
+ const [bStepsRaw, bFindingsRaw, cStepsRaw, cFindingsRaw] = await Promise.all([
11194
+ readStepsForRun(projectRoot, baselineRunId),
11195
+ readFindingsForRun(projectRoot, baselineRunId),
11196
+ readStepsForRun(projectRoot, currentRunId),
11197
+ readFindingsForRun(projectRoot, currentRunId)
11198
+ ]);
11199
+ const baselineSteps = bStepsRaw.map(toJourneyStepInfo);
11200
+ const baselineFindings = bFindingsRaw.map(toStepFindingInfo);
11201
+ const currentSteps = cStepsRaw.map(toJourneyStepInfo);
11202
+ const currentFindings = cFindingsRaw.map(toStepFindingInfo);
11203
+ return alignJourneySteps(scenarioSlug ?? "", baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings);
11204
+ }
11205
+ function renderJourneyDiffMarkdown(diff) {
11206
+ const lines = [
11207
+ "### Chronological journey diff",
11208
+ "",
11209
+ `Baseline: \`${diff.baselineRunId}\` vs Current: \`${diff.currentRunId}\``
11210
+ ];
11211
+ const parts = [];
11212
+ if (diff.stats.same > 0)
11213
+ parts.push(`${diff.stats.same} unchanged`);
11214
+ if (diff.stats.resolved > 0)
11215
+ parts.push(`${diff.stats.resolved} resolved`);
11216
+ if (diff.stats.persists > 0)
11217
+ parts.push(`${diff.stats.persists} persists`);
11218
+ if (diff.stats.regressions > 0)
11219
+ parts.push(`${diff.stats.regressions} new issues`);
11220
+ if (diff.stats.stalledInBaseline > 0)
11221
+ parts.push(`${diff.stats.stalledInBaseline} stalled in baseline`);
11222
+ if (diff.stats.bypassed > 0)
11223
+ parts.push(`${diff.stats.bypassed} bypassed`);
11224
+ if (diff.stats.newPath > 0)
11225
+ parts.push(`${diff.stats.newPath} new path steps`);
11226
+ if (parts.length > 0) {
11227
+ lines.push(`- **Summary**: ${parts.join(", ")}`);
11228
+ }
11229
+ lines.push("", "| # | Route | Baseline (Run N-1) | Current (Run N) | Status | Notes |", "|---|-------|--------------------|-----------------|--------|-------|");
11230
+ for (const step of diff.steps) {
11231
+ const route = journeyStepRoute(step);
11232
+ const baseAction = step.baselineStep ? `Step ${step.baselineStep.step_number}: ${escapeMarkdownCell(step.baselineStep.action)}` : "\u2014";
11233
+ const currAction = step.currentStep ? `Step ${step.currentStep.step_number}: ${escapeMarkdownCell(step.currentStep.action)}` : "\u2014";
11234
+ const statusBadge = `\`${step.status}\``;
11235
+ const notes = escapeMarkdownCell(step.summary);
11236
+ lines.push(`| ${step.index} | \`${escapeMarkdownCell(route)}\` | ${baseAction} | ${currAction} | ${statusBadge} | ${notes} |`);
11237
+ }
11238
+ return lines.join("\n");
11239
+ }
11240
+ function escapeMarkdownCell(text) {
11241
+ return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
11242
+ }
11243
+ var PROBLEM_TYPES2, STALL_KEYWORDS;
11244
+ var init_journey_diff = __esm({
11245
+ "../../packages/local-store/dist/journey-diff.js"() {
11246
+ "use strict";
11247
+ init_revisit();
11248
+ init_walkthrough_canonical2();
11249
+ PROBLEM_TYPES2 = /* @__PURE__ */ new Set(["hinders", "missing", "incorrect"]);
11250
+ STALL_KEYWORDS = [
11251
+ "blocked",
11252
+ "unable to proceed",
11253
+ "stalled",
11254
+ "halted",
11255
+ "stuck",
11256
+ "cannot proceed",
11257
+ "could not proceed",
11258
+ "failed to load",
11259
+ "timed out"
11260
+ ];
11261
+ }
11262
+ });
11263
+
11264
+ // ../../packages/local-store/dist/report-markdown.js
11265
+ function runVerdict(canonical) {
11266
+ const judged = (canonical?.human_tests ?? []).map((t) => t.verdict).filter((v) => v === "yes" || v === "no" || v === "partial");
11267
+ if (judged.length === 0)
11268
+ return null;
11269
+ if (judged.every((v) => v === "yes"))
11270
+ return "yes";
11271
+ if (judged.every((v) => v === "no"))
11272
+ return "no";
11273
+ return "partial";
11274
+ }
11275
+ function rank(value, order) {
11276
+ const i = order.indexOf(value ?? "");
11277
+ return i === -1 ? order.length : i;
11278
+ }
11279
+ function pagePath(url) {
11280
+ if (!url)
11281
+ return null;
11282
+ try {
11283
+ return new URL(url).pathname;
11284
+ } catch {
11285
+ return url;
11286
+ }
11287
+ }
11288
+ function dayOf(value) {
11289
+ const m = /^(\d{4}-\d{2}-\d{2})/.exec(value ?? "");
11290
+ return m?.[1] ?? null;
11291
+ }
11292
+ function stepNumbers(ids) {
11293
+ const nums = (ids ?? []).map((id) => /\d+/.exec(id)?.[0]).filter((n) => Boolean(n));
11294
+ if (nums.length === 0)
11295
+ return "";
11296
+ return ` (step${nums.length === 1 ? "" : "s"} ${nums.join(", ")})`;
11297
+ }
11298
+ function titleOf(f) {
11299
+ if (f.title)
11300
+ return f.title;
11301
+ const fact = f.ui_fact ?? f.id;
11302
+ return /^(.+?[.!?])(\s|$)/.exec(fact)?.[1] ?? fact;
11303
+ }
11304
+ function renderHumanTest(c, lines) {
11305
+ lines.push("", "## The human test");
11306
+ const verdict = runVerdict(c);
11307
+ const label = verdict ? `**${cap(verdict)}**` : c.blocked ? "_skipped (walk blocked)_" : "_pending judge_";
11308
+ lines.push("", `**${c.human_test_overall.question}** ${label}`);
11309
+ if (c.human_test_overall.answer) {
11310
+ lines.push("", c.human_test_overall.answer);
11311
+ if (c.human_test_overall.reasoning) {
11312
+ lines.push("", c.human_test_overall.reasoning);
11313
+ }
11314
+ }
11315
+ if (c.human_tests.length === 0) {
11316
+ lines.push("", "_No human tests on the outline._");
11317
+ return;
11318
+ }
11319
+ lines.push("");
11320
+ for (const ht of c.human_tests) {
11321
+ const v = ht.verdict ? `**${cap(ht.verdict.replace("_", " "))}.**` : "_Pending._";
11322
+ lines.push(`- ${stripAnswerHint(ht.question)} ${v}${ht.reasoning ? ` ${ht.reasoning}` : ""}`);
11323
+ }
11324
+ }
11325
+ function severityShift(before, now) {
11326
+ if (!before || !now || before === now)
11327
+ return "";
11328
+ const b = rank(before, SEVERITY_ORDER);
11329
+ const n = rank(now, SEVERITY_ORDER);
11330
+ if (b === n)
11331
+ return "";
11332
+ return `${n > b ? "less severe" : "worse"}, ${before} \u2192 ${now}`;
11333
+ }
11334
+ function checkOutcomes(c) {
11335
+ const checks = c.checks;
11336
+ if (!checks)
11337
+ return [];
11338
+ const v = c.verification;
11339
+ const fixed = new Set(v?.resolved.map((r) => r.canonical_id) ?? []);
11340
+ const seen = new Set(c.findings.map((f) => f.canonical_id).filter(Boolean));
11341
+ return checks.fixes.map((f) => ({
11342
+ ...f,
11343
+ outcome: f.canonical_id && fixed.has(f.canonical_id) ? "fixed" : f.canonical_id && seen.has(f.canonical_id) ? "came_back" : "unconfirmed"
11344
+ }));
11345
+ }
11346
+ function renderChecks(c, checks, lines) {
11347
+ lines.push("", "## What this run checks");
11348
+ const fixes = checkOutcomes(c);
11349
+ if (fixes.length > 0) {
11350
+ const count = (o) => fixes.filter((f) => f.outcome === o).length;
11351
+ lines.push("", `${fixes.length} fix${fixes.length === 1 ? " was" : "es were"} waiting for a re-check: ${count("fixed")} confirmed fixed, ${count("came_back")} came back, ${count("unconfirmed")} not confirmed either way.`, "");
11352
+ const order = ["came_back", "fixed", "unconfirmed"];
11353
+ const sorted = [...fixes].sort((a, b) => order.indexOf(a.outcome) - order.indexOf(b.outcome));
11354
+ for (const f of sorted) {
11355
+ const where = [
11356
+ pagePath(f.page) ? `\`${pagePath(f.page)}\`` : "",
11357
+ f.reached ? "" : "not reached"
11358
+ ].filter(Boolean).join(", ");
11359
+ lines.push(`- ${OUTCOME_TEXT[f.outcome]}: **${f.title}**${f.fix ? `. Fix: ${f.fix}` : ""}${where ? ` (${where})` : ""}`);
11360
+ }
11361
+ }
11362
+ if (checks.directions.length > 0) {
11363
+ lines.push("", `Directions completed since the last run: ${checks.directions.map((d) => `**${d}**`).join(", ")}.`);
11364
+ }
11365
+ }
11366
+ function sinceRow(item, outcome) {
11367
+ const qualifiers = [
11368
+ outcome === "Persists" ? severityShift(item.severity_before, item.severity_now) : "",
11369
+ outcome === "Persists" && item.fixed_before ? "the fix did not hold" : ""
11370
+ ].filter(Boolean);
11371
+ const now = outcome === "Resolved" || outcome === "Persists" ? `**${outcome}**${qualifiers.length > 0 ? ` (${qualifiers.join("; ")})` : ""}` : outcome;
11372
+ const note = outcome === "Not seen" ? `The walk did not pass this way${item.fixed_before ? " (fixed, awaiting a re-check)" : ""}. Not evidence of a fix.` : `${item.note ?? ""}${stepNumbers(item.step_ids)}`.trim();
11373
+ return `| ${cell(item.title)} | ${now} | ${cell(note) || "\u2014"} |`;
11374
+ }
11375
+ function renderSince(v, newProblems, newStrengths, lines) {
11376
+ const day = dayOf(v.baseline_run_id);
11377
+ lines.push("", "## Since the last run", "", `Compared with the run of ${day ?? v.baseline_run_id}.`);
11378
+ const rows = [
11379
+ ...v.resolved.map((i) => sinceRow(i, "Resolved")),
11380
+ ...v.persists.map((i) => sinceRow(i, "Persists")),
11381
+ ...v.not_observed.map((i) => sinceRow(i, "Not seen")),
11382
+ ...v.uncertain.map((i) => sinceRow(i, "Unclear"))
11383
+ ];
11384
+ if (rows.length > 0) {
11385
+ lines.push("", "| Earlier finding | Now | Notes |", "|---|---|---|", ...rows);
11386
+ }
11387
+ const added = [
11388
+ newProblems > 0 ? `${newProblems} problem${newProblems === 1 ? "" : "s"}` : "",
11389
+ newStrengths > 0 ? `${newStrengths} strength${newStrengths === 1 ? "" : "s"}` : ""
11390
+ ].filter(Boolean);
11391
+ lines.push("", added.length > 0 ? `New this run: ${added.join(" and ")}, marked _new_ below.` : "Nothing new this run.");
11392
+ }
11393
+ function renderFinding(c, f, n, since, lines) {
11394
+ lines.push("", `### ${n}. ${titleOf(f)}`);
11395
+ const steps = f.linked_step_ids.map((id) => c.steps.find((s) => s.id === id)).filter((s) => Boolean(s));
11396
+ const page = pagePath(f.page_url ?? steps[0]?.url);
11397
+ const where = [
11398
+ page ? `\`${page}\`` : "",
11399
+ steps.length > 0 ? `step ${steps.map((s) => s.step_number).join(", ")}` : ""
11400
+ ].filter(Boolean).join(" ");
11401
+ const meta = [
11402
+ f.severity ? `**${cap(f.severity)}**` : "",
11403
+ f.type,
11404
+ f.task_impact ? TASK_IMPACT[f.task_impact] : "",
11405
+ where,
11406
+ since ? `_${since}_` : ""
11407
+ ].filter(Boolean);
11408
+ lines.push("", meta.join(" \xB7 "));
11409
+ if (f.ui_fact === null || f.consequence === null) {
11410
+ lines.push("", "> _Legacy finding: predates the UI-fact / consequence depth gate._");
11411
+ }
11412
+ if (f.ui_fact)
11413
+ lines.push("", f.ui_fact);
11414
+ if (f.consequence) {
11415
+ const who = c.scenario.persona_name;
11416
+ lines.push("", `> ${f.consequence}${who ? ` \u2014 ${who}` : ""}`);
11417
+ }
11418
+ if (f.recommendation)
11419
+ lines.push("", `**Recommendation:** ${f.recommendation}`);
11420
+ for (const s of steps) {
11421
+ if (s.screenshot_ref) {
11422
+ lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
11423
+ }
11424
+ }
11425
+ }
11426
+ function renderAnalysis(analysis, lines) {
11427
+ if (analysis.directions.length > 0) {
11428
+ lines.push("", "## Directions", "", `_Structural changes proposed by ${analysis.advisor} after the walk. Reach is a prediction until the listed scenarios are re-run._`);
11429
+ for (const d of analysis.directions) {
11430
+ lines.push("", `### ${d.title} (${d.size})`, "", d.change);
11431
+ lines.push("", `Absorbs ${d.absorbs.length} finding${d.absorbs.length === 1 ? "" : "s"}.`);
11432
+ for (const r of d.reach.filter((x) => x.effect === "helps"))
11433
+ lines.push(`- Likely helps ${r.scenario}: ${r.reason}`);
11434
+ for (const r of d.reach.filter((x) => x.effect === "risk"))
11435
+ lines.push(`- Could hurt ${r.scenario}: ${r.reason}`);
11436
+ if (d.verify.length > 0)
11437
+ lines.push("", `Re-run to verify: ${d.verify.join(", ")}`);
11438
+ }
11439
+ }
11440
+ if (analysis.observations.length > 0) {
11441
+ lines.push("", "## Also worth knowing", "");
11442
+ for (const o of analysis.observations) {
11443
+ const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
11444
+ lines.push(`- **${label}: ${o.title}.** ${o.detail}`);
11445
+ if (o.suggested_scenario) {
11446
+ lines.push(` Suggested scenario: ${o.suggested_scenario}`);
11447
+ }
11448
+ }
11449
+ }
11125
11450
  }
11126
- async function readCapabilityCoverage(projectRoot, coverage) {
11127
- const [capsResult, outlinesResult] = await Promise.all([
11128
- listEntities(projectRoot, "capability"),
11129
- listEntities(projectRoot, "outline")
11130
- ]);
11131
- const caps = capsResult.entities.map((e) => ({
11132
- slug: e.frontmatter.slug,
11133
- title: e.frontmatter.title,
11134
- area: e.frontmatter.area ?? null,
11135
- paths: e.frontmatter.paths ?? []
11136
- }));
11137
- const outlines = outlinesResult.entities.map((e) => ({
11138
- primary_capability: e.frontmatter.primary_capability,
11139
- additional_capabilities: e.frontmatter.additional_capabilities ?? []
11140
- }));
11141
- return summarizeCapabilityCoverage(coverage.surfaces, caps, outlines);
11451
+ function renderRunDetails(c, lines) {
11452
+ lines.push("", "## Run details", "", `- Run ID: ${c.run_id}`, `- Target URL: ${c.target.url}`);
11453
+ if (c.environment)
11454
+ lines.push(`- Environment: ${c.environment}`);
11455
+ if (c.build) {
11456
+ const commit = c.build.git_sha?.slice(0, 7);
11457
+ const build = [
11458
+ c.build.label,
11459
+ c.build.git_branch,
11460
+ c.build.git_dirty && c.build.worktree_hash ? `${commit ?? "HEAD"} + uncommitted changes (${c.build.worktree_hash.slice(0, 6)}\u2026)` : commit,
11461
+ c.build.ci?.provider
11462
+ ].filter(Boolean).join(" \xB7 ");
11463
+ if (build)
11464
+ lines.push(`- Build: ${build}`);
11465
+ }
11466
+ if (c.viewport) {
11467
+ lines.push(`- Viewport: ${c.viewport.device} (${c.viewport.width}\xD7${c.viewport.height})`);
11468
+ }
11469
+ lines.push(`- Started: ${c.provenance.started_at}`, `- Completed: ${c.provenance.completed_at}`, `- Walker prompt: ${c.provenance.walker_prompt.id}@${c.provenance.walker_prompt.version}`);
11470
+ const models = c.models;
11471
+ if (models) {
11472
+ const parts = [
11473
+ models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
11474
+ models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
11475
+ models.matcher ? `Matcher: ${models.matcher}` : ""
11476
+ ].filter(Boolean);
11477
+ if (parts.length > 0)
11478
+ lines.push(`- Models: ${parts.join(" \xB7 ")}`);
11479
+ if (typeof models.velocity_sec_per_step === "number") {
11480
+ lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
11481
+ }
11482
+ }
11483
+ const diff = c.journey_diff;
11484
+ if (diff)
11485
+ lines.push("", renderJourneyDiffMarkdown(diff));
11142
11486
  }
11143
- function actionSignature(a) {
11144
- 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;
11145
- return `${a.role} "${a.name}" \u2192 ${where}`;
11487
+ function renderSteps(c, lines) {
11488
+ lines.push("", "## Steps");
11489
+ if (c.steps.length === 0) {
11490
+ lines.push("", "_No steps recorded._");
11491
+ return;
11492
+ }
11493
+ for (const s of c.steps) {
11494
+ lines.push("", `### ${s.step_number}. ${s.screen_title ?? "(untitled)"} \u2014 ${s.id}`);
11495
+ const where = [s.surface, s.url].filter(Boolean).join(" \u2014 ");
11496
+ if (where)
11497
+ lines.push("", `Page: ${where}`);
11498
+ if (s.persona_reasoning)
11499
+ lines.push("", `> ${s.persona_reasoning.replace(/\n/g, "\n> ")}`);
11500
+ if (s.grounding)
11501
+ lines.push("", `Grounding: ${s.grounding}`);
11502
+ if (s.screenshot_ref)
11503
+ lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
11504
+ }
11146
11505
  }
11147
- function diffExploreMaps(a, b) {
11148
- const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
11149
- const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
11150
- const ref = (s) => ({
11151
- key: s.key,
11152
- label: s.label
11153
- });
11154
- const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
11155
- const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
11156
- const changed = [];
11157
- let common2 = 0;
11158
- for (const sa of a.surfaces) {
11159
- const sb = bByKey.get(sa.key);
11160
- if (!sb)
11161
- continue;
11162
- common2++;
11163
- const setA = new Set(sa.actions.map(actionSignature));
11164
- const setB = new Set(sb.actions.map(actionSignature));
11165
- const inA = [...setA].filter((x) => !setB.has(x)).sort();
11166
- const inB = [...setB].filter((x) => !setA.has(x)).sort();
11167
- if (inA.length > 0 || inB.length > 0) {
11168
- changed.push({ ...ref(sa), only_a: inA, only_b: inB });
11506
+ function renderCanonicalMarkdown(canonical) {
11507
+ const c = canonical;
11508
+ const persona = c.scenario.persona_name ?? c.scenario.persona_slug;
11509
+ const situation = c.scenario.outline_title ?? c.scenario.outline_slug;
11510
+ const heading = persona && situation ? `${persona} \xB7 ${situation}` : c.scenario.slug;
11511
+ const runNumber = c.run_number;
11512
+ const subtitle = [
11513
+ dayOf(c.provenance.completed_at),
11514
+ runNumber ? `run ${runNumber} of this scenario` : "",
11515
+ `${c.steps.length} step${c.steps.length === 1 ? "" : "s"}`,
11516
+ c.target.url
11517
+ ].filter(Boolean).join(" \xB7 ");
11518
+ const lines = [
11519
+ "<!-- GENERATED: do not edit by hand. Regen via complete_walkthrough or local-store renderer. -->",
11520
+ "",
11521
+ `# ${heading}`,
11522
+ "",
11523
+ subtitle
11524
+ ];
11525
+ if (c.scenario.persona_role) {
11526
+ lines.push("", `${persona}, ${c.scenario.persona_role}.`);
11527
+ }
11528
+ if (c.blocked) {
11529
+ lines.push("", "> **Blocked \u2014 the walk could not start as it was set up.**", `> ${c.blocked.reason}`, ">", "> This run is not compared with earlier runs and proves nothing about earlier findings. Fix the setup (the scenario's starting state or the persona's access) and run it again.");
11530
+ }
11531
+ const checks = c.checks;
11532
+ if (checks)
11533
+ renderChecks(c, checks, lines);
11534
+ renderHumanTest(c, lines);
11535
+ const verification = c.verification;
11536
+ const newIds = new Set(verification?.new_findings.map((n) => n.id) ?? []);
11537
+ const problems = c.findings.filter((f) => f.type !== "helps").sort((a, b) => rank(a.severity, SEVERITY_ORDER) - rank(b.severity, SEVERITY_ORDER) || rank(a.type, TYPE_ORDER) - rank(b.type, TYPE_ORDER));
11538
+ const strengths = c.findings.filter((f) => f.type === "helps");
11539
+ if (verification) {
11540
+ renderSince(verification, problems.filter((f) => newIds.has(f.id)).length, strengths.filter((f) => newIds.has(f.id)).length, lines);
11541
+ }
11542
+ lines.push("", "## Findings");
11543
+ if (problems.length === 0) {
11544
+ lines.push("", "_No problems recorded._");
11545
+ } else {
11546
+ const bySeverity = SEVERITY_ORDER.map((s) => {
11547
+ const n = problems.filter((f) => f.severity === s).length;
11548
+ return n > 0 ? `${n} ${s}` : "";
11549
+ }).filter(Boolean);
11550
+ lines.push("", `${problems.length} problem${problems.length === 1 ? "" : "s"} (${bySeverity.join(", ")}), worst first.`);
11551
+ const persisting = new Set(verification?.persists.map((p) => p.canonical_id) ?? []);
11552
+ problems.forEach((f, i) => {
11553
+ const since = newIds.has(f.id) ? "new" : f.canonical_id && persisting.has(f.canonical_id) ? "persists" : null;
11554
+ renderFinding(c, f, i + 1, since, lines);
11555
+ });
11556
+ }
11557
+ const analysis = c.analysis;
11558
+ if (analysis)
11559
+ renderAnalysis(analysis, lines);
11560
+ const what = c.what_worked ?? [];
11561
+ if (strengths.length > 0 || what.length > 0) {
11562
+ lines.push("", "## What worked", "");
11563
+ for (const f of strengths) {
11564
+ lines.push(`- **${titleOf(f)}.** ${f.ui_fact ?? ""}`.trimEnd());
11169
11565
  }
11566
+ for (const w of what)
11567
+ lines.push(`- ${w}`);
11170
11568
  }
11171
- const union2 = common2 + onlyA.length + onlyB.length;
11172
- return {
11173
- a: a.id,
11174
- b: b.id,
11175
- common: common2,
11176
- only_a: onlyA,
11177
- only_b: onlyB,
11178
- surface_overlap: union2 === 0 ? 1 : common2 / union2,
11179
- changed_actions: changed
11180
- };
11569
+ const open2 = c.open_questions ?? [];
11570
+ if (open2.length > 0) {
11571
+ lines.push("", "## Open questions", "");
11572
+ for (const q of open2)
11573
+ lines.push(`- ${q}`);
11574
+ }
11575
+ const setup = c.setup_notes ?? [];
11576
+ if (setup.length > 0) {
11577
+ lines.push("", "## Test setup notes", "", "_Doubts about the test, not the product: for whoever stages the scenario._", "");
11578
+ for (const n of setup)
11579
+ lines.push(`- ${n}`);
11580
+ }
11581
+ renderSteps(c, lines);
11582
+ renderRunDetails(c, lines);
11583
+ return `${lines.join("\n")}
11584
+ `;
11181
11585
  }
11182
- var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
11183
- var init_explore_map = __esm({
11184
- "../../packages/local-store/dist/explore-map.js"() {
11586
+ var SEVERITY_ORDER, TYPE_ORDER, TASK_IMPACT, cap, stripAnswerHint, cell, OUTCOME_TEXT;
11587
+ var init_report_markdown = __esm({
11588
+ "../../packages/local-store/dist/report-markdown.js"() {
11185
11589
  "use strict";
11186
- init_atomic();
11187
- init_entity();
11188
- init_paths();
11189
- init_revisit();
11190
- init_runs();
11191
- init_walkthrough_canonical2();
11192
- EXPLORE_MAP_SCHEMA = 1;
11193
- UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11194
- DATE = /^\d{4}-\d{2}-\d{2}$/;
11195
- MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
11590
+ init_journey_diff();
11591
+ SEVERITY_ORDER = ["critical", "major", "minor", "observation"];
11592
+ TYPE_ORDER = ["incorrect", "hinders", "missing", "irrelevant", "helps"];
11593
+ TASK_IMPACT = {
11594
+ blocks: "blocks the task",
11595
+ impairs: "impairs the task",
11596
+ slows: "slows the task"
11597
+ };
11598
+ cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
11599
+ stripAnswerHint = (q) => q.replace(/\s*\((?:yes|no|partial)(?:\s*\/\s*(?:yes|no|partial))+\)\s*$/i, "");
11600
+ cell = (s) => s.replace(/\|/g, "\\|").replace(/\n+/g, " ");
11601
+ OUTCOME_TEXT = {
11602
+ fixed: "**confirmed fixed**",
11603
+ came_back: "**came back**",
11604
+ unconfirmed: "not confirmed"
11605
+ };
11196
11606
  }
11197
11607
  });
11198
11608
 
11199
11609
  // ../../packages/local-store/dist/human-test-link.js
11200
- import { promises as fs21 } from "fs";
11201
- import path16 from "path";
11610
+ import { promises as fs22 } from "fs";
11611
+ import path17 from "path";
11202
11612
  function humanTestLinks(verdicts) {
11203
11613
  const best = /* @__PURE__ */ new Map();
11204
11614
  for (const v of verdicts) {
11205
- const rank = VERDICT_RANK[v.verdict];
11206
- if (rank === void 0)
11615
+ const rank2 = VERDICT_RANK[v.verdict];
11616
+ if (rank2 === void 0)
11207
11617
  continue;
11208
11618
  for (const findingId of v.grounding_finding_ids) {
11209
11619
  const seen = best.get(findingId);
11210
- if (!seen || rank < seen.rank) {
11211
- best.set(findingId, { test: v.human_test_id, rank });
11620
+ if (!seen || rank2 < seen.rank) {
11621
+ best.set(findingId, { test: v.human_test_id, rank: rank2 });
11212
11622
  }
11213
11623
  }
11214
11624
  }
@@ -11221,7 +11631,7 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
11221
11631
  const dir = entityDirPath(projectRoot, "finding");
11222
11632
  let names2;
11223
11633
  try {
11224
- names2 = await fs21.readdir(dir);
11634
+ names2 = await fs22.readdir(dir);
11225
11635
  } catch {
11226
11636
  return [];
11227
11637
  }
@@ -11229,11 +11639,11 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
11229
11639
  for (const name of names2) {
11230
11640
  if (!name.endsWith(".md") || name.startsWith("."))
11231
11641
  continue;
11232
- const file = path16.join(dir, name);
11642
+ const file = path17.join(dir, name);
11233
11643
  let raw;
11234
11644
  let fm;
11235
11645
  try {
11236
- raw = await fs21.readFile(file, "utf8");
11646
+ raw = await fs22.readFile(file, "utf8");
11237
11647
  fm = parseFrontmatter(raw).frontmatter;
11238
11648
  } catch {
11239
11649
  continue;
@@ -11464,14 +11874,14 @@ var init_cloud_client = __esm({
11464
11874
  });
11465
11875
 
11466
11876
  // ../../packages/local-store/dist/last-pull.js
11467
- import { promises as fs22 } from "fs";
11468
- import path17 from "path";
11877
+ import { promises as fs23 } from "fs";
11878
+ import path18 from "path";
11469
11879
  function lastPullPath(projectRoot) {
11470
- return path17.join(cacheDir(projectRoot), "last-pull.json");
11880
+ return path18.join(cacheDir(projectRoot), "last-pull.json");
11471
11881
  }
11472
11882
  async function readLastPull(projectRoot) {
11473
11883
  try {
11474
- const raw = await fs22.readFile(lastPullPath(projectRoot), "utf8");
11884
+ const raw = await fs23.readFile(lastPullPath(projectRoot), "utf8");
11475
11885
  const parsed = JSON.parse(raw);
11476
11886
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11477
11887
  return parsed;
@@ -11577,7 +11987,7 @@ var init_types2 = __esm({
11577
11987
  });
11578
11988
 
11579
11989
  // ../../packages/local-store/dist/pull/frontmatter.js
11580
- import { randomUUID as randomUUID6 } from "crypto";
11990
+ import { randomUUID as randomUUID7 } from "crypto";
11581
11991
  function resolveBody(row, fallback) {
11582
11992
  if (typeof row.bodyMarkdown === "string") {
11583
11993
  return { text: row.bodyMarkdown, fromCloud: true };
@@ -11619,7 +12029,7 @@ function buildOutlineFrontmatter(row, slug, capabilityIdToSlug) {
11619
12029
  missingCaps.push(capId);
11620
12030
  }
11621
12031
  const humanTests = (row.humanTests ?? []).map((h) => ({
11622
- id: h.id ?? randomUUID6().slice(0, 8),
12032
+ id: h.id ?? randomUUID7().slice(0, 8),
11623
12033
  question: h.question
11624
12034
  }));
11625
12035
  const frontmatter = {
@@ -11786,7 +12196,7 @@ var init_frontmatter2 = __esm({
11786
12196
  });
11787
12197
 
11788
12198
  // ../../packages/local-store/dist/pull/write.js
11789
- import { promises as fs23 } from "fs";
12199
+ import { promises as fs24 } from "fs";
11790
12200
  async function loadLocalIndex(projectRoot, type2) {
11791
12201
  const byId = /* @__PURE__ */ new Map();
11792
12202
  const bySlug = /* @__PURE__ */ new Map();
@@ -11817,7 +12227,7 @@ async function classifyExisting(projectRoot, type2, cloudId, localSlug, targetSl
11817
12227
  if (localSlug !== targetSlug)
11818
12228
  return "untracked";
11819
12229
  const lastSynced = localIndex.lastPushById[cloudId];
11820
- const bytes = await fs23.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
12230
+ const bytes = await fs24.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
11821
12231
  return lastSynced && hashNormalizedContent(bytes) === lastSynced ? "fast_forward" : "diverged";
11822
12232
  }
11823
12233
  async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body, localIndex, mode, cloudVersion) {
@@ -11878,13 +12288,13 @@ async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body,
11878
12288
  }
11879
12289
  if (existingSlugForId && existingSlugForId !== slug) {
11880
12290
  try {
11881
- await fs23.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
12291
+ await fs24.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
11882
12292
  localIndex.bySlug.delete(existingSlugForId);
11883
12293
  } catch {
11884
12294
  }
11885
12295
  }
11886
12296
  await writeEntity(projectRoot, type2, frontmatter, text);
11887
- const bytes = await fs23.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
12297
+ const bytes = await fs24.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
11888
12298
  const hash = hashNormalizedContent(bytes);
11889
12299
  localIndex.byId.set(cloudId, slug);
11890
12300
  localIndex.bySlug.set(slug, cloudId);
@@ -11938,7 +12348,7 @@ var init_write = __esm({
11938
12348
  });
11939
12349
 
11940
12350
  // ../../packages/local-store/dist/pull/entities.js
11941
- import { promises as fs24 } from "fs";
12351
+ import { promises as fs25 } from "fs";
11942
12352
  function buildContextFrontmatter(payload) {
11943
12353
  const cloudCtx = payload.context ?? {};
11944
12354
  const glossary = (payload.glossary ?? []).map((g) => ({
@@ -11993,7 +12403,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
11993
12403
  const frontmatter = buildContextFrontmatter(payload);
11994
12404
  let hasLocal = false;
11995
12405
  try {
11996
- await fs24.access(contextPath(projectRoot));
12406
+ await fs25.access(contextPath(projectRoot));
11997
12407
  hasLocal = true;
11998
12408
  } catch {
11999
12409
  hasLocal = false;
@@ -12023,7 +12433,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
12023
12433
  frontmatter,
12024
12434
  notes_body: existingNotes
12025
12435
  });
12026
- const bytes = await fs24.readFile(contextPath(projectRoot), "utf8");
12436
+ const bytes = await fs25.readFile(contextPath(projectRoot), "utf8");
12027
12437
  const hash = hashNormalizedContent(bytes);
12028
12438
  await recordLastPull(projectRoot, "context:root", hash);
12029
12439
  await recordLastPush(projectRoot, "context:root", hash);
@@ -12538,14 +12948,14 @@ var init_repair = __esm({
12538
12948
  });
12539
12949
 
12540
12950
  // ../../packages/local-store/dist/session-state.js
12541
- import { randomUUID as randomUUID7 } from "crypto";
12951
+ import { randomUUID as randomUUID8 } from "crypto";
12542
12952
  import { closeSync as closeSync2, existsSync as existsSync2, fsyncSync as fsyncSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
12543
12953
  import os2 from "os";
12544
- import path18 from "path";
12954
+ import path19 from "path";
12545
12955
  function quarantineCorruptedSessionState(file, raw) {
12546
12956
  try {
12547
- const dir = path18.dirname(file);
12548
- const quarantine = path18.join(dir, `session-state.corrupted.${Date.now()}.json`);
12957
+ const dir = path19.dirname(file);
12958
+ const quarantine = path19.join(dir, `session-state.corrupted.${Date.now()}.json`);
12549
12959
  atomicWriteFileSync(quarantine, raw);
12550
12960
  } catch {
12551
12961
  }
@@ -12554,8 +12964,8 @@ function withSessionStateLock(filePath, fn) {
12554
12964
  const lockPath = `${filePath}.lock`;
12555
12965
  const deadline = Date.now() + 5e3;
12556
12966
  const staleMs = 15e3;
12557
- const token = randomUUID7();
12558
- mkdirSync2(path18.dirname(lockPath), { recursive: true });
12967
+ const token = randomUUID8();
12968
+ mkdirSync2(path19.dirname(lockPath), { recursive: true });
12559
12969
  const tryCreate = () => {
12560
12970
  try {
12561
12971
  const fd = openSync2(lockPath, "wx", 420);
@@ -12629,242 +13039,67 @@ function readSessionState(projectRoot) {
12629
13039
  const file = sessionStatePath(projectRoot);
12630
13040
  if (!existsSync2(file))
12631
13041
  return null;
12632
- try {
12633
- const raw = readFileSync(file, "utf-8");
12634
- if (!raw.trim())
12635
- return null;
12636
- const parsed = JSON.parse(raw);
12637
- if (parsed && typeof parsed === "object" && parsed.version === 1) {
12638
- return parsed;
12639
- }
12640
- quarantineCorruptedSessionState(file, raw);
12641
- return null;
12642
- } catch {
12643
- try {
12644
- const raw = readFileSync(file, "utf-8");
12645
- if (raw.trim()) {
12646
- quarantineCorruptedSessionState(file, raw);
12647
- }
12648
- } catch {
12649
- }
12650
- return null;
12651
- }
12652
- }
12653
- function updateSessionState(projectRoot, patch) {
12654
- const file = sessionStatePath(projectRoot);
12655
- const cacheDir2 = path18.dirname(file);
12656
- if (!existsSync2(cacheDir2)) {
12657
- mkdirSync2(cacheDir2, { recursive: true });
12658
- }
12659
- return withSessionStateLock(file, () => {
12660
- const existing = readSessionState(projectRoot);
12661
- const now = (/* @__PURE__ */ new Date()).toISOString();
12662
- const cleanRunPatch = patch.active_run ? Object.fromEntries(Object.entries(patch.active_run).filter(([_, v]) => v !== void 0)) : void 0;
12663
- const cleanFindingPatch = patch.active_finding ? Object.fromEntries(Object.entries(patch.active_finding).filter(([_, v]) => v !== void 0)) : void 0;
12664
- const next = {
12665
- version: 1,
12666
- updated_at: now,
12667
- active_scenario: patch.active_scenario ? { slug: patch.active_scenario.slug, updated_at: now } : existing?.active_scenario,
12668
- active_run: patch.active_run ? {
12669
- id: patch.active_run.id ?? existing?.active_run?.id ?? "",
12670
- scenario_slug: patch.active_run.scenario_slug ?? existing?.active_run?.scenario_slug ?? "",
12671
- status: patch.active_run.status ?? existing?.active_run?.status ?? "running",
12672
- ...existing?.active_run ?? {},
12673
- ...cleanRunPatch,
12674
- updated_at: now
12675
- } : existing?.active_run,
12676
- active_finding: patch.active_finding ? {
12677
- slug: patch.active_finding.slug,
12678
- ...existing?.active_finding ?? {},
12679
- ...cleanFindingPatch,
12680
- updated_at: now
12681
- } : existing?.active_finding,
12682
- active_persona: patch.active_persona ? { slug: patch.active_persona.slug, updated_at: now } : existing?.active_persona
12683
- };
12684
- atomicWriteFileSync(file, JSON.stringify(next, null, 2) + "\n");
12685
- return next;
12686
- });
12687
- }
12688
- var init_session_state = __esm({
12689
- "../../packages/local-store/dist/session-state.js"() {
12690
- "use strict";
12691
- init_atomic();
12692
- init_paths();
12693
- }
12694
- });
12695
-
12696
- // ../../packages/local-store/dist/directions.js
12697
- import { randomUUID as randomUUID8 } from "crypto";
12698
- import { promises as fs25 } from "fs";
12699
- import path19 from "path";
12700
- import { z as z26 } from "zod";
12701
- function directionsDir(projectRoot) {
12702
- return path19.join(iamuDir(projectRoot), DIRECTIONS_DIR);
12703
- }
12704
- function directionPath(projectRoot, slug) {
12705
- return path19.join(directionsDir(projectRoot), `${slug}.md`);
12706
- }
12707
- function renderBody(texts) {
12708
- return [
12709
- "## Problem",
12710
- "",
12711
- texts.problem.trim(),
12712
- "",
12713
- "## Change",
12714
- "",
12715
- texts.change.trim(),
12716
- "",
12717
- "## Leading practice",
12718
- "",
12719
- texts.leading_practice.trim(),
12720
- ""
12721
- ].join("\n");
12722
- }
12723
- function section(body, heading) {
12724
- const re = new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, "m");
12725
- return re.exec(body)?.[1]?.trim() ?? "";
12726
- }
12727
- function parseDirection(raw, file) {
12728
- const { frontmatter, body } = parseFrontmatter(raw);
12729
- return {
12730
- frontmatter: directionFrontmatterSchema.parse(frontmatter),
12731
- texts: {
12732
- problem: section(body, "Problem"),
12733
- change: section(body, "Change"),
12734
- leading_practice: section(body, "Leading practice")
12735
- },
12736
- path: file
12737
- };
12738
- }
12739
- async function readDirection(projectRoot, slug) {
12740
- const file = directionPath(projectRoot, slug);
12741
- let raw;
12742
- try {
12743
- raw = await fs25.readFile(file, "utf8");
12744
- } catch (err) {
12745
- if (err.code === "ENOENT")
12746
- return null;
12747
- throw err;
12748
- }
12749
- return parseDirection(raw, file);
12750
- }
12751
- async function listDirections(projectRoot) {
12752
- let names2;
12753
- try {
12754
- names2 = await fs25.readdir(directionsDir(projectRoot));
12755
- } catch (err) {
12756
- if (err.code === "ENOENT")
12757
- return [];
12758
- throw err;
12759
- }
12760
- const out = [];
12761
- for (const name of names2.sort()) {
12762
- if (!name.endsWith(".md"))
12763
- continue;
12764
- const file = path19.join(directionsDir(projectRoot), name);
13042
+ try {
13043
+ const raw = readFileSync(file, "utf-8");
13044
+ if (!raw.trim())
13045
+ return null;
13046
+ const parsed = JSON.parse(raw);
13047
+ if (parsed && typeof parsed === "object" && parsed.version === 1) {
13048
+ return parsed;
13049
+ }
13050
+ quarantineCorruptedSessionState(file, raw);
13051
+ return null;
13052
+ } catch {
12765
13053
  try {
12766
- out.push(parseDirection(await fs25.readFile(file, "utf8"), file));
13054
+ const raw = readFileSync(file, "utf-8");
13055
+ if (raw.trim()) {
13056
+ quarantineCorruptedSessionState(file, raw);
13057
+ }
12767
13058
  } catch {
12768
13059
  }
13060
+ return null;
12769
13061
  }
12770
- return out.sort((a, b) => b.frontmatter.updated_at.localeCompare(a.frontmatter.updated_at));
12771
- }
12772
- async function writeDirection(projectRoot, fm, texts) {
12773
- const file = directionPath(projectRoot, fm.slug);
12774
- const validated = directionFrontmatterSchema.parse(fm);
12775
- await atomicWriteFile(file, serializeFrontmatter(validated, renderBody(texts)));
12776
- return file;
12777
13062
  }
12778
- async function upsertDirection(projectRoot, proposal, source, advisedBy, now = (/* @__PURE__ */ new Date()).toISOString()) {
12779
- const existing = await readDirection(projectRoot, proposal.slug);
12780
- const texts = {
12781
- problem: proposal.problem,
12782
- change: proposal.change,
12783
- leading_practice: proposal.leading_practice
12784
- };
12785
- if (existing?.frontmatter.status === "rejected") {
12786
- return { outcome: "previously_rejected" };
13063
+ function updateSessionState(projectRoot, patch) {
13064
+ const file = sessionStatePath(projectRoot);
13065
+ const cacheDir2 = path19.dirname(file);
13066
+ if (!existsSync2(cacheDir2)) {
13067
+ mkdirSync2(cacheDir2, { recursive: true });
12787
13068
  }
12788
- const prev = existing?.frontmatter;
12789
- const fm = {
12790
- id: prev?.id ?? randomUUID8(),
12791
- slug: proposal.slug,
12792
- title: proposal.title,
12793
- status: prev?.status ?? "proposed",
12794
- size: proposal.size,
12795
- absorbs: union(prev?.absorbs ?? [], proposal.absorbs, (x) => x),
12796
- reach: union(prev?.reach ?? [], proposal.reach, (r) => `${r.scenario}:${r.effect}`),
12797
- verify: union(prev?.verify ?? [], proposal.verify, (x) => x),
12798
- builds_on: union(prev?.builds_on ?? [], proposal.builds_on, (r) => `${r.path}#${r.symbol}`),
12799
- seen_in_runs: union(prev?.seen_in_runs ?? [], [source], (x) => x),
12800
- advised_by: advisedBy,
12801
- created_at: prev?.created_at ?? now,
12802
- updated_at: now
12803
- };
12804
- const file = await writeDirection(projectRoot, fm, texts);
12805
- return {
12806
- outcome: existing ? "updated" : "created",
12807
- direction: { frontmatter: fm, texts, path: file }
12808
- };
12809
- }
12810
- async function setDirectionStatus(projectRoot, slug, status2, now = (/* @__PURE__ */ new Date()).toISOString()) {
12811
- const existing = await readDirection(projectRoot, slug);
12812
- if (!existing)
12813
- return null;
12814
- const fm = { ...existing.frontmatter, status: status2, updated_at: now };
12815
- await writeDirection(projectRoot, fm, existing.texts);
12816
- return { ...existing, frontmatter: fm };
13069
+ return withSessionStateLock(file, () => {
13070
+ const existing = readSessionState(projectRoot);
13071
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13072
+ const cleanRunPatch = patch.active_run ? Object.fromEntries(Object.entries(patch.active_run).filter(([_, v]) => v !== void 0)) : void 0;
13073
+ const cleanFindingPatch = patch.active_finding ? Object.fromEntries(Object.entries(patch.active_finding).filter(([_, v]) => v !== void 0)) : void 0;
13074
+ const next = {
13075
+ version: 1,
13076
+ updated_at: now,
13077
+ active_scenario: patch.active_scenario ? { slug: patch.active_scenario.slug, updated_at: now } : existing?.active_scenario,
13078
+ active_run: patch.active_run ? {
13079
+ id: patch.active_run.id ?? existing?.active_run?.id ?? "",
13080
+ scenario_slug: patch.active_run.scenario_slug ?? existing?.active_run?.scenario_slug ?? "",
13081
+ status: patch.active_run.status ?? existing?.active_run?.status ?? "running",
13082
+ ...existing?.active_run ?? {},
13083
+ ...cleanRunPatch,
13084
+ updated_at: now
13085
+ } : existing?.active_run,
13086
+ active_finding: patch.active_finding ? {
13087
+ slug: patch.active_finding.slug,
13088
+ ...existing?.active_finding ?? {},
13089
+ ...cleanFindingPatch,
13090
+ updated_at: now
13091
+ } : existing?.active_finding,
13092
+ active_persona: patch.active_persona ? { slug: patch.active_persona.slug, updated_at: now } : existing?.active_persona
13093
+ };
13094
+ atomicWriteFileSync(file, JSON.stringify(next, null, 2) + "\n");
13095
+ return next;
13096
+ });
12817
13097
  }
12818
- var DIRECTIONS_DIR, DIRECTION_STATUSES, directionStatusSchema, directionSizeSchema, directionReachSchema, codeRefSchema, directionFrontmatterSchema, union;
12819
- var init_directions = __esm({
12820
- "../../packages/local-store/dist/directions.js"() {
13098
+ var init_session_state = __esm({
13099
+ "../../packages/local-store/dist/session-state.js"() {
12821
13100
  "use strict";
12822
13101
  init_atomic();
12823
- init_markdown();
12824
13102
  init_paths();
12825
- DIRECTIONS_DIR = "directions";
12826
- DIRECTION_STATUSES = [
12827
- "proposed",
12828
- "accepted",
12829
- "done",
12830
- "rejected"
12831
- ];
12832
- directionStatusSchema = z26.enum(DIRECTION_STATUSES);
12833
- directionSizeSchema = z26.enum(["small", "medium", "large"]);
12834
- directionReachSchema = z26.object({
12835
- scenario: z26.string().min(1),
12836
- effect: z26.enum(["helps", "risk"]),
12837
- reason: z26.string().min(1)
12838
- });
12839
- codeRefSchema = z26.object({
12840
- path: z26.string().min(1),
12841
- symbol: z26.string().min(1)
12842
- });
12843
- directionFrontmatterSchema = z26.object({
12844
- id: z26.string().uuid(),
12845
- slug: z26.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
12846
- title: z26.string().min(1),
12847
- status: directionStatusSchema.default("proposed"),
12848
- size: directionSizeSchema,
12849
- /** Finding ids this direction would resolve. */
12850
- absorbs: z26.array(z26.string().min(1)).default([]),
12851
- /** Predicted effect on other scenarios — never evidence. */
12852
- reach: z26.array(directionReachSchema).default([]),
12853
- /** Scenarios to re-run once the direction lands. */
12854
- verify: z26.array(z26.string().min(1)).default([]),
12855
- builds_on: z26.array(codeRefSchema).default([]),
12856
- /** Runs (or `project:<timestamp>`) whose analysis proposed it. */
12857
- seen_in_runs: z26.array(z26.string().min(1)).default([]),
12858
- advised_by: z26.string().regex(/^advisor@\S+$/),
12859
- created_at: z26.string(),
12860
- updated_at: z26.string()
12861
- });
12862
- union = (a, b, key) => {
12863
- const seen = /* @__PURE__ */ new Map();
12864
- for (const x of [...a, ...b])
12865
- seen.set(key(x), x);
12866
- return [...seen.values()];
12867
- };
12868
13103
  }
12869
13104
  });
12870
13105
 
@@ -12993,7 +13228,7 @@ async function readContextSummary(projectRoot) {
12993
13228
  return null;
12994
13229
  }
12995
13230
  }
12996
- function pagePath(url) {
13231
+ function pagePath2(url) {
12997
13232
  try {
12998
13233
  return new URL(url).pathname || "/";
12999
13234
  } catch {
@@ -13017,7 +13252,7 @@ async function walkedCoverage(projectRoot) {
13017
13252
  const entry = out.get(slug) ?? { paths: /* @__PURE__ */ new Set(), runs: 0 };
13018
13253
  entry.runs++;
13019
13254
  for (const step of await readStepsForRun(projectRoot, name)) {
13020
- const p = typeof step.url === "string" ? pagePath(step.url) : null;
13255
+ const p = typeof step.url === "string" ? pagePath2(step.url) : null;
13021
13256
  if (p)
13022
13257
  entry.paths.add(p);
13023
13258
  }
@@ -13672,6 +13907,7 @@ var init_dist2 = __esm({
13672
13907
  init_revisit();
13673
13908
  init_explore_map();
13674
13909
  init_walkthrough_canonical2();
13910
+ init_report_markdown();
13675
13911
  init_journey_diff();
13676
13912
  init_finding_match();
13677
13913
  init_guidance2();
@@ -13780,15 +14016,15 @@ function resolveApiUrl(configured) {
13780
14016
  assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
13781
14017
  return url;
13782
14018
  }
13783
- function parseStore(raw, path43) {
14019
+ function parseStore(raw, path44) {
13784
14020
  let parsed;
13785
14021
  try {
13786
14022
  parsed = JSON.parse(raw);
13787
14023
  } catch (err) {
13788
- throw new CredentialsFileError(path43, "not valid JSON", err);
14024
+ throw new CredentialsFileError(path44, "not valid JSON", err);
13789
14025
  }
13790
14026
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13791
- throw new CredentialsFileError(path43, "expected a JSON object");
14027
+ throw new CredentialsFileError(path44, "expected a JSON object");
13792
14028
  }
13793
14029
  return parsed;
13794
14030
  }
@@ -13796,26 +14032,26 @@ function isNotFound(err) {
13796
14032
  return typeof err === "object" && err !== null && err.code === "ENOENT";
13797
14033
  }
13798
14034
  async function readCredentialsStore() {
13799
- const path43 = getCredentialsPath();
14035
+ const path44 = getCredentialsPath();
13800
14036
  let raw;
13801
14037
  try {
13802
- raw = await readFile(path43, "utf-8");
14038
+ raw = await readFile(path44, "utf-8");
13803
14039
  } catch (err) {
13804
14040
  if (isNotFound(err)) return {};
13805
14041
  warnUnreadableStoreOnce(err);
13806
14042
  throw err;
13807
14043
  }
13808
14044
  try {
13809
- return parseStore(raw, path43);
14045
+ return parseStore(raw, path44);
13810
14046
  } catch (err) {
13811
14047
  warnUnreadableStoreOnce(err);
13812
14048
  throw err;
13813
14049
  }
13814
14050
  }
13815
14051
  async function writeCredentialsStore(store) {
13816
- const path43 = getCredentialsPath();
13817
- await mkdir(dirname(path43), { recursive: true, mode: 448 });
13818
- await atomicWriteFile(path43, `${JSON.stringify(store, null, 2)}
14052
+ const path44 = getCredentialsPath();
14053
+ await mkdir(dirname(path44), { recursive: true, mode: 448 });
14054
+ await atomicWriteFile(path44, `${JSON.stringify(store, null, 2)}
13819
14055
  `, {
13820
14056
  mode: 384
13821
14057
  });
@@ -13835,16 +14071,16 @@ function resolveApiKey(apiUrl) {
13835
14071
  } catch {
13836
14072
  return null;
13837
14073
  }
13838
- const path43 = getCredentialsPath();
14074
+ const path44 = getCredentialsPath();
13839
14075
  let raw;
13840
14076
  try {
13841
- raw = readFileSync2(path43, "utf-8");
14077
+ raw = readFileSync2(path44, "utf-8");
13842
14078
  } catch (err) {
13843
14079
  if (!isNotFound(err)) warnUnreadableStoreOnce(err);
13844
14080
  return null;
13845
14081
  }
13846
14082
  try {
13847
- const entry = parseStore(raw, path43)[url];
14083
+ const entry = parseStore(raw, path44)[url];
13848
14084
  if (typeof entry !== "object" || entry === null) return null;
13849
14085
  const token = entry.token;
13850
14086
  return typeof token === "string" && token ? token : null;
@@ -13883,12 +14119,12 @@ var init_credentials = __esm({
13883
14119
  * @param reason - What is wrong with it.
13884
14120
  * @param cause - Underlying parse error, if any.
13885
14121
  */
13886
- constructor(path43, reason, cause) {
14122
+ constructor(path44, reason, cause) {
13887
14123
  super(
13888
- `Credentials file ${path43} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
14124
+ `Credentials file ${path44} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
13889
14125
  {
13890
14126
  cause,
13891
- suggestedAction: `Delete ${path43} and run \`iamu auth login\`, or set IAMU_TOKEN.`
14127
+ suggestedAction: `Delete ${path44} and run \`iamu auth login\`, or set IAMU_TOKEN.`
13892
14128
  }
13893
14129
  );
13894
14130
  }
@@ -14669,8 +14905,8 @@ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
14669
14905
  import { fileURLToPath as fileURLToPath2 } from "url";
14670
14906
  import chalk6 from "chalk";
14671
14907
  function getVersion() {
14672
- if ("0.4.0-beta.0") {
14673
- return "0.4.0-beta.0";
14908
+ if ("0.4.0-beta.1") {
14909
+ return "0.4.0-beta.1";
14674
14910
  }
14675
14911
  try {
14676
14912
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -14777,16 +15013,16 @@ async function needsWrite(filePath, content) {
14777
15013
  return true;
14778
15014
  }
14779
15015
  }
14780
- function isCliWrittenSkill(path43) {
15016
+ function isCliWrittenSkill(path44) {
14781
15017
  try {
14782
- return /^generatedBy:/m.test(readFileSync4(path43, "utf-8"));
15018
+ return /^generatedBy:/m.test(readFileSync4(path44, "utf-8"));
14783
15019
  } catch {
14784
15020
  return false;
14785
15021
  }
14786
15022
  }
14787
- function isCliWrittenPrompt(path43) {
15023
+ function isCliWrittenPrompt(path44) {
14788
15024
  try {
14789
- const content = readFileSync4(path43, "utf-8");
15025
+ const content = readFileSync4(path44, "utf-8");
14790
15026
  return content.includes("iamu") || /^generatedBy:/m.test(content);
14791
15027
  } catch {
14792
15028
  return false;
@@ -14798,8 +15034,8 @@ function planSkillFiles(hosts, projectRoot, version) {
14798
15034
  for (const host of hosts) {
14799
15035
  if (pluginProvidesSkills(host, projectRoot)) {
14800
15036
  for (const name of skillTemplates.keys()) {
14801
- const path43 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
14802
- if (isCliWrittenSkill(path43)) files.push({ host, path: path43, content: null });
15037
+ const path44 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
15038
+ if (isCliWrittenSkill(path44)) files.push({ host, path: path44, content: null });
14803
15039
  }
14804
15040
  } else if (host.supportsSkills) {
14805
15041
  const skillsDir = host.getSkillsDir(projectRoot);
@@ -16356,8 +16592,17 @@ function lead(text) {
16356
16592
  }
16357
16593
  function severityCell(severity) {
16358
16594
  const s = (severity ?? "\u2014").toUpperCase();
16359
- const cell = padRight(s, 8);
16360
- return severity === "critical" ? chalk12.red(cell) : severity === "major" ? chalk12.yellow(cell) : chalk12.dim(cell);
16595
+ const cell2 = padRight(s, 8);
16596
+ return severity === "critical" ? chalk12.red(cell2) : severity === "major" ? chalk12.yellow(cell2) : chalk12.dim(cell2);
16597
+ }
16598
+ function severityMix(r) {
16599
+ if (r.problems === 0) return chalk12.green("no problems");
16600
+ const { critical, major, minor } = r.bySeverity;
16601
+ return [
16602
+ critical > 0 ? chalk12.red(`${critical} critical`) : "",
16603
+ major > 0 ? chalk12.yellow(`${major} major`) : "",
16604
+ minor > 0 ? chalk12.dim(`${minor} minor`) : ""
16605
+ ].filter(Boolean).join(" ");
16361
16606
  }
16362
16607
  function deltaCell(r) {
16363
16608
  const parts = [
@@ -16375,6 +16620,13 @@ function renderRunView(v, width = 100) {
16375
16620
  );
16376
16621
  const meta = [shortDate(v.completedAt), `${v.steps} steps`, v.targetUrl].filter(Boolean).join(" \xB7 ");
16377
16622
  lines.push(` ${chalk12.dim(meta)}`);
16623
+ if (v.checks) {
16624
+ const parts = [
16625
+ v.checks.fixes > 0 ? `re-checked ${v.checks.fixes} fix${v.checks.fixes === 1 ? "" : "es"}: ${v.checks.fixed} confirmed, ${v.checks.cameBack} came back, ${v.checks.fixes - v.checks.fixed - v.checks.cameBack} not confirmed` : "",
16626
+ v.checks.directions.length > 0 ? `after ${v.checks.directions.length} direction${v.checks.directions.length === 1 ? "" : "s"} landed` : ""
16627
+ ].filter(Boolean);
16628
+ lines.push(` ${chalk12.dim(`This run ${parts.join(", ")}`)}`);
16629
+ }
16378
16630
  lines.push("");
16379
16631
  const color = verdictColor(v.verdict.label);
16380
16632
  const question = v.verdict.question ?? "Would they come back?";
@@ -16402,12 +16654,21 @@ function renderRunView(v, width = 100) {
16402
16654
  );
16403
16655
  for (const item of s.items) {
16404
16656
  const mark = SINCE_MARKS[item.outcome];
16405
- lines.push(` ${mark} ${truncate(item.title, w - 20)}`);
16657
+ const qualifiers = [
16658
+ item.outcome === "persists" ? item.shift : "",
16659
+ item.outcome === "persists" && item.fixedBefore ? "fix did not hold" : ""
16660
+ ].filter(Boolean);
16661
+ const held = qualifiers.length > 0 ? (item.fixedBefore ? chalk12.red : chalk12.dim)(
16662
+ ` (${qualifiers.join("; ")})`
16663
+ ) : "";
16664
+ lines.push(
16665
+ ` ${mark} ${truncate(item.title, w - 18 - stripAnsi(held).length)}${held}`
16666
+ );
16406
16667
  }
16407
16668
  }
16408
16669
  lines.push("");
16409
16670
  const problems = [...v.findings].sort(
16410
- (a, b) => SEVERITY_ORDER.indexOf(a.severity ?? "") - SEVERITY_ORDER.indexOf(b.severity ?? "") || TYPE_ORDER.indexOf(a.type) - TYPE_ORDER.indexOf(b.type)
16671
+ (a, b) => SEVERITY_ORDER2.indexOf(a.severity ?? "") - SEVERITY_ORDER2.indexOf(b.severity ?? "") || TYPE_ORDER2.indexOf(a.type) - TYPE_ORDER2.indexOf(b.type)
16411
16672
  );
16412
16673
  const strengthNote = v.strengths.length > 0 ? `, ${v.strengths.length} strength${v.strengths.length === 1 ? "" : "s"}` : "";
16413
16674
  lines.push(
@@ -16418,15 +16679,23 @@ function renderRunView(v, width = 100) {
16418
16679
  24,
16419
16680
  Math.max(0, ...problems.map((f) => (f.where ?? "").length))
16420
16681
  );
16421
- const titleWidth = Math.max(24, w - 4 - 3 - 9 - 10 - whereWidth - 2 - 9);
16682
+ const tagWidth = problems.some((f) => f.persists) ? 9 : 0;
16683
+ const titleWidth = Math.max(
16684
+ 24,
16685
+ w - 4 - 3 - 9 - 10 - whereWidth - 2 - tagWidth
16686
+ );
16422
16687
  problems.forEach((f, i) => {
16423
16688
  const where = chalk12.dim(
16424
16689
  padRight(truncate(f.where ?? "", whereWidth), whereWidth)
16425
16690
  );
16426
16691
  const tag = f.persists ? chalk12.red(" persists") : "";
16692
+ const titleLines = wrap(f.title, titleWidth, "");
16427
16693
  lines.push(
16428
- ` ${padRight(String(i + 1), 3)}${severityCell(f.severity)} ${padRight(chalk12.dim(f.type), 9)} ${padRight(truncate(f.title, titleWidth), titleWidth)} ${where}${tag}`
16694
+ ` ${padRight(String(i + 1), 3)}${severityCell(f.severity)} ${padRight(chalk12.dim(f.type), 9)} ${padRight(titleLines[0] ?? "", titleWidth)} ${where}${tag}`
16429
16695
  );
16696
+ for (const more of titleLines.slice(1)) {
16697
+ lines.push(`${" ".repeat(26)}${more}`);
16698
+ }
16430
16699
  if (f.recommendation && (f.severity === "critical" || f.severity === "major")) {
16431
16700
  const fix = wrap(`\u2192 ${lead(f.recommendation)}`, w - 30, " ".repeat(26));
16432
16701
  if (fix.length > 2) {
@@ -16452,8 +16721,13 @@ function renderRunView(v, width = 100) {
16452
16721
  );
16453
16722
  }
16454
16723
  }
16455
- if (v.observations.length > 0) {
16724
+ if (v.observations.length > 0 || v.setupNotes.length > 0) {
16456
16725
  lines.push("", ` ${chalk12.bold("Also worth knowing")}`);
16726
+ for (const n of v.setupNotes) {
16727
+ lines.push(
16728
+ ` ${chalk12.dim("\xB7")} ${chalk12.dim("Test setup:")} ${truncate(n, w - 22)}`
16729
+ );
16730
+ }
16457
16731
  for (const o of v.observations) {
16458
16732
  const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
16459
16733
  lines.push(
@@ -16488,7 +16762,7 @@ function renderOverview(v, width = 100) {
16488
16762
  }
16489
16763
  for (const r of shown) {
16490
16764
  lines.push(
16491
- ` ${padRight(chalk12.dim(shortDate(r.completedAt)), 13)} ${verdictColor(r.label)(padRight(r.label, 8))} ${padRight(chalk12.dim(`${r.problems} problem${r.problems === 1 ? "" : "s"}`), 11)} ${deltaCell(r)}`
16765
+ ` ${padRight(chalk12.dim(shortDate(r.completedAt)), 13)} ${verdictColor(r.label)(padRight(r.label, 8))} ${padRight(severityMix(r), 24)} ${deltaCell(r)}`
16492
16766
  );
16493
16767
  }
16494
16768
  if (s.latestAnswer) {
@@ -16517,7 +16791,7 @@ function renderOverview(v, width = 100) {
16517
16791
  );
16518
16792
  return lines.map((l) => l.trimEnd());
16519
16793
  }
16520
- var SINCE_MARKS, SEVERITY_ORDER, TYPE_ORDER;
16794
+ var SINCE_MARKS, SEVERITY_ORDER2, TYPE_ORDER2;
16521
16795
  var init_report_view = __esm({
16522
16796
  "src/ui/report-view.ts"() {
16523
16797
  "use strict";
@@ -16528,8 +16802,8 @@ var init_report_view = __esm({
16528
16802
  not_seen: chalk12.dim(padRight("\u25CB not seen", 12)),
16529
16803
  uncertain: chalk12.dim(padRight("? unclear", 12))
16530
16804
  };
16531
- SEVERITY_ORDER = ["critical", "major", "minor", "observation"];
16532
- TYPE_ORDER = ["incorrect", "hinders", "missing", "irrelevant"];
16805
+ SEVERITY_ORDER2 = ["critical", "major", "minor", "observation"];
16806
+ TYPE_ORDER2 = ["incorrect", "hinders", "missing", "irrelevant"];
16533
16807
  }
16534
16808
  });
16535
16809
 
@@ -16918,9 +17192,7 @@ ${formatIssues(fileResult.error)}`,
16918
17192
  console.log(
16919
17193
  ` ${chalk14.dim("verdicts:")} ${path23.relative(projectRoot, verdictsPath)}`
16920
17194
  );
16921
- console.log(
16922
- ` ${chalk14.dim("report:")} ${path23.relative(projectRoot, reportPath)}`
16923
- );
17195
+ console.log(` ${chalk14.dim("report:")} ${reportPath}`);
16924
17196
  printCloudRunInfo(cloudInfo);
16925
17197
  await promptViewResults(projectRoot, { ...opts, runId });
16926
17198
  }
@@ -16976,7 +17248,10 @@ function sinceItems(v) {
16976
17248
  return groups.flatMap(
16977
17249
  ([outcome, items]) => (items ?? []).map((i) => ({
16978
17250
  title: i.title ?? i.canonical_id ?? "untitled",
16979
- outcome
17251
+ outcome,
17252
+ note: i.note ?? null,
17253
+ fixedBefore: Boolean(i.fixed_before),
17254
+ shift: outcome === "persists" ? severityShift(i.severity_before, i.severity_now) : ""
16980
17255
  }))
16981
17256
  );
16982
17257
  }
@@ -17005,11 +17280,8 @@ async function readCompletedRuns(projectRoot, runIds, scenario) {
17005
17280
  return out;
17006
17281
  }
17007
17282
  function verdictLabel(c) {
17008
- const judged = (c?.human_tests ?? []).map((t) => t.verdict).filter((v) => v === "yes" || v === "no" || v === "partial");
17009
- if (judged.length === 0) return "PENDING";
17010
- if (judged.every((v) => v === "yes")) return "YES";
17011
- if (judged.every((v) => v === "no")) return "NO";
17012
- return "PARTIAL";
17283
+ const v = runVerdict(c);
17284
+ return v ? v.toUpperCase() : "PENDING";
17013
17285
  }
17014
17286
  function testLabel(verdict) {
17015
17287
  return verdict === "yes" ? "YES" : verdict === "no" ? "NO" : verdict === "partial" ? "PARTIAL" : "PENDING";
@@ -17063,6 +17335,17 @@ async function openDirections(projectRoot) {
17063
17335
  (d) => d.frontmatter.status === "proposed" || d.frontmatter.status === "accepted"
17064
17336
  );
17065
17337
  }
17338
+ function checksOf(c) {
17339
+ const checks = c?.checks;
17340
+ if (!c || !checks) return null;
17341
+ const fixes = checkOutcomes(c);
17342
+ return {
17343
+ fixes: fixes.length,
17344
+ fixed: fixes.filter((f) => f.outcome === "fixed").length,
17345
+ cameBack: fixes.filter((f) => f.outcome === "came_back").length,
17346
+ directions: checks.directions
17347
+ };
17348
+ }
17066
17349
  async function buildRunView(projectRoot, run) {
17067
17350
  const c = run.canonical;
17068
17351
  const slugs = slugsOf(run);
@@ -17078,12 +17361,13 @@ async function buildRunView(projectRoot, run) {
17078
17361
  const v = verificationOf(c);
17079
17362
  const persisting = new Set(v?.persists.map((p) => p.canonical_id) ?? []);
17080
17363
  const added = new Set(v?.new_findings.map((n) => n.id) ?? []);
17364
+ const stepUrl2 = new Map((c?.steps ?? []).map((st) => [st.id, st.url]));
17081
17365
  const findings2 = [];
17082
17366
  const strengths = [];
17083
17367
  for (const f of c?.findings ?? []) {
17084
17368
  const entity = byId.get(f.id);
17085
17369
  const fm = entity?.frontmatter;
17086
- const title = fm?.title ?? firstSentence(f.ui_fact ?? fm?.ui_fact ?? f.id);
17370
+ const title = f.title ?? fm?.title ?? firstSentence(f.ui_fact ?? fm?.ui_fact ?? f.id);
17087
17371
  if (f.type === "helps") {
17088
17372
  strengths.push(title);
17089
17373
  continue;
@@ -17097,7 +17381,7 @@ async function buildRunView(projectRoot, run) {
17097
17381
  recommendation: f.recommendation || null,
17098
17382
  isNew: added.has(f.id),
17099
17383
  where: whereOf(
17100
- fm?.page_url,
17384
+ f.page_url ?? fm?.page_url ?? stepUrl2.get(f.linked_step_ids?.[0] ?? ""),
17101
17385
  fm?.walkthrough_step_id ?? f.linked_step_ids?.[0]
17102
17386
  ),
17103
17387
  persists: Boolean(f.canonical_id && persisting.has(f.canonical_id))
@@ -17140,7 +17424,7 @@ async function buildRunView(projectRoot, run) {
17140
17424
  answer: overall?.answer ?? null
17141
17425
  },
17142
17426
  tests: tests.map((t) => ({
17143
- question: t.question,
17427
+ question: stripAnswerHint(t.question),
17144
17428
  label: testLabel(t.verdict)
17145
17429
  })),
17146
17430
  since: v ? {
@@ -17159,9 +17443,17 @@ async function buildRunView(projectRoot, run) {
17159
17443
  absorbs: d.absorbs?.length ?? 0,
17160
17444
  verify: d.verify?.length ?? 0
17161
17445
  })),
17162
- observations: analysis?.observations ?? []
17446
+ observations: analysis?.observations ?? [],
17447
+ checks: checksOf(c),
17448
+ setupNotes: c?.setup_notes ?? []
17163
17449
  };
17164
17450
  }
17451
+ function severityCounts(c) {
17452
+ const problems = (c?.findings ?? []).filter(isProblem);
17453
+ const critical = problems.filter((f) => f.severity === "critical").length;
17454
+ const major = problems.filter((f) => f.severity === "major").length;
17455
+ return { critical, major, minor: problems.length - critical - major };
17456
+ }
17165
17457
  async function buildOverview(projectRoot, runs) {
17166
17458
  const groups = /* @__PURE__ */ new Map();
17167
17459
  for (const r of runs) {
@@ -17188,6 +17480,7 @@ async function buildOverview(projectRoot, runs) {
17188
17480
  completedAt: r.manifest.completed_at ?? null,
17189
17481
  label: verdictLabel(r.canonical),
17190
17482
  problems: (r.canonical?.findings ?? []).filter(isProblem).length,
17483
+ bySeverity: severityCounts(r.canonical),
17191
17484
  resolved: v?.resolved.length ?? 0,
17192
17485
  persists: v?.persists.length ?? 0,
17193
17486
  added: v?.new_findings.filter(isProblem).length ?? 0
@@ -17239,6 +17532,10 @@ __export(html_report_exports, {
17239
17532
  });
17240
17533
  import { promises as fs31 } from "fs";
17241
17534
  import path25 from "path";
17535
+ function firstSentenceOf(text) {
17536
+ if (!text) return null;
17537
+ return /^(.+?[.!?])(\s|$)/.exec(text.trim())?.[1] ?? text.trim();
17538
+ }
17242
17539
  function esc(str2) {
17243
17540
  if (!str2) return "";
17244
17541
  return str2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -17345,6 +17642,70 @@ function renderStepScreenshotHtml(s) {
17345
17642
  </div>
17346
17643
  `;
17347
17644
  }
17645
+ function sortProblems(list2) {
17646
+ const r = (v, order) => {
17647
+ const i = order.indexOf(v ?? "");
17648
+ return i === -1 ? order.length : i;
17649
+ };
17650
+ return [...list2].sort(
17651
+ (a, b) => r(a.severity, SEVERITY_RANK) - r(b.severity, SEVERITY_RANK) || r(a.type, TYPE_RANK) - r(b.type, TYPE_RANK)
17652
+ );
17653
+ }
17654
+ function inlineMarkdown(text) {
17655
+ return esc(text).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\n+/g, "<br>");
17656
+ }
17657
+ function persistsLabel(i) {
17658
+ const qualifiers = [
17659
+ severityShift(i.severity_before, i.severity_now),
17660
+ i.fixed_before ? "fix did not hold" : ""
17661
+ ].filter(Boolean);
17662
+ return qualifiers.length > 0 ? `Persists (${qualifiers.join("; ")})` : "Persists";
17663
+ }
17664
+ function renderChecks2(fixes, directions) {
17665
+ const items = fixes.map(
17666
+ (f) => `<li style="margin-bottom:0.25rem"><strong>${CHECK_LABEL[f.outcome]}:</strong> ${esc(f.title)}${f.fix ? `. Fix: ${esc(f.fix)}` : ""}${f.reached ? "" : " <em>(not reached)</em>"}</li>`
17667
+ ).join("");
17668
+ const landed = directions.length > 0 ? `<p style="font-size:13px">Directions completed since the last run: ${directions.map((d) => `<strong>${esc(d)}</strong>`).join(", ")}.</p>` : "";
17669
+ return `
17670
+ <section>
17671
+ <h2>What this run checks</h2>
17672
+ <div class="card">
17673
+ ${items ? `<p style="font-size:13px">${fixes.length} fix${fixes.length === 1 ? " was" : "es were"} waiting for a re-check.</p><ul style="padding-left:1.25rem;font-size:13px;line-height:1.6">${items}</ul>` : ""}
17674
+ ${landed}
17675
+ </div>
17676
+ </section>`;
17677
+ }
17678
+ function renderSinceTable(v) {
17679
+ const steps = (ids) => ids && ids.length > 0 ? ` (step${ids.length === 1 ? "" : "s"} ${ids.map((id) => /\d+/.exec(id)?.[0] ?? id).join(", ")})` : "";
17680
+ const td = 'style="padding:6px 8px;border-top:1px solid #E5E7EB;vertical-align:top"';
17681
+ const row = (i, now, note) => `<tr><td ${td}>${esc(i.title)}</td><td ${td}><strong>${esc(now)}</strong></td><td ${td}>${esc(note) || "\u2014"}</td></tr>`;
17682
+ const rows = [
17683
+ ...v.resolved.map(
17684
+ (i) => row(i, "Resolved", `${i.note ?? ""}${steps(i.step_ids)}`)
17685
+ ),
17686
+ ...v.persists.map(
17687
+ (i) => row(i, persistsLabel(i), `${i.note ?? ""}${steps(i.step_ids)}`)
17688
+ ),
17689
+ ...(v.notObserved ?? []).map(
17690
+ (i) => row(
17691
+ i,
17692
+ "Not seen",
17693
+ `The walk did not pass this way${i.fixed_before ? " (fixed, awaiting a re-check)" : ""}. Not evidence of a fix.`
17694
+ )
17695
+ )
17696
+ ];
17697
+ if (rows.length === 0) return "";
17698
+ return `
17699
+ <section>
17700
+ <h2>Since the last run</h2>
17701
+ <div class="card">
17702
+ <table style="width:100%;border-collapse:collapse;font-size:13px;line-height:1.5">
17703
+ <thead><tr><th style="text-align:left;padding:6px 8px">Earlier finding</th><th style="text-align:left;padding:6px 8px">Now</th><th style="text-align:left;padding:6px 8px">Notes</th></tr></thead>
17704
+ <tbody>${rows.join("")}</tbody>
17705
+ </table>
17706
+ </div>
17707
+ </section>`;
17708
+ }
17348
17709
  function generateHtmlReport(data) {
17349
17710
  const {
17350
17711
  scenarioName,
@@ -17353,6 +17714,7 @@ function generateHtmlReport(data) {
17353
17714
  steps,
17354
17715
  findings: findings2,
17355
17716
  verdicts,
17717
+ overall,
17356
17718
  verification,
17357
17719
  whatWorked,
17358
17720
  openQuestions,
@@ -17363,16 +17725,17 @@ function generateHtmlReport(data) {
17363
17725
  runNumber
17364
17726
  } = data;
17365
17727
  const dateStr = formatDate(completedAt ?? startedAt);
17366
- const criticalCount = findings2.filter(
17367
- (f) => f.severity === "critical" || f.type === "incorrect"
17728
+ const problems = sortProblems(findings2.filter((f) => f.type !== "helps"));
17729
+ const strengths = findings2.filter((f) => f.type === "helps");
17730
+ const criticalCount = problems.filter(
17731
+ (f) => f.severity === "critical"
17368
17732
  ).length;
17369
- const majorCount = findings2.filter(
17370
- (f) => f.severity === "major" || f.severity === "important" || f.type === "hinders" || f.type === "missing"
17733
+ const majorCount = problems.filter((f) => f.severity === "major").length;
17734
+ const minorCount = problems.filter(
17735
+ (f) => f.severity !== "critical" && f.severity !== "major"
17371
17736
  ).length;
17372
- const minorCount = findings2.filter(
17373
- (f) => f.severity === "minor" || f.type === "friction" || !f.severity && f.type !== "helps"
17374
- ).length;
17375
- const helpsCount = findings2.filter((f) => f.type === "helps").length;
17737
+ const helpsCount = strengths.length;
17738
+ const newProblems = verification ? verification.newFindings.filter((n) => n.type !== "helps").length : 0;
17376
17739
  return `<!DOCTYPE html>
17377
17740
  <html lang="en">
17378
17741
  <head>
@@ -17847,16 +18210,27 @@ function generateHtmlReport(data) {
17847
18210
  <div class="verification-notice">
17848
18211
  Since last run: <strong>${verification.resolved.length} resolved</strong> \xB7
17849
18212
  <strong>${verification.persists.length} persists</strong> \xB7
17850
- <strong>${verification.newFindings.length} new</strong>
18213
+ <strong>${newProblems} new</strong>
17851
18214
  </div>` : ""}
17852
18215
  </div>
17853
18216
 
17854
- <!-- The Human Test Scorecard -->
17855
- ${verdicts && verdicts.length > 0 ? `
18217
+ ${data.checks ? renderChecks2(data.checks.fixes, data.checks.directions) : ""}
18218
+
18219
+ <!-- The Human Test: the run's outcome, first -->
18220
+ ${overall || verdicts && verdicts.length > 0 ? `
17856
18221
  <section>
17857
- <h2>The Human Test Checkpoints</h2>
18222
+ <h2>The Human Test</h2>
17858
18223
  <div class="card">
17859
- ${verdicts.map(
18224
+ ${overall ? `
18225
+ <div class="verdict-item">
18226
+ <div class="verdict-header">
18227
+ <span style="font-weight:600;font-size:15px">${esc(overall.question)}</span>
18228
+ <span class="badge badge-${esc(overall.label)}">${esc(overall.label)}</span>
18229
+ </div>
18230
+ ${overall.answer ? `<div class="prose-voice" style="margin-top:0.35rem;font-weight:500">${esc(overall.answer)}</div>` : ""}
18231
+ ${overall.reasoning ? `<div class="prose-voice" style="margin-top:0.35rem">${esc(overall.reasoning)}</div>` : ""}
18232
+ </div>` : ""}
18233
+ ${(verdicts ?? []).map(
17860
18234
  (v) => `
17861
18235
  <div class="verdict-item">
17862
18236
  <div class="verdict-header">
@@ -17871,14 +18245,46 @@ function generateHtmlReport(data) {
17871
18245
  </section>
17872
18246
  ` : ""}
17873
18247
 
18248
+ ${verification ? renderSinceTable(verification) : ""}
18249
+
18250
+ <!-- Findings, worst first -->
18251
+ ${problems.length > 0 ? `
18252
+ <section>
18253
+ <h2>Findings (${problems.length})</h2>
18254
+ ${problems.map(
18255
+ (f, i) => `
18256
+ <div class="finding-item">
18257
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.35rem 0.75rem">
18258
+ <span class="finding-title">${i + 1}. ${esc(f.title)}${f.isNew ? ' <span class="badge">new</span>' : f.persists ? ' <span class="badge">persists</span>' : ""}</span>
18259
+ <span class="badge badge-${esc(f.severity ?? f.type)}">${esc(f.severity ?? f.type)}</span>
18260
+ </div>
18261
+ <div class="finding-meta">${esc([f.type, f.where].filter(Boolean).join(" \xB7 "))}</div>
18262
+ ${f.uiFact ? `<div class="finding-meta">${esc(f.uiFact)}</div>` : ""}
18263
+ ${f.consequence ? `<blockquote class="finding-meta prose-voice">${esc(f.consequence)}${persona?.name ? ` \u2014 ${esc(persona.name)}` : ""}</blockquote>` : ""}
18264
+ ${f.recommendation ? `<div class="finding-meta finding-rec"><strong>Recommendation:</strong> ${inlineMarkdown(f.recommendation)}</div>` : ""}
18265
+ </div>
18266
+ `
18267
+ ).join("")}
18268
+ </section>
18269
+ ` : ""}
18270
+
17874
18271
  <!-- What Worked & Open Questions -->
17875
- ${whatWorked && whatWorked.length > 0 || openQuestions && openQuestions.length > 0 ? `
18272
+ ${strengths.length > 0 || whatWorked && whatWorked.length > 0 || openQuestions && openQuestions.length > 0 || data.setupNotes && data.setupNotes.length > 0 ? `
17876
18273
  <section style="display:flex;gap:1.25rem;flex-wrap:wrap">
17877
- ${whatWorked && whatWorked.length > 0 ? `
18274
+ ${strengths.length > 0 || whatWorked && whatWorked.length > 0 ? `
17878
18275
  <div class="card" style="flex:1;min-width:260px">
17879
18276
  <div class="card-label">What Worked</div>
17880
18277
  <ul style="padding-left:1.25rem;font-size:13px;line-height:1.6">
17881
- ${whatWorked.map((w) => `<li style="margin-bottom:0.25rem">${esc(w)}</li>`).join("")}
18278
+ ${strengths.map((f) => `<li style="margin-bottom:0.25rem"><strong>${esc(f.title)}.</strong> ${esc(f.uiFact)}</li>`).join("")}
18279
+ ${(whatWorked ?? []).map((w) => `<li style="margin-bottom:0.25rem">${esc(w)}</li>`).join("")}
18280
+ </ul>
18281
+ </div>
18282
+ ` : ""}
18283
+ ${data.setupNotes && data.setupNotes.length > 0 ? `
18284
+ <div class="card" style="flex:1;min-width:260px">
18285
+ <div class="card-label">Test Setup Notes</div>
18286
+ <ul style="padding-left:1.25rem;font-size:13px;line-height:1.6">
18287
+ ${data.setupNotes.map((n) => `<li style="margin-bottom:0.25rem">${esc(n)}</li>`).join("")}
17882
18288
  </ul>
17883
18289
  </div>
17884
18290
  ` : ""}
@@ -17893,26 +18299,6 @@ function generateHtmlReport(data) {
17893
18299
  </section>
17894
18300
  ` : ""}
17895
18301
 
17896
- <!-- Findings Overview -->
17897
- ${findings2.length > 0 ? `
17898
- <section>
17899
- <h2>Findings (${findings2.length})</h2>
17900
- ${findings2.map(
17901
- (f) => `
17902
- <div class="finding-item">
17903
- <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.35rem 0.75rem">
17904
- <span class="finding-title">${esc(f.title)}</span>
17905
- <span class="badge badge-${esc(f.severity ?? f.type)}">${esc(f.severity ?? f.type)}</span>
17906
- </div>
17907
- ${f.uiFact ? `<div class="finding-meta"><strong>Observed:</strong> ${esc(f.uiFact)}</div>` : ""}
17908
- ${f.consequence ? `<div class="finding-meta"><strong>Consequence:</strong> ${esc(f.consequence)}</div>` : ""}
17909
- ${f.recommendation ? `<div class="finding-meta finding-rec"><strong>Recommendation:</strong> ${esc(f.recommendation)}</div>` : ""}
17910
- </div>
17911
- `
17912
- ).join("")}
17913
- </section>
17914
- ` : ""}
17915
-
17916
18302
  <!-- Step Timeline with Connected Gutter -->
17917
18303
  ${steps.length > 0 ? `
17918
18304
  <section>
@@ -17980,7 +18366,6 @@ async function buildReportDataFromRun(projectRoot, runId) {
17980
18366
  scenarioSlug
17981
18367
  );
17982
18368
  if (record) {
17983
- scenarioName = record.frontmatter.slug || scenarioSlug;
17984
18369
  scenarioNarrative = record.body?.trim() || null;
17985
18370
  }
17986
18371
  } catch {
@@ -18036,47 +18421,82 @@ async function buildReportDataFromRun(projectRoot, runId) {
18036
18421
  screenshotUrl
18037
18422
  });
18038
18423
  }
18424
+ const names2 = canonical?.scenario;
18425
+ if (names2?.outline_title) {
18426
+ scenarioName = `${names2.persona_name ?? personaData?.name ?? names2.persona_slug} \xB7 ${names2.outline_title}`;
18427
+ }
18428
+ const verification = canonical?.verification;
18429
+ const newIds = new Set((verification?.new_findings ?? []).map((n) => n.id));
18430
+ const persistingIds = new Set(
18431
+ (verification?.persists ?? []).map(
18432
+ (p) => p.canonical_id
18433
+ )
18434
+ );
18039
18435
  const findings2 = (canonical?.findings ?? []).map((f) => {
18040
- const rawF = f;
18041
- const title = typeof rawF.title === "string" && rawF.title.trim().length > 0 ? rawF.title : f.ui_fact || f.id;
18436
+ const step = rawSteps.find((st) => st.id === f.linked_step_ids[0]);
18437
+ let page = null;
18438
+ const url = f.page_url ?? step?.url;
18439
+ if (url) {
18440
+ try {
18441
+ page = new URL(url).pathname;
18442
+ } catch {
18443
+ page = url;
18444
+ }
18445
+ }
18042
18446
  return {
18043
18447
  id: f.id,
18044
- title,
18448
+ title: f.title?.trim() || firstSentenceOf(f.ui_fact) || f.id,
18045
18449
  type: f.type,
18046
18450
  severity: f.severity,
18047
18451
  uiFact: f.ui_fact,
18048
18452
  consequence: f.consequence,
18049
- recommendation: f.recommendation
18453
+ recommendation: f.recommendation,
18454
+ where: [page, step ? `step ${step.step_number}` : null].filter(Boolean).join(" \xB7 ") || null,
18455
+ isNew: newIds.has(f.id),
18456
+ persists: Boolean(f.canonical_id && persistingIds.has(f.canonical_id))
18050
18457
  };
18051
18458
  });
18052
18459
  const verdicts = (canonical?.human_tests ?? []).map(
18053
18460
  (ht) => ({
18054
- question: ht.question,
18461
+ question: stripAnswerHint(ht.question),
18055
18462
  rating: ht.verdict || "pending",
18056
18463
  reasoning: typeof ht.reasoning === "string" ? ht.reasoning : void 0
18057
18464
  })
18058
18465
  );
18059
- const verification = canonical?.verification;
18060
18466
  const verificationData = verification ? {
18061
18467
  resolved: verification.resolved ?? [],
18062
18468
  persists: verification.persists ?? [],
18469
+ notObserved: verification.not_observed ?? [],
18063
18470
  newFindings: verification.new_findings ?? []
18064
18471
  } : null;
18472
+ const verdict = runVerdict(canonical);
18473
+ const overall = canonical?.human_test_overall ? {
18474
+ question: canonical.human_test_overall.question,
18475
+ label: verdict ?? "pending",
18476
+ answer: canonical.human_test_overall.answer,
18477
+ reasoning: canonical.human_test_overall.reasoning
18478
+ } : null;
18065
18479
  return {
18066
18480
  scenarioName,
18067
18481
  scenarioNarrative,
18068
18482
  persona: personaData,
18069
18483
  walkthroughId: runId,
18070
- runNumber: manifest.run_number ?? null,
18484
+ runNumber: manifest.run_number ?? canonical?.run_number ?? null,
18071
18485
  startedAt: manifest.started_at ?? canonical?.provenance?.started_at ?? null,
18072
18486
  completedAt: manifest.completed_at ?? canonical?.provenance?.completed_at ?? null,
18073
18487
  targetUrl: manifest.target_url ?? canonical?.target?.url ?? null,
18074
18488
  steps,
18075
18489
  findings: findings2,
18076
18490
  verdicts,
18491
+ overall,
18077
18492
  verification: verificationData,
18078
18493
  whatWorked: canonical?.what_worked,
18079
- openQuestions: canonical?.open_questions
18494
+ openQuestions: canonical?.open_questions,
18495
+ setupNotes: canonical?.setup_notes,
18496
+ checks: (() => {
18497
+ const raw = canonical?.checks;
18498
+ return canonical && raw ? { fixes: checkOutcomes(canonical), directions: raw.directions } : null;
18499
+ })()
18080
18500
  };
18081
18501
  }
18082
18502
  async function createLocalHtmlReport(projectRoot, runId, destination) {
@@ -18086,11 +18506,19 @@ async function createLocalHtmlReport(projectRoot, runId, destination) {
18086
18506
  await atomicWriteFile(targetPath, html);
18087
18507
  return { filePath: targetPath, html };
18088
18508
  }
18509
+ var SEVERITY_RANK, TYPE_RANK, CHECK_LABEL;
18089
18510
  var init_html_report = __esm({
18090
18511
  "src/ui/html-report.ts"() {
18091
18512
  "use strict";
18092
18513
  init_dist2();
18093
18514
  init_evaluate();
18515
+ SEVERITY_RANK = ["critical", "major", "minor", "observation"];
18516
+ TYPE_RANK = ["incorrect", "hinders", "missing", "irrelevant"];
18517
+ CHECK_LABEL = {
18518
+ fixed: "Confirmed fixed",
18519
+ came_back: "Came back",
18520
+ unconfirmed: "Not confirmed"
18521
+ };
18094
18522
  }
18095
18523
  });
18096
18524
 
@@ -18099,6 +18527,7 @@ var report_exports = {};
18099
18527
  __export(report_exports, {
18100
18528
  report: () => report
18101
18529
  });
18530
+ import path26 from "path";
18102
18531
  import chalk15 from "chalk";
18103
18532
  async function report(scenario, opts) {
18104
18533
  const ctx = await resolveProject(opts);
@@ -18219,6 +18648,9 @@ function jsonRows(projectRoot, runs) {
18219
18648
  human_tests: r.canonical?.human_tests ?? [],
18220
18649
  human_test_overall: r.canonical?.human_test_overall,
18221
18650
  summary: await buildRunView(projectRoot, r),
18651
+ // Absolute, so a terminal can open them (relative paths are not links).
18652
+ run_dir: runDir(projectRoot, r.runId),
18653
+ report_path: path26.join(runDir(projectRoot, r.runId), "report.md"),
18222
18654
  ...cloudUrl ? { cloud_url: cloudUrl } : {}
18223
18655
  };
18224
18656
  })
@@ -18243,7 +18675,7 @@ __export(open_exports, {
18243
18675
  openDashboard: () => openDashboard
18244
18676
  });
18245
18677
  import { existsSync as existsSync8, promises as fs32 } from "fs";
18246
- import path26 from "path";
18678
+ import path27 from "path";
18247
18679
  import { isCancel as isCancel3, select as select3 } from "@clack/prompts";
18248
18680
  import chalk16 from "chalk";
18249
18681
  async function openDashboard(runId, opts) {
@@ -18339,7 +18771,7 @@ async function openLocalReport(projectRoot, runId, opts) {
18339
18771
  for (const rid of [...runs].reverse()) {
18340
18772
  try {
18341
18773
  const manifestRaw = await fs32.readFile(
18342
- path26.join(runDir(projectRoot, rid), "walkthrough.json"),
18774
+ path27.join(runDir(projectRoot, rid), "walkthrough.json"),
18343
18775
  "utf8"
18344
18776
  );
18345
18777
  const manifest = JSON.parse(manifestRaw);
@@ -20114,7 +20546,7 @@ var init_status2 = __esm({
20114
20546
 
20115
20547
  // src/commands/browse-actions.ts
20116
20548
  import { existsSync as existsSync11 } from "fs";
20117
- import path27 from "path";
20549
+ import path28 from "path";
20118
20550
  function byRole(page, role, name, opts) {
20119
20551
  const loc = page.getByRole(role, { name, exact: false });
20120
20552
  return opts.nth !== void 0 ? loc.nth(Number(opts.nth)) : loc.first();
@@ -20305,7 +20737,7 @@ async function fillIndexedElement(page, index, text, submit) {
20305
20737
  await fillOrSelect(loc.first(), text, submit);
20306
20738
  }
20307
20739
  async function setFilesOnElement(el, filePath) {
20308
- const resolved = path27.resolve(filePath);
20740
+ const resolved = path28.resolve(filePath);
20309
20741
  if (!existsSync11(resolved)) {
20310
20742
  throw new Error(`Upload file does not exist: "${resolved}"`);
20311
20743
  }
@@ -20645,7 +21077,7 @@ var init_browse_driver = __esm({
20645
21077
 
20646
21078
  // src/commands/step-numbering.ts
20647
21079
  import { open, readdir as readdir2, unlink } from "fs/promises";
20648
- import path28 from "path";
21080
+ import path29 from "path";
20649
21081
  function stepFileName(stepNumber2) {
20650
21082
  return `${String(stepNumber2).padStart(3, "0")}.json`;
20651
21083
  }
@@ -20667,7 +21099,7 @@ async function reserveStepFile(stepsDir, limit) {
20667
21099
  if (candidate > limit.maxSteps) {
20668
21100
  throw new Error(stepLimitMessage(limit.maxSteps, limit.runId));
20669
21101
  }
20670
- const stepPath = path28.join(stepsDir, stepFileName(candidate));
21102
+ const stepPath = path29.join(stepsDir, stepFileName(candidate));
20671
21103
  try {
20672
21104
  const handle = await open(stepPath, "wx");
20673
21105
  await handle.close();
@@ -20699,7 +21131,7 @@ __export(step_exports, {
20699
21131
  stepCommand: () => stepCommand
20700
21132
  });
20701
21133
  import { copyFile, mkdir as mkdir5, stat as stat2 } from "fs/promises";
20702
- import path29 from "path";
21134
+ import path30 from "path";
20703
21135
  import chalk22 from "chalk";
20704
21136
  function parseMaxSteps(val) {
20705
21137
  if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
@@ -20718,8 +21150,8 @@ async function recordStep(input) {
20718
21150
  if (!runDirPath) {
20719
21151
  throw new Error(`Run directory not found for run "${input.runId}"`);
20720
21152
  }
20721
- const stepsDir = path29.join(runDirPath, "steps");
20722
- const screenshotsDir = path29.join(runDirPath, "screenshots");
21153
+ const stepsDir = path30.join(runDirPath, "steps");
21154
+ const screenshotsDir = path30.join(runDirPath, "screenshots");
20723
21155
  await mkdir5(stepsDir, { recursive: true });
20724
21156
  await mkdir5(screenshotsDir, { recursive: true });
20725
21157
  const shotSource = input.shot ? await existingShot(input.shot) : null;
@@ -20733,11 +21165,11 @@ async function recordStep(input) {
20733
21165
  let finalScreenshotPath = null;
20734
21166
  if (shotSource) {
20735
21167
  const destName = `step-${stepNumber2}.png`;
20736
- finalScreenshotPath = path29.join(screenshotsDir, destName);
20737
- if (path29.resolve(finalScreenshotPath) !== shotSource) {
21168
+ finalScreenshotPath = path30.join(screenshotsDir, destName);
21169
+ if (path30.resolve(finalScreenshotPath) !== shotSource) {
20738
21170
  await copyFile(shotSource, finalScreenshotPath);
20739
21171
  }
20740
- screenshotRef = path29.join("screenshots", destName);
21172
+ screenshotRef = path30.join("screenshots", destName);
20741
21173
  }
20742
21174
  const parsedStep = runStepSchema.parse({
20743
21175
  step_number: stepNumber2,
@@ -20776,7 +21208,7 @@ async function recordStep(input) {
20776
21208
  }
20777
21209
  }
20778
21210
  async function existingShot(shot) {
20779
- const resolved = path29.resolve(shot);
21211
+ const resolved = path30.resolve(shot);
20780
21212
  const isFile = await stat2(resolved).then((s) => s.isFile()).catch(() => false);
20781
21213
  if (!isFile) {
20782
21214
  throw new Error(
@@ -20830,7 +21262,7 @@ async function stepCommand(runArg, opts) {
20830
21262
  {
20831
21263
  step_number: res.step.step_number,
20832
21264
  cancelled: true,
20833
- path: path29.relative(projectRoot, res.stepPath),
21265
+ path: path30.relative(projectRoot, res.stepPath),
20834
21266
  action: res.step.action,
20835
21267
  note: res.step.note,
20836
21268
  url: res.step.url,
@@ -20858,7 +21290,7 @@ async function stepCommand(runArg, opts) {
20858
21290
  JSON.stringify(
20859
21291
  {
20860
21292
  step_number: res.step.step_number,
20861
- path: path29.relative(projectRoot, res.stepPath),
21293
+ path: path30.relative(projectRoot, res.stepPath),
20862
21294
  action: res.step.action,
20863
21295
  note: res.step.note,
20864
21296
  url: res.step.url,
@@ -20887,7 +21319,7 @@ async function stepCommand(runArg, opts) {
20887
21319
  console.log(` ${chalk22.dim("shot:")} ${res.step.screenshot}`);
20888
21320
  }
20889
21321
  console.log(
20890
- ` ${chalk22.dim("file:")} ${path29.relative(projectRoot, res.stepPath)}`
21322
+ ` ${chalk22.dim("file:")} ${path30.relative(projectRoot, res.stepPath)}`
20891
21323
  );
20892
21324
  console.log();
20893
21325
  } else {
@@ -20948,21 +21380,21 @@ import {
20948
21380
  } from "fs";
20949
21381
  import { rm as rm2 } from "fs/promises";
20950
21382
  import { homedir as homedir3 } from "os";
20951
- import path30 from "path";
21383
+ import path31 from "path";
20952
21384
  import chalk23 from "chalk";
20953
21385
  function stateDir() {
20954
- return path30.join(
20955
- process.env.IAMU_HOME ?? path30.join(homedir3(), ".iamu"),
21386
+ return path31.join(
21387
+ process.env.IAMU_HOME ?? path31.join(homedir3(), ".iamu"),
20956
21388
  "browse"
20957
21389
  );
20958
21390
  }
20959
21391
  function profileDir() {
20960
- return path30.join(stateDir(), "profile");
21392
+ return path31.join(stateDir(), "profile");
20961
21393
  }
20962
21394
  function readBrowseState() {
20963
21395
  try {
20964
21396
  return JSON.parse(
20965
- readFileSync6(path30.join(stateDir(), "state.json"), "utf8")
21397
+ readFileSync6(path31.join(stateDir(), "state.json"), "utf8")
20966
21398
  );
20967
21399
  } catch {
20968
21400
  return null;
@@ -20970,7 +21402,7 @@ function readBrowseState() {
20970
21402
  }
20971
21403
  function writeState(state) {
20972
21404
  mkdirSync3(stateDir(), { recursive: true });
20973
- writeFileSync3(path30.join(stateDir(), "state.json"), JSON.stringify(state));
21405
+ writeFileSync3(path31.join(stateDir(), "state.json"), JSON.stringify(state));
20974
21406
  }
20975
21407
  function fail(message, opts) {
20976
21408
  if (opts?.json) {
@@ -21034,7 +21466,7 @@ async function requestedViewport(opts) {
21034
21466
  let manifest;
21035
21467
  try {
21036
21468
  manifest = JSON.parse(
21037
- readFileSync6(path30.join(dir, "walkthrough.json"), "utf8")
21469
+ readFileSync6(path31.join(dir, "walkthrough.json"), "utf8")
21038
21470
  );
21039
21471
  } catch {
21040
21472
  fail(
@@ -21140,7 +21572,7 @@ async function stop(opts, driver) {
21140
21572
  await driver.closeBrowser(state.port);
21141
21573
  if (state.pid) await reapBrowser(state.pid, profileDir(), driver);
21142
21574
  try {
21143
- unlinkSync3(path30.join(stateDir(), "state.json"));
21575
+ unlinkSync3(path31.join(stateDir(), "state.json"));
21144
21576
  } catch {
21145
21577
  }
21146
21578
  report2(opts, { stopped: true }, [` ${chalk23.green("\u2713")} Browser stopped`]);
@@ -21149,8 +21581,8 @@ function computePageFingerprint(url, bodySnippet) {
21149
21581
  return createHash4("sha256").update(`${url}|${bodySnippet.trim().replace(/\s+/g, " ")}`).digest("hex").slice(0, 16);
21150
21582
  }
21151
21583
  function checkAndRecordStagnation(projectRoot, runId, fingerprint, action) {
21152
- const cacheDir2 = path30.join(projectRoot, "iamu", ".cache");
21153
- const cacheFile = path30.join(cacheDir2, `stagnation-${runId}.json`);
21584
+ const cacheDir2 = path31.join(projectRoot, "iamu", ".cache");
21585
+ const cacheFile = path31.join(cacheDir2, `stagnation-${runId}.json`);
21154
21586
  let previous = null;
21155
21587
  try {
21156
21588
  if (existsSync12(cacheFile)) {
@@ -21320,8 +21752,8 @@ async function withPage(opts, driver, fn, target) {
21320
21752
  let contentSnippet;
21321
21753
  if (opts.shot) {
21322
21754
  await waitForVisualStability(page);
21323
- screenshot = path30.resolve(opts.shot);
21324
- mkdirSync3(path30.dirname(screenshot), { recursive: true });
21755
+ screenshot = path31.resolve(opts.shot);
21756
+ mkdirSync3(path31.dirname(screenshot), { recursive: true });
21325
21757
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
21326
21758
  if (typeof page.evaluate === "function") {
21327
21759
  contentSnippet = await page.evaluate(() => {
@@ -21486,7 +21918,7 @@ function printStep(opts, record, projectRoot, stagnationWarning, challengeWarnin
21486
21918
  if (opts.json) {
21487
21919
  const doc = {
21488
21920
  step_number: step.step_number,
21489
- path: path30.relative(projectRoot, record.stepPath),
21921
+ path: path31.relative(projectRoot, record.stepPath),
21490
21922
  action: step.action,
21491
21923
  note: step.note,
21492
21924
  url: step.url,
@@ -21589,10 +22021,10 @@ async function browseStep(args, opts, driver) {
21589
22021
  const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
21590
22022
  if (!runDirPath) return;
21591
22023
  const maxSteps = parseMaxSteps(opts.maxSteps);
21592
- if (await peekNextStepNumber(path30.join(runDirPath, "steps")) > maxSteps) {
22024
+ if (await peekNextStepNumber(path31.join(runDirPath, "steps")) > maxSteps) {
21593
22025
  return fail(stepLimitMessage(maxSteps, opts.run), opts);
21594
22026
  }
21595
- const pendingShot = path30.join(
22027
+ const pendingShot = path31.join(
21596
22028
  runDirPath,
21597
22029
  "screenshots",
21598
22030
  `.pending-${randomUUID9()}.png`
@@ -22444,10 +22876,10 @@ __export(preflight_exports, {
22444
22876
  });
22445
22877
  import { spawnSync } from "child_process";
22446
22878
  import { existsSync as existsSync14, promises as fs34 } from "fs";
22447
- import path31 from "path";
22879
+ import path32 from "path";
22448
22880
  import chalk25 from "chalk";
22449
22881
  async function detectPreflightCommand(targetDir) {
22450
- const pkgPath = path31.join(targetDir, "package.json");
22882
+ const pkgPath = path32.join(targetDir, "package.json");
22451
22883
  if (existsSync14(pkgPath)) {
22452
22884
  try {
22453
22885
  const raw = await fs34.readFile(pkgPath, "utf8");
@@ -22462,12 +22894,12 @@ async function detectPreflightCommand(targetDir) {
22462
22894
  } catch {
22463
22895
  }
22464
22896
  }
22465
- const tsconfigPath = path31.join(targetDir, "tsconfig.json");
22897
+ const tsconfigPath = path32.join(targetDir, "tsconfig.json");
22466
22898
  if (existsSync14(tsconfigPath)) {
22467
22899
  return { command: "npx", args: ["--no-install", "tsc", "--noEmit"] };
22468
22900
  }
22469
- const biomePath = path31.join(targetDir, "biome.json");
22470
- const biomecPath = path31.join(targetDir, "biome.jsonc");
22901
+ const biomePath = path32.join(targetDir, "biome.json");
22902
+ const biomecPath = path32.join(targetDir, "biome.jsonc");
22471
22903
  if (existsSync14(biomePath) || existsSync14(biomecPath)) {
22472
22904
  return { command: "npx", args: ["--no-install", "biome", "check"] };
22473
22905
  }
@@ -22504,7 +22936,7 @@ async function runPreflightCheck(targetDir) {
22504
22936
  };
22505
22937
  }
22506
22938
  async function preflight(opts = {}) {
22507
- const targetDir = opts.cwd ? path31.resolve(opts.cwd) : process.cwd();
22939
+ const targetDir = opts.cwd ? path32.resolve(opts.cwd) : process.cwd();
22508
22940
  const result = await runPreflightCheck(targetDir);
22509
22941
  if (result.ok) {
22510
22942
  if (opts.json) {
@@ -22576,7 +23008,7 @@ import {
22576
23008
  readlinkSync,
22577
23009
  realpathSync as realpathSync2
22578
23010
  } from "fs";
22579
- import path32 from "path";
23011
+ import path33 from "path";
22580
23012
  function git(args, cwd) {
22581
23013
  return execFileSync("git", args, {
22582
23014
  cwd,
@@ -22629,8 +23061,8 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
22629
23061
  const top = git(["rev-parse", "--show-toplevel"], projectRoot).toString("utf8").trim();
22630
23062
  if (!top) return void 0;
22631
23063
  const realTop = realpathSync2(top);
22632
- const realIamu = path32.join(realpathSync2(projectRoot), iamuDirName);
22633
- const iamuRel = path32.relative(realTop, realIamu).split(path32.sep).join("/");
23064
+ const realIamu = path33.join(realpathSync2(projectRoot), iamuDirName);
23065
+ const iamuRel = path33.relative(realTop, realIamu).split(path33.sep).join("/");
22634
23066
  const pathspec = iamuRel.startsWith("..") ? [":(top)"] : [":(top)", `:(top,exclude,literal)${iamuRel}`];
22635
23067
  const status2 = git(
22636
23068
  [
@@ -22660,7 +23092,7 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
22660
23092
  );
22661
23093
  for (const file of sorted.slice(0, MAX_UNTRACKED_HASHED)) {
22662
23094
  const rel = file.replace(/\/$/, "");
22663
- hash.update(`${file}\0${hashFile(path32.join(realTop, rel))}
23095
+ hash.update(`${file}\0${hashFile(path33.join(realTop, rel))}
22664
23096
  `);
22665
23097
  }
22666
23098
  if (sorted.length > MAX_UNTRACKED_HASHED) {
@@ -23266,12 +23698,12 @@ __export(validate_exports, {
23266
23698
  });
23267
23699
  import { randomUUID as randomUUID10 } from "crypto";
23268
23700
  import { promises as fs35 } from "fs";
23269
- import path33 from "path";
23701
+ import path34 from "path";
23270
23702
  import chalk27 from "chalk";
23271
23703
  function inferTypeFromPath(filePath, projectRoot) {
23272
- const rel = path33.relative(path33.join(projectRoot, "iamu"), filePath);
23273
- if (rel.startsWith("..") || path33.isAbsolute(rel)) return null;
23274
- const segments = rel.split(path33.sep);
23704
+ const rel = path34.relative(path34.join(projectRoot, "iamu"), filePath);
23705
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) return null;
23706
+ const segments = rel.split(path34.sep);
23275
23707
  if (segments.length < 2) return null;
23276
23708
  return TYPE_BY_ALIAS.get(segments[0]) ?? null;
23277
23709
  }
@@ -23301,7 +23733,7 @@ function validateFile(filePath, raw, projectRoot) {
23301
23733
  );
23302
23734
  return result;
23303
23735
  }
23304
- if (path33.basename(filePath) === "context.yml") {
23736
+ if (path34.basename(filePath) === "context.yml") {
23305
23737
  applyZodValidation(contextFrontmatterSchema, frontmatter, result);
23306
23738
  const warnings = [];
23307
23739
  const hasNotes = Boolean(
@@ -23370,7 +23802,7 @@ async function buildRepairLookups(projectRoot) {
23370
23802
  }
23371
23803
  }
23372
23804
  const runScenario = /* @__PURE__ */ new Map();
23373
- const runsDir2 = path33.join(projectRoot, "iamu", ".runs");
23805
+ const runsDir2 = path34.join(projectRoot, "iamu", ".runs");
23374
23806
  let runIds = [];
23375
23807
  try {
23376
23808
  runIds = await fs35.readdir(runsDir2);
@@ -23380,7 +23812,7 @@ async function buildRepairLookups(projectRoot) {
23380
23812
  try {
23381
23813
  const manifest = JSON.parse(
23382
23814
  await fs35.readFile(
23383
- path33.join(runsDir2, runId, "walkthrough.json"),
23815
+ path34.join(runsDir2, runId, "walkthrough.json"),
23384
23816
  "utf8"
23385
23817
  )
23386
23818
  );
@@ -23411,7 +23843,7 @@ async function fixFindings(projectRoot, results, dryRun) {
23411
23843
  parsed.frontmatter,
23412
23844
  {
23413
23845
  ...lookups,
23414
- fileSlug: path33.basename(result.path, ".md"),
23846
+ fileSlug: path34.basename(result.path, ".md"),
23415
23847
  newId: randomUUID10,
23416
23848
  body: parsed.body
23417
23849
  }
@@ -23450,7 +23882,7 @@ async function validate(filePath, opts) {
23450
23882
  if (process.exitCode === 1) return;
23451
23883
  const results = [];
23452
23884
  if (filePath) {
23453
- const abs = path33.isAbsolute(filePath) ? filePath : path33.resolve(process.cwd(), filePath);
23885
+ const abs = path34.isAbsolute(filePath) ? filePath : path34.resolve(process.cwd(), filePath);
23454
23886
  try {
23455
23887
  const raw = await fs35.readFile(abs, "utf8");
23456
23888
  results.push(validateFile(abs, raw, projectRoot));
@@ -23466,7 +23898,7 @@ async function validate(filePath, opts) {
23466
23898
  return;
23467
23899
  }
23468
23900
  } else if (opts.run) {
23469
- const findingsDir = path33.join(projectRoot, "iamu", ENTITY_DIRS.finding);
23901
+ const findingsDir = path34.join(projectRoot, "iamu", ENTITY_DIRS.finding);
23470
23902
  let names2 = [];
23471
23903
  try {
23472
23904
  names2 = await fs35.readdir(findingsDir);
@@ -23474,7 +23906,7 @@ async function validate(filePath, opts) {
23474
23906
  }
23475
23907
  for (const name of names2) {
23476
23908
  if (!name.endsWith(".md")) continue;
23477
- const abs = path33.join(findingsDir, name);
23909
+ const abs = path34.join(findingsDir, name);
23478
23910
  try {
23479
23911
  const raw = await fs35.readFile(abs, "utf8");
23480
23912
  const { frontmatter } = parseFrontmatter(raw);
@@ -23483,7 +23915,7 @@ async function validate(filePath, opts) {
23483
23915
  }
23484
23916
  } catch {
23485
23917
  results.push({
23486
- path: path33.relative(projectRoot, abs),
23918
+ path: path34.relative(projectRoot, abs),
23487
23919
  valid: false,
23488
23920
  errors: ["Cannot read file or parse frontmatter"]
23489
23921
  });
@@ -23516,7 +23948,7 @@ async function validate(filePath, opts) {
23516
23948
  } else {
23517
23949
  const typesToCheck = typeFilter ? ENTITY_TYPES.filter((t) => typeFilter.has(t)) : ENTITY_TYPES;
23518
23950
  for (const type2 of typesToCheck) {
23519
- const iamuTypeDir = path33.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
23951
+ const iamuTypeDir = path34.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
23520
23952
  let names2;
23521
23953
  try {
23522
23954
  names2 = await fs35.readdir(iamuTypeDir);
@@ -23525,20 +23957,20 @@ async function validate(filePath, opts) {
23525
23957
  }
23526
23958
  for (const name of names2) {
23527
23959
  if (!name.endsWith(".md")) continue;
23528
- const abs = path33.join(iamuTypeDir, name);
23960
+ const abs = path34.join(iamuTypeDir, name);
23529
23961
  try {
23530
23962
  const raw = await fs35.readFile(abs, "utf8");
23531
23963
  results.push(validateFile(abs, raw, projectRoot));
23532
23964
  } catch {
23533
23965
  results.push({
23534
- path: path33.relative(projectRoot, abs),
23966
+ path: path34.relative(projectRoot, abs),
23535
23967
  valid: false,
23536
23968
  errors: ["Cannot read file"]
23537
23969
  });
23538
23970
  }
23539
23971
  }
23540
23972
  }
23541
- const ctxPath = path33.join(projectRoot, "iamu", "context.yml");
23973
+ const ctxPath = path34.join(projectRoot, "iamu", "context.yml");
23542
23974
  try {
23543
23975
  const raw = await fs35.readFile(ctxPath, "utf8");
23544
23976
  results.push(validateFile(ctxPath, raw, projectRoot));
@@ -23563,19 +23995,19 @@ async function validate(filePath, opts) {
23563
23995
  valid,
23564
23996
  errors: results.filter((r) => !r.valid).flatMap(
23565
23997
  (r) => r.errors.map((e) => ({
23566
- path: path33.relative(projectRoot, r.path),
23998
+ path: path34.relative(projectRoot, r.path),
23567
23999
  message: e
23568
24000
  }))
23569
24001
  ),
23570
24002
  warnings: results.filter((r) => r.warnings && r.warnings.length > 0).flatMap(
23571
24003
  (r) => (r.warnings ?? []).map((w) => ({
23572
- path: path33.relative(projectRoot, r.path),
24004
+ path: path34.relative(projectRoot, r.path),
23573
24005
  message: w
23574
24006
  }))
23575
24007
  ),
23576
24008
  ...opts.fix ? {
23577
24009
  repaired: repaired.map((r) => ({
23578
- path: path33.relative(projectRoot, r.path),
24010
+ path: path34.relative(projectRoot, r.path),
23579
24011
  changes: r.repairs,
23580
24012
  valid: r.valid
23581
24013
  }))
@@ -23597,7 +24029,7 @@ async function validate(filePath, opts) {
23597
24029
  }
23598
24030
  let hasErrors = false;
23599
24031
  for (const r of results) {
23600
- const relPath = path33.relative(projectRoot, r.path);
24032
+ const relPath = path34.relative(projectRoot, r.path);
23601
24033
  if (r.valid) {
23602
24034
  if (r.warnings && r.warnings.length > 0) {
23603
24035
  console.log(` ${symbols.warning} ` + chalk27.yellow(relPath));
@@ -23998,7 +24430,7 @@ __export(set_entity_exports, {
23998
24430
  setEntity: () => setEntity
23999
24431
  });
24000
24432
  import { promises as fs36 } from "fs";
24001
- import path34 from "path";
24433
+ import path35 from "path";
24002
24434
  import chalk30 from "chalk";
24003
24435
  import { ZodError as ZodError2 } from "zod";
24004
24436
  function given(opts, key) {
@@ -24139,7 +24571,7 @@ async function setEntity(type2, slug, opts) {
24139
24571
  }
24140
24572
  raw = Buffer.concat(chunks).toString("utf8");
24141
24573
  } else {
24142
- raw = await fs36.readFile(path34.resolve(projectRoot, opts.file), "utf8");
24574
+ raw = await fs36.readFile(path35.resolve(projectRoot, opts.file), "utf8");
24143
24575
  }
24144
24576
  } catch (err) {
24145
24577
  commandError(
@@ -24567,7 +24999,7 @@ __export(edit_entity_exports, {
24567
24999
  });
24568
25000
  import { spawn as spawn2 } from "child_process";
24569
25001
  import { promises as fs37 } from "fs";
24570
- import path35 from "path";
25002
+ import path36 from "path";
24571
25003
  import chalk31 from "chalk";
24572
25004
  async function editEntity(type2, slug, opts = {}) {
24573
25005
  const ctx = await resolveProject(opts);
@@ -24581,19 +25013,19 @@ async function editEntity(type2, slug, opts = {}) {
24581
25013
  return;
24582
25014
  }
24583
25015
  const dir = ENTITY_DIRS[type2];
24584
- const filePath = path35.join(projectRoot, "iamu", dir, `${slug}.md`);
25016
+ const filePath = path36.join(projectRoot, "iamu", dir, `${slug}.md`);
24585
25017
  try {
24586
25018
  await fs37.access(filePath);
24587
25019
  } catch {
24588
25020
  commandError(
24589
- `${type2} "${slug}" not found at ${path35.relative(projectRoot, filePath)}.`,
25021
+ `${type2} "${slug}" not found at ${path36.relative(projectRoot, filePath)}.`,
24590
25022
  { ...opts, code: "entity_not_found", fix: `iamu list ${type2}s` }
24591
25023
  );
24592
25024
  return;
24593
25025
  }
24594
25026
  if (!process.stdin.isTTY || process.env.CI) {
24595
25027
  commandError(
24596
- `Cannot open editor in a non-interactive environment. Edit the file directly: ${path35.relative(projectRoot, filePath)}`,
25028
+ `Cannot open editor in a non-interactive environment. Edit the file directly: ${path36.relative(projectRoot, filePath)}`,
24597
25029
  {
24598
25030
  ...opts,
24599
25031
  code: "non_interactive_environment",
@@ -24668,7 +25100,7 @@ async function editEntity(type2, slug, opts = {}) {
24668
25100
  console.log();
24669
25101
  console.log(
24670
25102
  chalk31.dim(
24671
- ` Fix the file and run: iamu validate ${path35.relative(projectRoot, filePath)}`
25103
+ ` Fix the file and run: iamu validate ${path36.relative(projectRoot, filePath)}`
24672
25104
  )
24673
25105
  );
24674
25106
  console.log();
@@ -24689,7 +25121,7 @@ async function editEntity(type2, slug, opts = {}) {
24689
25121
  {
24690
25122
  ...opts,
24691
25123
  code: "validation_error",
24692
- fix: `iamu validate ${path35.relative(projectRoot, filePath)}`
25124
+ fix: `iamu validate ${path36.relative(projectRoot, filePath)}`
24693
25125
  }
24694
25126
  );
24695
25127
  }
@@ -24826,7 +25258,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
24826
25258
  const byKey = /* @__PURE__ */ new Map();
24827
25259
  const byPlaceView = /* @__PURE__ */ new Map();
24828
25260
  const signatureOf = (s) => exploreSignature(s.headings, s.title);
24829
- function addSurface(state, place, view, path43, depth) {
25261
+ function addSurface(state, place, view, path44, depth) {
24830
25262
  const key = exploreSurfaceKey(place, view);
24831
25263
  const surface = {
24832
25264
  key,
@@ -24838,7 +25270,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
24838
25270
  headings: state.headings,
24839
25271
  fields: state.fields,
24840
25272
  depth,
24841
- path: path43,
25273
+ path: path44,
24842
25274
  actions: []
24843
25275
  };
24844
25276
  surfaces.push(surface);
@@ -24856,9 +25288,9 @@ ${surface.signature}`, key);
24856
25288
  });
24857
25289
  await settle(page, settleMs);
24858
25290
  }
24859
- async function replay(path43) {
24860
- if (!("goto" in (path43[0] ?? {}))) await goto(opts.startUrl);
24861
- for (const step of path43) {
25291
+ async function replay(path44) {
25292
+ if (!("goto" in (path44[0] ?? {}))) await goto(opts.startUrl);
25293
+ for (const step of path44) {
24862
25294
  if ("goto" in step) {
24863
25295
  await goto(step.goto);
24864
25296
  } else {
@@ -24982,8 +25414,8 @@ ${sig}`, key);
24982
25414
  if (stats.surfaces >= budget.max_surfaces) {
24983
25415
  stats.truncated = true;
24984
25416
  } else {
24985
- const path43 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
24986
- addSurface(after, place, view, path43, surface.depth + 1);
25417
+ const path44 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
25418
+ addSurface(after, place, view, path44, surface.depth + 1);
24987
25419
  }
24988
25420
  }
24989
25421
  }
@@ -25183,7 +25615,7 @@ __export(explore_exports, {
25183
25615
  exploreList: () => exploreList,
25184
25616
  exploreRun: () => exploreRun
25185
25617
  });
25186
- import path36 from "path";
25618
+ import path37 from "path";
25187
25619
  import chalk32 from "chalk";
25188
25620
  function fail2(message, json2) {
25189
25621
  if (json2) console.error(JSON.stringify({ error: message }));
@@ -25317,7 +25749,7 @@ async function exploreRun(url, opts) {
25317
25749
  surfaces: result.surfaces
25318
25750
  };
25319
25751
  const file = await writeExploreMap(ctx.projectRoot, map2);
25320
- const rel = path36.relative(process.cwd(), file);
25752
+ const rel = path37.relative(process.cwd(), file);
25321
25753
  if (opts.json) {
25322
25754
  console.log(JSON.stringify({ id: map2.id, path: rel, stats: map2.stats }));
25323
25755
  return;
@@ -25423,15 +25855,15 @@ async function exploreCoverageCommand(mapId, opts) {
25423
25855
  " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
25424
25856
  )
25425
25857
  );
25426
- for (const cap of capReport.capabilities) {
25427
- const title = cap.slug.padEnd(24);
25428
- const surfaces = `${cap.reached_surfaces}/${cap.total_surfaces}`.padStart(
25858
+ for (const cap2 of capReport.capabilities) {
25859
+ const title = cap2.slug.padEnd(24);
25860
+ const surfaces = `${cap2.reached_surfaces}/${cap2.total_surfaces}`.padStart(
25429
25861
  8
25430
25862
  );
25431
- const covStr = `${cap.coverage_pct}%`.padStart(8);
25432
- const scenarios = `${cap.primary_scenarios} / ${cap.secondary_scenarios}`;
25433
- const untestedFlag = cap.primary_scenarios === 0 && cap.unreached_surfaces > 0 ? chalk32.yellow(" (untested)") : "";
25434
- const covColored = cap.coverage_pct === 100 ? chalk32.green(covStr) : cap.coverage_pct === 0 ? chalk32.red(covStr) : chalk32.yellow(covStr);
25863
+ const covStr = `${cap2.coverage_pct}%`.padStart(8);
25864
+ const scenarios = `${cap2.primary_scenarios} / ${cap2.secondary_scenarios}`;
25865
+ const untestedFlag = cap2.primary_scenarios === 0 && cap2.unreached_surfaces > 0 ? chalk32.yellow(" (untested)") : "";
25866
+ const covColored = cap2.coverage_pct === 100 ? chalk32.green(covStr) : cap2.coverage_pct === 0 ? chalk32.red(covStr) : chalk32.yellow(covStr);
25435
25867
  console.log(
25436
25868
  ` ${title} ${surfaces} ${covColored} ${scenarios}${untestedFlag}`
25437
25869
  );
@@ -25469,10 +25901,10 @@ async function exploreCoverageCommand(mapId, opts) {
25469
25901
  const hasGaps = capReport.capabilities.some((c) => c.unreached_surfaces > 0) || capReport.unassigned_surfaces.some((s) => s.status !== "reached");
25470
25902
  if (hasGaps) {
25471
25903
  console.log(chalk32.red("\n UNREACHED SURFACES (Gaps)"));
25472
- for (const cap of capReport.capabilities) {
25473
- const gaps = cap.surfaces.filter((s) => s.status !== "reached");
25904
+ for (const cap2 of capReport.capabilities) {
25905
+ const gaps = cap2.surfaces.filter((s) => s.status !== "reached");
25474
25906
  if (gaps.length === 0) continue;
25475
- console.log(` ${chalk32.bold(cap.slug)}:`);
25907
+ console.log(` ${chalk32.bold(cap2.slug)}:`);
25476
25908
  for (const s of gaps) {
25477
25909
  console.log(
25478
25910
  ` ${chalk32.red("\u25CB")} ${s.key.padEnd(32)} ${s.label}${chalk32.dim(` (depth ${s.depth})`)}`
@@ -25493,10 +25925,10 @@ async function exploreCoverageCommand(mapId, opts) {
25493
25925
  }
25494
25926
  if (opts.all) {
25495
25927
  console.log(chalk32.green("\n REACHED SURFACES"));
25496
- for (const cap of capReport.capabilities) {
25497
- const reached = cap.surfaces.filter((s) => s.status === "reached");
25928
+ for (const cap2 of capReport.capabilities) {
25929
+ const reached = cap2.surfaces.filter((s) => s.status === "reached");
25498
25930
  if (reached.length === 0) continue;
25499
- console.log(` ${chalk32.bold(cap.slug)}:`);
25931
+ console.log(` ${chalk32.bold(cap2.slug)}:`);
25500
25932
  for (const s of reached) {
25501
25933
  console.log(
25502
25934
  ` ${chalk32.green("\u2713")} ${s.key.padEnd(32)} ${s.label}${chalk32.dim(` (depth ${s.depth})`)}`
@@ -25818,7 +26250,7 @@ __export(start_run_exports, {
25818
26250
  startRun: () => startRun,
25819
26251
  testabilityTarget: () => testabilityTarget2
25820
26252
  });
25821
- import path37 from "path";
26253
+ import path38 from "path";
25822
26254
  import chalk33 from "chalk";
25823
26255
  async function testabilityTarget2(projectRoot) {
25824
26256
  try {
@@ -26062,7 +26494,7 @@ async function startRun(scenarioSlugArg, opts) {
26062
26494
  ...viewport ? { viewport } : {}
26063
26495
  };
26064
26496
  await atomicWriteFile(
26065
- path37.join(run.absPath, "walkthrough.json"),
26497
+ path38.join(run.absPath, "walkthrough.json"),
26066
26498
  JSON.stringify(manifest, null, 2) + "\n"
26067
26499
  );
26068
26500
  const liveRes = await registerLiveRun({
@@ -26078,7 +26510,7 @@ async function startRun(scenarioSlugArg, opts) {
26078
26510
  JSON.stringify(
26079
26511
  {
26080
26512
  run_id: run.runId,
26081
- path: path37.relative(projectRoot, run.absPath),
26513
+ path: path38.relative(projectRoot, run.absPath),
26082
26514
  scenario_slug: scenarioSlug,
26083
26515
  target_url: start2.url,
26084
26516
  target_reachable: targetReachable,
@@ -26132,7 +26564,7 @@ async function startRun(scenarioSlugArg, opts) {
26132
26564
  );
26133
26565
  }
26134
26566
  console.log(
26135
- ` ${chalk33.dim("path:")} ${path37.relative(projectRoot, run.absPath)}`
26567
+ ` ${chalk33.dim("path:")} ${path38.relative(projectRoot, run.absPath)}`
26136
26568
  );
26137
26569
  console.log();
26138
26570
  console.log(` ${chalk33.bold("Next steps for blind execution:")}`);
@@ -26362,7 +26794,7 @@ function composeWalkerPrompt(p) {
26362
26794
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
26363
26795
  `Record findings with: ${iamu} new-finding --scenario ${p.scenarioSlug} --run ${p.runId} --step <N> --title "..." --type <helps|hinders|missing|irrelevant|incorrect> --severity <critical|major|minor|observation> --ui-fact "..." --consequence "..." --task-impact <blocks|impairs|slows|neutral|accelerates|enables> --confidence <high|medium|low> --need "<what you needed, no solution>" --description "..." --json`,
26364
26796
  "`--step <N>` is the step whose screenshot shows the problem (or strength): it is the evidence people see next to the finding, so record that step and its screenshot first, and pick the screenshot where the thing is actually visible.",
26365
- `Before you finish, record synthesis with: ${iamu} summary ${p.runId} --worked "..." --worked "..." --question "..."`,
26797
+ `Before you finish, record synthesis with: ${iamu} summary ${p.runId} --worked "..." --worked "..." --question "..." [--setup-note "..."]. --worked: what worked for this persona, one point each. --question: decisions for the product team the walk raised. --setup-note: doubts about the test setup (missing data, accounts, dates), never the product.`,
26366
26798
  `If the walk cannot start as it was set up \u2014 you cannot become this persona (the switch or login fails, or you are someone else), the world the situation needs is not there and you cannot create it, or the starting page does not load \u2014 stop immediately and end the walk with: ${iamu} summary ${p.runId} --blocked "<what was missing, as you saw it>". Do NOT record preliminary findings if preconditions are not met. That is about the setup, not the product: when the product has no way to do what you need, that is a finding (--type missing), not a block. Do not record findings about the missing setup itself.`,
26367
26799
  "Execute everything via CLI commands in the shell. Never invoke host Edit or Write tools on files in the workspace.",
26368
26800
  "",
@@ -27012,7 +27444,7 @@ __export(new_finding_exports, {
27012
27444
  });
27013
27445
  import { randomUUID as randomUUID12 } from "crypto";
27014
27446
  import { access, readFile as readFile7 } from "fs/promises";
27015
- import path38 from "path";
27447
+ import path39 from "path";
27016
27448
  import chalk36 from "chalk";
27017
27449
  async function newFinding(opts) {
27018
27450
  const ctx = await resolveProject(opts);
@@ -27089,7 +27521,7 @@ async function newFinding(opts) {
27089
27521
  }
27090
27522
  try {
27091
27523
  await access(
27092
- path38.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
27524
+ path39.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
27093
27525
  );
27094
27526
  } catch {
27095
27527
  commandError(
@@ -27108,7 +27540,7 @@ async function newFinding(opts) {
27108
27540
  if (!scenarioSlug) {
27109
27541
  try {
27110
27542
  const manifestRaw = await readFile7(
27111
- path38.join(runDirPath, "walkthrough.json"),
27543
+ path39.join(runDirPath, "walkthrough.json"),
27112
27544
  "utf8"
27113
27545
  );
27114
27546
  const manifest = JSON.parse(manifestRaw);
@@ -27255,7 +27687,7 @@ __export(summary_exports, {
27255
27687
  summaryCommand: () => summaryCommand
27256
27688
  });
27257
27689
  import { promises as fs38 } from "fs";
27258
- import path39 from "path";
27690
+ import path40 from "path";
27259
27691
  import chalk37 from "chalk";
27260
27692
  function blockedFrom(value) {
27261
27693
  const reason = value && typeof value === "object" ? value.reason : value;
@@ -27278,12 +27710,18 @@ async function summaryCommand(runIdArg, opts) {
27278
27710
  if (!runDirPath) return;
27279
27711
  let whatWorked = [];
27280
27712
  let openQuestions = [];
27713
+ let setupNotes = [];
27281
27714
  let model = opts.model?.trim() || void 0;
27282
- const summaryPath = path39.join(runDirPath, "summary.json");
27715
+ const summaryPath = path40.join(runDirPath, "summary.json");
27283
27716
  let blocked;
27284
27717
  try {
27285
27718
  const prev = JSON.parse(await fs38.readFile(summaryPath, "utf8"));
27286
27719
  blocked = blockedFrom(prev.blocked);
27720
+ if (Array.isArray(prev.setup_notes)) {
27721
+ setupNotes = prev.setup_notes.filter(
27722
+ (x) => typeof x === "string" && x.trim() !== ""
27723
+ );
27724
+ }
27287
27725
  } catch {
27288
27726
  }
27289
27727
  if (opts.file) {
@@ -27292,7 +27730,7 @@ async function summaryCommand(runIdArg, opts) {
27292
27730
  if (opts.file === "-") {
27293
27731
  raw = await readStdinText();
27294
27732
  } else {
27295
- raw = await fs38.readFile(path39.resolve(projectRoot, opts.file), "utf8");
27733
+ raw = await fs38.readFile(path40.resolve(projectRoot, opts.file), "utf8");
27296
27734
  }
27297
27735
  const parsed = JSON.parse(raw);
27298
27736
  if (Array.isArray(parsed.what_worked)) {
@@ -27305,6 +27743,11 @@ async function summaryCommand(runIdArg, opts) {
27305
27743
  (x) => typeof x === "string" && x.trim() !== ""
27306
27744
  );
27307
27745
  }
27746
+ if (Array.isArray(parsed.setup_notes)) {
27747
+ setupNotes = parsed.setup_notes.filter(
27748
+ (x) => typeof x === "string" && x.trim() !== ""
27749
+ );
27750
+ }
27308
27751
  if (typeof parsed.model === "string" && parsed.model.trim()) {
27309
27752
  model = parsed.model.trim();
27310
27753
  }
@@ -27330,6 +27773,7 @@ async function summaryCommand(runIdArg, opts) {
27330
27773
  if (cliQuestions.length > 0) {
27331
27774
  openQuestions = [...openQuestions, ...cliQuestions];
27332
27775
  }
27776
+ setupNotes = [.../* @__PURE__ */ new Set([...setupNotes, ...toList(opts.setupNote)])];
27333
27777
  if (opts.blocked === false) {
27334
27778
  blocked = void 0;
27335
27779
  } else {
@@ -27338,6 +27782,7 @@ async function summaryCommand(runIdArg, opts) {
27338
27782
  const data = {
27339
27783
  what_worked: whatWorked,
27340
27784
  open_questions: openQuestions,
27785
+ setup_notes: setupNotes,
27341
27786
  ...model ? { model } : {},
27342
27787
  ...blocked ? { blocked } : {}
27343
27788
  };
@@ -27348,7 +27793,7 @@ async function summaryCommand(runIdArg, opts) {
27348
27793
  JSON.stringify(
27349
27794
  {
27350
27795
  run_id: runId,
27351
- path: path39.relative(projectRoot, summaryPath),
27796
+ path: path40.relative(projectRoot, summaryPath),
27352
27797
  summary: data
27353
27798
  },
27354
27799
  null,
@@ -27374,6 +27819,11 @@ async function summaryCommand(runIdArg, opts) {
27374
27819
  ` ${chalk37.dim("open questions:")} ${data.open_questions.length} item(s)`
27375
27820
  );
27376
27821
  }
27822
+ if (data.setup_notes.length > 0) {
27823
+ console.log(
27824
+ ` ${chalk37.dim("setup notes:")} ${data.setup_notes.length} item(s)`
27825
+ );
27826
+ }
27377
27827
  console.log();
27378
27828
  }
27379
27829
  var init_summary = __esm({
@@ -27408,7 +27858,7 @@ __export(complete_exports, {
27408
27858
  validateCanonicalWalkthrough: () => validateCanonicalWalkthrough
27409
27859
  });
27410
27860
  import { promises as fs39 } from "fs";
27411
- import path40 from "path";
27861
+ import path41 from "path";
27412
27862
  import chalk38 from "chalk";
27413
27863
  async function complete(runIdArg, opts) {
27414
27864
  const ctx = await resolveProject(opts);
@@ -27445,7 +27895,7 @@ async function complete(runIdArg, opts) {
27445
27895
  let judgeReported;
27446
27896
  try {
27447
27897
  const verdictsRaw = await fs39.readFile(
27448
- path40.join(run.runDirPath, "verdicts.json"),
27898
+ path41.join(run.runDirPath, "verdicts.json"),
27449
27899
  "utf8"
27450
27900
  );
27451
27901
  const parsed = JSON.parse(verdictsRaw);
@@ -27521,7 +27971,7 @@ async function loadRun(runId, opts) {
27521
27971
  });
27522
27972
  return null;
27523
27973
  }
27524
- const manifestPath = path40.join(runDirPath, "walkthrough.json");
27974
+ const manifestPath = path41.join(runDirPath, "walkthrough.json");
27525
27975
  let manifest;
27526
27976
  try {
27527
27977
  manifest = JSON.parse(
@@ -27552,7 +28002,7 @@ async function reportAlreadyCompleted(run, opts) {
27552
28002
  if (opts?.json) {
27553
28003
  try {
27554
28004
  const raw = await fs39.readFile(
27555
- path40.join(run.runDirPath, "canonical.json"),
28005
+ path41.join(run.runDirPath, "canonical.json"),
27556
28006
  "utf8"
27557
28007
  );
27558
28008
  console.log(raw.trim());
@@ -27688,15 +28138,11 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
27688
28138
  }
27689
28139
  console.log();
27690
28140
  console.log(
27691
- ` ${chalk38.dim("canonical:")} ${chalk38.dim(path40.relative(projectRoot, assembled.canonicalPath))}`
27692
- );
27693
- console.log(
27694
- ` ${chalk38.dim("report:")} ${chalk38.dim(path40.relative(projectRoot, assembled.reportPath))}`
28141
+ ` ${chalk38.dim("canonical:")} ${chalk38.dim(path41.relative(projectRoot, assembled.canonicalPath))}`
27695
28142
  );
28143
+ console.log(` ${chalk38.dim("report:")} ${assembled.reportPath}`);
27696
28144
  if (assembled.htmlReportPath) {
27697
- console.log(
27698
- ` ${chalk38.dim("html:")} ${chalk38.dim(path40.relative(projectRoot, assembled.htmlReportPath))}`
27699
- );
28145
+ console.log(` ${chalk38.dim("html:")} ${assembled.htmlReportPath}`);
27700
28146
  }
27701
28147
  if (extras.viewportWarning) {
27702
28148
  console.log();
@@ -27765,7 +28211,7 @@ function screenshotFitsViewport(width, expected) {
27765
28211
  }
27766
28212
  async function screenshotSizeWarning(runDirPath, viewport) {
27767
28213
  if (!viewport) return null;
27768
- const dir = path40.join(runDirPath, "screenshots");
28214
+ const dir = path41.join(runDirPath, "screenshots");
27769
28215
  let first;
27770
28216
  try {
27771
28217
  first = (await fs39.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
@@ -27775,7 +28221,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
27775
28221
  if (!first) return null;
27776
28222
  let size = null;
27777
28223
  try {
27778
- const handle = await fs39.open(path40.join(dir, first), "r");
28224
+ const handle = await fs39.open(path41.join(dir, first), "r");
27779
28225
  try {
27780
28226
  const head = Buffer.alloc(24);
27781
28227
  const { bytesRead } = await handle.read(head, 0, 24, 0);
@@ -27792,7 +28238,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
27792
28238
  async function readWalkerSummary(runDirPath) {
27793
28239
  try {
27794
28240
  const data = JSON.parse(
27795
- await fs39.readFile(path40.join(runDirPath, "summary.json"), "utf8")
28241
+ await fs39.readFile(path41.join(runDirPath, "summary.json"), "utf8")
27796
28242
  );
27797
28243
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
27798
28244
  const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
@@ -27800,6 +28246,7 @@ async function readWalkerSummary(runDirPath) {
27800
28246
  return {
27801
28247
  what_worked: strings(data.what_worked),
27802
28248
  open_questions: strings(data.open_questions),
28249
+ setup_notes: strings(data.setup_notes),
27803
28250
  model,
27804
28251
  ...typeof reason === "string" && reason.trim() !== "" ? { blocked: { reason: reason.trim() } } : {}
27805
28252
  };
@@ -27901,7 +28348,7 @@ function validateCanonicalWalkthrough(canonical) {
27901
28348
  return canonicalWalkthroughSchema.parse(canonical);
27902
28349
  }
27903
28350
  async function persistCanonical(runDirPath, canonical) {
27904
- const canonicalPath = path40.join(runDirPath, "canonical.json");
28351
+ const canonicalPath = path41.join(runDirPath, "canonical.json");
27905
28352
  await atomicWriteFile(
27906
28353
  canonicalPath,
27907
28354
  `${JSON.stringify(canonical, null, 2)}
@@ -27910,7 +28357,7 @@ async function persistCanonical(runDirPath, canonical) {
27910
28357
  return canonicalPath;
27911
28358
  }
27912
28359
  async function persistReport(runDirPath, canonical) {
27913
- const reportPath = path40.join(runDirPath, "report.md");
28360
+ const reportPath = path41.join(runDirPath, "report.md");
27914
28361
  await atomicWriteFile(reportPath, renderCanonicalMarkdown(canonical));
27915
28362
  return reportPath;
27916
28363
  }
@@ -28089,13 +28536,13 @@ __export(match_exports, {
28089
28536
  matchRecord: () => matchRecord
28090
28537
  });
28091
28538
  import { promises as fs40 } from "fs";
28092
- import path41 from "path";
28539
+ import path42 from "path";
28093
28540
  import chalk39 from "chalk";
28094
28541
  async function completedManifest2(runDirPath, runId, opts) {
28095
28542
  let manifest;
28096
28543
  try {
28097
28544
  manifest = JSON.parse(
28098
- await fs40.readFile(path41.join(runDirPath, "walkthrough.json"), "utf8")
28545
+ await fs40.readFile(path42.join(runDirPath, "walkthrough.json"), "utf8")
28099
28546
  );
28100
28547
  } catch {
28101
28548
  commandError(`Run not found: ${runId}`, {
@@ -28132,7 +28579,7 @@ async function match(runId, opts) {
28132
28579
  );
28133
28580
  const needed = !manifest.blocked && input.current.length > 0 && (input.prior.length > 0 || (input.guidance ?? []).length > 0);
28134
28581
  const prompt = needed ? composeMatchPrompt(input) : null;
28135
- const promptFilePath = opts?.promptFile ?? path41.join(runDirPath, "match_prompt.md");
28582
+ const promptFilePath = opts?.promptFile ?? path42.join(runDirPath, "match_prompt.md");
28136
28583
  if (prompt) {
28137
28584
  await fs40.writeFile(promptFilePath, prompt, "utf8");
28138
28585
  } else {
@@ -28148,8 +28595,8 @@ async function match(runId, opts) {
28148
28595
  ...input,
28149
28596
  needed,
28150
28597
  ...manifest.blocked ? { blocked: manifest.blocked } : {},
28151
- prompt_file: prompt ? path41.relative(ctx.projectRoot, promptFilePath) : null,
28152
- prompt_file_abs: prompt ? path41.resolve(promptFilePath) : null,
28598
+ prompt_file: prompt ? path42.relative(ctx.projectRoot, promptFilePath) : null,
28599
+ prompt_file_abs: prompt ? path42.resolve(promptFilePath) : null,
28153
28600
  match_prompt: prompt
28154
28601
  },
28155
28602
  null,
@@ -28177,7 +28624,7 @@ async function matchRecord(runId, opts) {
28177
28624
  const manifest = await completedManifest2(runDirPath, runId, opts);
28178
28625
  if (!manifest) return;
28179
28626
  const input = await readAnswerInput(opts, "matches", {
28180
- defaultPath: path41.join(runDirPath, "matches.json"),
28627
+ defaultPath: path42.join(runDirPath, "matches.json"),
28181
28628
  code: "missing_matches_input",
28182
28629
  fix: `iamu match-record ${runId} --data '<json>'`
28183
28630
  });
@@ -28256,10 +28703,10 @@ __export(advise_exports, {
28256
28703
  readAdvisePrompt: () => readAdvisePrompt
28257
28704
  });
28258
28705
  import { existsSync as existsSync17, promises as fs41, readFileSync as readFileSync10 } from "fs";
28259
- import path42 from "path";
28706
+ import path43 from "path";
28260
28707
  import chalk40 from "chalk";
28261
28708
  function readAdvisePrompt(projectRoot) {
28262
- const installed = path42.join(projectRoot, "iamu", ".prompts", "advise.md");
28709
+ const installed = path43.join(projectRoot, "iamu", ".prompts", "advise.md");
28263
28710
  if (existsSync17(installed)) {
28264
28711
  try {
28265
28712
  return readFileSync10(installed, "utf8");
@@ -28304,7 +28751,7 @@ async function advise(runId, opts = {}) {
28304
28751
  findings2 = input.findings.length;
28305
28752
  needed = findings2 >= 2;
28306
28753
  if (!needed) reason = "fewer than two open findings in the project";
28307
- promptPath = opts.promptFile ?? path42.join(analysisDir(projectRoot), "advise_prompt.md");
28754
+ promptPath = opts.promptFile ?? path43.join(analysisDir(projectRoot), "advise_prompt.md");
28308
28755
  } else {
28309
28756
  if (!requireRunId(runId, opts, "iamu advise <run-id>")) return;
28310
28757
  const runDirPath = resolveRunDir(projectRoot, runId, opts);
@@ -28314,21 +28761,21 @@ async function advise(runId, opts = {}) {
28314
28761
  input = await buildAdviseInput(projectRoot, runId, manifest.scenario_slug);
28315
28762
  refsDir = runDirPath;
28316
28763
  findings2 = input.findings.length;
28317
- const advised = existsSync17(path42.join(runDirPath, ADVICE_FILE));
28764
+ const advised = existsSync17(path43.join(runDirPath, ADVICE_FILE));
28318
28765
  needed = !manifest.blocked && findings2 > 0 && !advised;
28319
28766
  if (manifest.blocked) reason = "the run was blocked";
28320
28767
  else if (advised) reason = "the run is already advised";
28321
28768
  else if (findings2 === 0) reason = "no open findings to advise";
28322
- promptPath = opts.promptFile ?? path42.join(runDirPath, "advise_prompt.md");
28769
+ promptPath = opts.promptFile ?? path43.join(runDirPath, "advise_prompt.md");
28323
28770
  }
28324
28771
  const { bundle, refs } = promptBundle(input);
28325
28772
  const prompt = needed ? composePrompt(projectRoot, bundle) : null;
28326
28773
  if (prompt) {
28327
- await fs41.mkdir(path42.dirname(promptPath), { recursive: true });
28774
+ await fs41.mkdir(path43.dirname(promptPath), { recursive: true });
28328
28775
  await fs41.writeFile(promptPath, prompt, "utf8");
28329
28776
  await fs41.mkdir(refsDir, { recursive: true });
28330
28777
  await fs41.writeFile(
28331
- path42.join(refsDir, ADVISE_REFS_FILE),
28778
+ path43.join(refsDir, ADVISE_REFS_FILE),
28332
28779
  `${JSON.stringify(refs, null, 2)}
28333
28780
  `,
28334
28781
  "utf8"
@@ -28342,8 +28789,8 @@ async function advise(runId, opts = {}) {
28342
28789
  ...reason ? { reason } : {},
28343
28790
  mode: opts.project ? "project" : "run",
28344
28791
  findings: findings2,
28345
- prompt_file: prompt ? path42.relative(projectRoot, promptPath) : null,
28346
- prompt_file_abs: prompt ? path42.resolve(promptPath) : null,
28792
+ prompt_file: prompt ? path43.relative(projectRoot, promptPath) : null,
28793
+ prompt_file_abs: prompt ? path43.resolve(promptPath) : null,
28347
28794
  prompt_chars: prompt?.length ?? 0,
28348
28795
  advise_prompt: prompt
28349
28796
  },
@@ -28407,7 +28854,7 @@ async function adviseRecord(runId, opts) {
28407
28854
  const { projectRoot } = ctx;
28408
28855
  if (opts.project) {
28409
28856
  const input2 = await readAnswerInput(opts, "advice", {
28410
- defaultPath: path42.join(analysisDir(projectRoot), "answer.json"),
28857
+ defaultPath: path43.join(analysisDir(projectRoot), "answer.json"),
28411
28858
  code: "missing_advice_input",
28412
28859
  fix: "iamu advise-record --project --data '<json>'"
28413
28860
  });
@@ -28437,7 +28884,7 @@ async function adviseRecord(runId, opts) {
28437
28884
  const manifest = await completedManifest2(runDirPath, runId, opts);
28438
28885
  if (!manifest) return;
28439
28886
  const input = await readAnswerInput(opts, "advice", {
28440
- defaultPath: path42.join(runDir(projectRoot, runId), "advise_answer.json"),
28887
+ defaultPath: path43.join(runDir(projectRoot, runId), "advise_answer.json"),
28441
28888
  code: "missing_advice_input",
28442
28889
  fix: `iamu advise-record ${runId} --data '<json>'`
28443
28890
  });
@@ -28471,9 +28918,9 @@ async function directionsList(opts) {
28471
28918
  const ctx = await resolveProject(opts);
28472
28919
  if (!ctx) return;
28473
28920
  const all = await listDirections(ctx.projectRoot);
28474
- const rank = (s) => ["accepted", "proposed", "done", "rejected"].indexOf(s);
28921
+ const rank2 = (s) => ["accepted", "proposed", "done", "rejected"].indexOf(s);
28475
28922
  const sorted = [...all].sort(
28476
- (a, b) => rank(a.frontmatter.status) - rank(b.frontmatter.status)
28923
+ (a, b) => rank2(a.frontmatter.status) - rank2(b.frontmatter.status)
28477
28924
  );
28478
28925
  if (opts.json) {
28479
28926
  console.log(
@@ -28483,7 +28930,7 @@ async function directionsList(opts) {
28483
28930
  problem: d.texts.problem,
28484
28931
  change: d.texts.change,
28485
28932
  leading_practice: d.texts.leading_practice,
28486
- path: path42.relative(ctx.projectRoot, d.path)
28933
+ path: path43.relative(ctx.projectRoot, d.path)
28487
28934
  })),
28488
28935
  null,
28489
28936
  2
@@ -28856,9 +29303,9 @@ function registerProjectCommands(program) {
28856
29303
  "after",
28857
29304
  "\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
28858
29305
  ).action(
28859
- async (path43, opts) => {
29306
+ async (path44, opts) => {
28860
29307
  const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
28861
- await validate2(path43, opts);
29308
+ await validate2(path44, opts);
28862
29309
  }
28863
29310
  );
28864
29311
  program.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
@@ -29232,13 +29679,16 @@ function registerWalkthroughCommands(program) {
29232
29679
  }
29233
29680
  );
29234
29681
  program.command("summary [run-id]").helpGroup("Walkthrough:").description(
29235
- "Record walker summary (what worked, open questions, or that the walk was blocked) atomically"
29682
+ "Record walker summary (what worked, open questions, setup notes, or that the walk was blocked) atomically"
29236
29683
  ).option("--run <run-id>", "Run ID (or pass as positional argument)").option(
29237
29684
  "--worked <note...>",
29238
29685
  "Things that worked well (can be specified multiple times)"
29239
29686
  ).option(
29240
29687
  "--question <note...>",
29241
- "Open questions that arose (can be specified multiple times)"
29688
+ "Questions for the product team the walk raised (can be specified multiple times)"
29689
+ ).option(
29690
+ "--setup-note <note...>",
29691
+ "Doubts about the test setup, not the product: missing data, accounts, dates (can be specified multiple times)"
29242
29692
  ).option("--model <name>", "Model identifier (e.g. flash, haiku)").option(
29243
29693
  "--blocked <reason>",
29244
29694
  "The walk could not start as set up (persona unreachable, world missing): what was missing"