@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/bin.js CHANGED
@@ -1433,7 +1433,11 @@ var init_walkthrough_canonical = __esm({
1433
1433
  slug: z22.string(),
1434
1434
  persona_slug: z22.string(),
1435
1435
  outline_slug: z22.string(),
1436
- brief: z22.string()
1436
+ brief: z22.string(),
1437
+ /** Display names, so reports never show slugs. Absent on older runs. */
1438
+ persona_name: z22.string().optional(),
1439
+ persona_role: z22.string().optional(),
1440
+ outline_title: z22.string().optional()
1437
1441
  }).passthrough();
1438
1442
  canonicalTargetSchema = z22.object({
1439
1443
  url: z22.string(),
@@ -1467,6 +1471,10 @@ var init_walkthrough_canonical = __esm({
1467
1471
  }).passthrough();
1468
1472
  canonicalFindingSchema = z22.object({
1469
1473
  id: z22.string(),
1474
+ /** The finding's short title. Absent on older runs. */
1475
+ title: z22.string().nullable().optional(),
1476
+ /** The page it was seen on (the finding's own, else its step's). */
1477
+ page_url: z22.string().nullable().optional(),
1470
1478
  /** Stable across runs for pattern tracking — null until cross-run wiring lands. */
1471
1479
  canonical_id: z22.string().nullable(),
1472
1480
  type: findingTypeEnum,
@@ -1728,14 +1736,14 @@ Bad background: "Construction worker. Experienced in the field. Uses a smartphon
1728
1736
  - observation: An enhancement opportunity. Not friction, but a missed chance to help.
1729
1737
 
1730
1738
  Finding type pairing:
1731
- - helps \u2192 always observation severity. Record sparingly \u2014 only when the product does something genuinely supportive that's worth preserving.
1739
+ - helps \u2192 always observation severity. Record each one you meet: something the product does that this person would miss if it were gone.
1732
1740
  - hinders \u2192 critical, major, or minor.
1733
1741
  - missing \u2192 critical, major, or minor.
1734
1742
  - incorrect \u2192 critical or major only.
1735
1743
  - irrelevant \u2192 minor or observation.
1736
1744
 
1737
1745
  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.`;
1738
- 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.`;
1746
+ 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.`;
1739
1747
  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.
1740
1748
 
1741
1749
  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.
@@ -1918,7 +1926,7 @@ var init_prompt_registry = __esm({
1918
1926
  },
1919
1927
  walkthrough: {
1920
1928
  id: "walkthroughPhilosophy",
1921
- version: "2.0.0",
1929
+ version: "2.1.0",
1922
1930
  compose: (opts) => composeWalkthroughPhilosophy(opts)
1923
1931
  },
1924
1932
  scenarioCraft: {
@@ -6849,413 +6857,196 @@ var init_revisit = __esm({
6849
6857
  }
6850
6858
  });
6851
6859
 
6852
- // ../../packages/local-store/dist/journey-diff.js
6853
- function isProblemFinding(type2) {
6854
- return PROBLEM_TYPES2.has(type2.toLowerCase());
6860
+ // ../../packages/local-store/dist/directions.js
6861
+ import { randomUUID as randomUUID5 } from "crypto";
6862
+ import { promises as fs12 } from "fs";
6863
+ import path9 from "path";
6864
+ import { z as z25 } from "zod";
6865
+ function directionsDir(projectRoot) {
6866
+ return path9.join(iamuDir(projectRoot), DIRECTIONS_DIR);
6855
6867
  }
6856
- function levenshteinDistance(a, b) {
6857
- if (a === b)
6858
- return 0;
6859
- if (!a.length)
6860
- return b.length;
6861
- if (!b.length)
6862
- return a.length;
6863
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
6864
- for (let i = 1; i <= a.length; i++) {
6865
- let prev = i;
6866
- for (let j = 1; j <= b.length; j++) {
6867
- const val = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], prev, row[j]) + 1;
6868
- row[j - 1] = prev;
6869
- prev = val;
6870
- }
6871
- row[b.length] = prev;
6872
- }
6873
- return row[b.length];
6868
+ function directionPath(projectRoot, slug) {
6869
+ return path9.join(directionsDir(projectRoot), `${slug}.md`);
6874
6870
  }
6875
- function normalizeAction(action) {
6876
- if (!action)
6877
- return "";
6878
- return action.toLowerCase().replace(/['"`]/g, "").replace(/[^\p{L}\p{N}\s/_-]+/gu, " ").replace(/\s+/g, " ").trim();
6871
+ function renderBody(texts) {
6872
+ return [
6873
+ "## Problem",
6874
+ "",
6875
+ texts.problem.trim(),
6876
+ "",
6877
+ "## Change",
6878
+ "",
6879
+ texts.change.trim(),
6880
+ "",
6881
+ "## Leading practice",
6882
+ "",
6883
+ texts.leading_practice.trim(),
6884
+ ""
6885
+ ].join("\n");
6879
6886
  }
6880
- function actionTokens(action) {
6881
- const norm = normalizeAction(action);
6882
- if (!norm)
6883
- return /* @__PURE__ */ new Set();
6884
- return new Set(norm.split(/\s+/).filter(Boolean));
6887
+ function section(body, heading) {
6888
+ const re = new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, "m");
6889
+ return re.exec(body)?.[1]?.trim() ?? "";
6885
6890
  }
6886
- function actionSimilarity(a, b) {
6887
- const normA = normalizeAction(a);
6888
- const normB = normalizeAction(b);
6889
- if (!normA && !normB)
6890
- return 1;
6891
- if (!normA || !normB)
6892
- return 0;
6893
- if (normA === normB)
6894
- return 1;
6895
- const tokensA = actionTokens(normA);
6896
- const tokensB = actionTokens(normB);
6897
- let intersection = 0;
6898
- for (const t of tokensA) {
6899
- if (tokensB.has(t))
6900
- intersection++;
6901
- }
6902
- if (intersection === 0) {
6903
- if (tokensA.size <= 1 && tokensB.size <= 1) {
6904
- const maxLen = Math.max(normA.length, normB.length);
6905
- const levDist = levenshteinDistance(normA, normB);
6906
- return levDist <= 2 && maxLen > 0 ? 1 - levDist / maxLen : 0;
6907
- }
6908
- return 0;
6909
- }
6910
- const union2 = tokensA.size + tokensB.size - intersection;
6911
- const jaccard = union2 > 0 ? intersection / union2 : 0;
6912
- const minSize = Math.min(tokensA.size, tokensB.size);
6913
- const subsetRatio = minSize > 0 ? intersection / minSize : 0;
6914
- if (normA.includes(normB) || normB.includes(normA) || subsetRatio === 1) {
6915
- return Math.max(0.85, 0.7 + 0.3 * jaccard);
6891
+ function parseDirection(raw, file) {
6892
+ const { frontmatter, body } = parseFrontmatter(raw);
6893
+ return {
6894
+ frontmatter: directionFrontmatterSchema.parse(frontmatter),
6895
+ texts: {
6896
+ problem: section(body, "Problem"),
6897
+ change: section(body, "Change"),
6898
+ leading_practice: section(body, "Leading practice")
6899
+ },
6900
+ path: file
6901
+ };
6902
+ }
6903
+ async function readDirection(projectRoot, slug) {
6904
+ const file = directionPath(projectRoot, slug);
6905
+ let raw;
6906
+ try {
6907
+ raw = await fs12.readFile(file, "utf8");
6908
+ } catch (err) {
6909
+ if (err.code === "ENOENT")
6910
+ return null;
6911
+ throw err;
6916
6912
  }
6917
- return 0.5 * jaccard + 0.5 * subsetRatio;
6913
+ return parseDirection(raw, file);
6918
6914
  }
6919
- function routeSimilarity(urlA, surfaceA, urlB, surfaceB) {
6920
- const normUrlA = normalizePlaceKey(urlA);
6921
- const normUrlB = normalizePlaceKey(urlB);
6922
- const hasUrlA = normUrlA !== null;
6923
- const hasUrlB = normUrlB !== null;
6924
- let urlScore = null;
6925
- if (hasUrlA && hasUrlB) {
6926
- if (normUrlA === normUrlB) {
6927
- urlScore = 1;
6928
- } else {
6929
- const pathA = normUrlA.split("?")[0];
6930
- const pathB = normUrlB.split("?")[0];
6931
- urlScore = pathA === pathB ? 0.8 : 0;
6932
- }
6915
+ async function listDirections(projectRoot) {
6916
+ let names2;
6917
+ try {
6918
+ names2 = await fs12.readdir(directionsDir(projectRoot));
6919
+ } catch (err) {
6920
+ if (err.code === "ENOENT")
6921
+ return [];
6922
+ throw err;
6933
6923
  }
6934
- const sA = surfaceA?.trim();
6935
- const sB = surfaceB?.trim();
6936
- let surfaceScore = null;
6937
- if (sA && sB) {
6938
- if (surfaceKey(sA) === surfaceKey(sB)) {
6939
- surfaceScore = 1;
6940
- } else if (surfaceMatches(sA, sB) || surfaceMatches(sB, sA)) {
6941
- surfaceScore = 0.8;
6942
- } else {
6943
- surfaceScore = 0;
6924
+ const out = [];
6925
+ for (const name of names2.sort()) {
6926
+ if (!name.endsWith(".md"))
6927
+ continue;
6928
+ const file = path9.join(directionsDir(projectRoot), name);
6929
+ try {
6930
+ out.push(parseDirection(await fs12.readFile(file, "utf8"), file));
6931
+ } catch {
6944
6932
  }
6945
6933
  }
6946
- if (urlScore !== null && surfaceScore !== null) {
6947
- if (urlScore >= 0.8 && surfaceScore >= 0.8)
6948
- return 1;
6949
- if (urlScore >= 0.8)
6950
- return 0.85;
6951
- if (surfaceScore >= 0.8)
6952
- return 0.75;
6953
- return 0;
6954
- }
6955
- if (urlScore !== null)
6956
- return urlScore;
6957
- if (surfaceScore !== null)
6958
- return surfaceScore;
6959
- return 0.5;
6934
+ return out.sort((a, b) => b.frontmatter.updated_at.localeCompare(a.frontmatter.updated_at));
6960
6935
  }
6961
- function stepSimilarity(a, b) {
6962
- const rSim = routeSimilarity(a.url, a.surface, b.url, b.surface);
6963
- const aSim = actionSimilarity(a.action, b.action);
6964
- if (aSim < 0.25 && a.action && b.action) {
6965
- return Math.min(0.2, rSim * 0.2);
6966
- }
6967
- if (rSim === 0) {
6968
- return aSim >= 0.8 ? aSim * 0.4 : 0.1;
6969
- }
6970
- return 0.5 * rSim + 0.5 * aSim;
6936
+ async function writeDirection(projectRoot, fm, texts) {
6937
+ const file = directionPath(projectRoot, fm.slug);
6938
+ const validated = directionFrontmatterSchema.parse(fm);
6939
+ await atomicWriteFile(file, serializeFrontmatter(validated, renderBody(texts)));
6940
+ return file;
6971
6941
  }
6972
- function findingMatchesStep(finding, stepNumber2) {
6973
- if (finding.linked_step_ids && finding.linked_step_ids.length > 0) {
6974
- return finding.linked_step_ids.some((id) => id === `step-${stepNumber2}` || id === String(stepNumber2));
6975
- }
6976
- if (finding.walkthrough_step_id) {
6977
- const raw = finding.walkthrough_step_id.trim();
6978
- if (raw === `step-${stepNumber2}` || raw === String(stepNumber2)) {
6979
- return true;
6980
- }
6942
+ async function upsertDirection(projectRoot, proposal, source, advisedBy, now = (/* @__PURE__ */ new Date()).toISOString()) {
6943
+ const existing = await readDirection(projectRoot, proposal.slug);
6944
+ const texts = {
6945
+ problem: proposal.problem,
6946
+ change: proposal.change,
6947
+ leading_practice: proposal.leading_practice
6948
+ };
6949
+ if (existing?.frontmatter.status === "rejected") {
6950
+ return { outcome: "previously_rejected" };
6981
6951
  }
6982
- return false;
6952
+ const prev = existing?.frontmatter;
6953
+ const fm = {
6954
+ id: prev?.id ?? randomUUID5(),
6955
+ slug: proposal.slug,
6956
+ title: proposal.title,
6957
+ status: prev?.status ?? "proposed",
6958
+ size: proposal.size,
6959
+ absorbs: union(prev?.absorbs ?? [], proposal.absorbs, (x) => x),
6960
+ reach: union(prev?.reach ?? [], proposal.reach, (r) => `${r.scenario}:${r.effect}`),
6961
+ verify: union(prev?.verify ?? [], proposal.verify, (x) => x),
6962
+ builds_on: union(prev?.builds_on ?? [], proposal.builds_on, (r) => `${r.path}#${r.symbol}`),
6963
+ seen_in_runs: union(prev?.seen_in_runs ?? [], [source], (x) => x),
6964
+ advised_by: advisedBy,
6965
+ created_at: prev?.created_at ?? now,
6966
+ updated_at: now
6967
+ };
6968
+ const file = await writeDirection(projectRoot, fm, texts);
6969
+ return {
6970
+ outcome: existing ? "updated" : "created",
6971
+ direction: { frontmatter: fm, texts, path: file }
6972
+ };
6983
6973
  }
6984
- function isStallNote(note) {
6985
- if (!note)
6986
- return false;
6987
- const lower = note.toLowerCase();
6988
- return STALL_KEYWORDS.some((kw) => lower.includes(kw));
6974
+ async function setDirectionStatus(projectRoot, slug, status2, now = (/* @__PURE__ */ new Date()).toISOString()) {
6975
+ const existing = await readDirection(projectRoot, slug);
6976
+ if (!existing)
6977
+ return null;
6978
+ const fm = { ...existing.frontmatter, status: status2, updated_at: now };
6979
+ await writeDirection(projectRoot, fm, existing.texts);
6980
+ return { ...existing, frontmatter: fm };
6989
6981
  }
6990
- function alignJourneySteps(scenario, baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings) {
6991
- const m = baselineSteps.length;
6992
- const n = currentSteps.length;
6993
- const GAP_PENALTY = -0.7;
6994
- const MATCH_THRESHOLD = 0.45;
6995
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
6996
- for (let i2 = 0; i2 <= m; i2++)
6997
- dp[i2][0] = i2 * GAP_PENALTY;
6998
- for (let j2 = 0; j2 <= n; j2++)
6999
- dp[0][j2] = j2 * GAP_PENALTY;
7000
- for (let i2 = 1; i2 <= m; i2++) {
7001
- for (let j2 = 1; j2 <= n; j2++) {
7002
- const sim = stepSimilarity(baselineSteps[i2 - 1], currentSteps[j2 - 1]);
7003
- const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
7004
- const match2 = dp[i2 - 1][j2 - 1] + matchScore;
7005
- const delP = dp[i2 - 1][j2] + GAP_PENALTY;
7006
- const insC = dp[i2][j2 - 1] + GAP_PENALTY;
7007
- dp[i2][j2] = Math.max(match2, delP, insC);
7008
- }
7009
- }
7010
- const alignedPairs = [];
7011
- let i = m;
7012
- let j = n;
7013
- while (i > 0 || j > 0) {
7014
- if (i > 0 && j > 0) {
7015
- const sim = stepSimilarity(baselineSteps[i - 1], currentSteps[j - 1]);
7016
- const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
7017
- const scoreFromMatch = dp[i - 1][j - 1] + matchScore;
7018
- if (Math.abs(dp[i][j] - scoreFromMatch) < 1e-6 && sim >= MATCH_THRESHOLD) {
7019
- alignedPairs.push({
7020
- baseline: baselineSteps[i - 1],
7021
- current: currentSteps[j - 1]
7022
- });
7023
- i--;
7024
- j--;
7025
- continue;
7026
- }
7027
- }
7028
- if (j > 0 && Math.abs(dp[i][j] - (dp[i][j - 1] + GAP_PENALTY)) < 1e-6) {
7029
- alignedPairs.push({
7030
- baseline: null,
7031
- current: currentSteps[j - 1]
7032
- });
7033
- j--;
7034
- } else if (i > 0 && Math.abs(dp[i][j] - (dp[i - 1][j] + GAP_PENALTY)) < 1e-6) {
7035
- alignedPairs.push({
7036
- baseline: baselineSteps[i - 1],
7037
- current: null
7038
- });
7039
- i--;
7040
- } else if (i > 0 && j > 0) {
7041
- alignedPairs.push({
7042
- baseline: baselineSteps[i - 1],
7043
- current: currentSteps[j - 1]
7044
- });
7045
- i--;
7046
- j--;
7047
- } else if (i > 0) {
7048
- alignedPairs.push({
7049
- baseline: baselineSteps[i - 1],
7050
- current: null
7051
- });
7052
- i--;
7053
- } else {
7054
- alignedPairs.push({
7055
- baseline: null,
7056
- current: currentSteps[j - 1]
7057
- });
7058
- j--;
7059
- }
7060
- }
7061
- alignedPairs.reverse();
7062
- const maxBaselineStepNum = baselineSteps.length > 0 ? Math.max(...baselineSteps.map((s) => s.step_number)) : -1;
7063
- const alignedSteps = [];
7064
- const stats = {
7065
- totalAlignedSteps: alignedPairs.length,
7066
- same: 0,
7067
- resolved: 0,
7068
- persists: 0,
7069
- regressions: 0,
7070
- stalledInBaseline: 0,
7071
- bypassed: 0,
7072
- newPath: 0
7073
- };
7074
- for (let idx = 0; idx < alignedPairs.length; idx++) {
7075
- const pair = alignedPairs[idx];
7076
- const bStep = pair.baseline;
7077
- const cStep = pair.current;
7078
- const bFindings = bStep ? baselineFindings.filter((f) => findingMatchesStep(f, bStep.step_number)) : [];
7079
- const cFindings = cStep ? currentFindings.filter((f) => findingMatchesStep(f, cStep.step_number)) : [];
7080
- const bProblems = bFindings.filter((f) => isProblemFinding(f.type));
7081
- const cProblems = cFindings.filter((f) => isProblemFinding(f.type));
7082
- let status2 = "same";
7083
- let summary = "Clean";
7084
- if (bStep && cStep) {
7085
- const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
7086
- if (bProblems.length > 0 && cProblems.length === 0) {
7087
- status2 = "resolved";
7088
- stats.resolved++;
7089
- const probDesc = bProblems[0].ui_fact || bProblems[0].title || bProblems[0].id;
7090
- summary = wasStall ? `Previous stall resolved \u2014 journey unblocked (${probDesc})` : `Resolved previous issue: ${probDesc}`;
7091
- } else if (bProblems.length > 0 && cProblems.length > 0) {
7092
- status2 = "persists";
7093
- stats.persists++;
7094
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7095
- summary = `Problem persists: ${probDesc}`;
7096
- } else if (bProblems.length === 0 && cProblems.length > 0) {
7097
- status2 = "regression";
7098
- stats.regressions++;
7099
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7100
- summary = `New issue reported: ${probDesc}`;
7101
- } else {
7102
- status2 = "same";
7103
- stats.same++;
7104
- summary = "Clean";
7105
- }
7106
- } else if (bStep && !cStep) {
7107
- const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
7108
- if (wasStall) {
7109
- status2 = "stalled";
7110
- stats.stalledInBaseline++;
7111
- const probDesc = bProblems[0]?.ui_fact || bProblems[0]?.title || "journey halted";
7112
- summary = `Stalled in baseline: ${probDesc}`;
7113
- } else {
7114
- status2 = "bypassed";
7115
- stats.bypassed++;
7116
- summary = "Bypassed in current run";
7117
- }
7118
- } else if (!bStep && cStep) {
7119
- if (cProblems.length > 0) {
7120
- status2 = "regression";
7121
- stats.regressions++;
7122
- const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
7123
- summary = `New step with issue: ${probDesc}`;
7124
- } else {
7125
- status2 = "new_path";
7126
- stats.newPath++;
7127
- summary = "New path / unblocked progress";
7128
- }
7129
- }
7130
- alignedSteps.push({
7131
- index: idx + 1,
7132
- status: status2,
7133
- baselineStep: bStep,
7134
- currentStep: cStep,
7135
- baselineFindings: bFindings,
7136
- currentFindings: cFindings,
7137
- summary
7138
- });
7139
- }
7140
- return {
7141
- scenario,
7142
- baselineRunId,
7143
- currentRunId,
7144
- steps: alignedSteps,
7145
- stats
7146
- };
7147
- }
7148
- function toStepFindingInfo(f) {
7149
- return {
7150
- id: f.frontmatter.id,
7151
- title: f.frontmatter.title ?? f.slug,
7152
- type: f.frontmatter.type,
7153
- severity: f.frontmatter.severity,
7154
- ui_fact: f.frontmatter.ui_fact,
7155
- consequence: f.frontmatter.consequence,
7156
- canonical_id: f.frontmatter.canonical_id,
7157
- walkthrough_step_id: f.frontmatter.walkthrough_step_id
7158
- };
7159
- }
7160
- function toJourneyStepInfo(s) {
7161
- return {
7162
- step_number: s.step_number,
7163
- action: s.action,
7164
- url: s.url ?? void 0,
7165
- surface: s.surface ?? void 0,
7166
- note: s.note ?? void 0
7167
- };
7168
- }
7169
- function journeyStepRoute(step) {
7170
- return step.currentStep?.url && normalizePlaceKey(step.currentStep.url) || step.baselineStep?.url && normalizePlaceKey(step.baselineStep.url) || step.currentStep?.surface || step.baselineStep?.surface || "\u2014";
7171
- }
7172
- async function computeJourneyDiff(projectRoot, baselineRunId, currentRunId, scenarioSlug) {
7173
- const [bStepsRaw, bFindingsRaw, cStepsRaw, cFindingsRaw] = await Promise.all([
7174
- readStepsForRun(projectRoot, baselineRunId),
7175
- readFindingsForRun(projectRoot, baselineRunId),
7176
- readStepsForRun(projectRoot, currentRunId),
7177
- readFindingsForRun(projectRoot, currentRunId)
7178
- ]);
7179
- const baselineSteps = bStepsRaw.map(toJourneyStepInfo);
7180
- const baselineFindings = bFindingsRaw.map(toStepFindingInfo);
7181
- const currentSteps = cStepsRaw.map(toJourneyStepInfo);
7182
- const currentFindings = cFindingsRaw.map(toStepFindingInfo);
7183
- return alignJourneySteps(scenarioSlug ?? "", baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings);
7184
- }
7185
- function renderJourneyDiffMarkdown(diff) {
7186
- const lines = [
7187
- "### Chronological journey diff",
7188
- "",
7189
- `Baseline: \`${diff.baselineRunId}\` vs Current: \`${diff.currentRunId}\``
7190
- ];
7191
- const parts = [];
7192
- if (diff.stats.same > 0)
7193
- parts.push(`${diff.stats.same} unchanged`);
7194
- if (diff.stats.resolved > 0)
7195
- parts.push(`${diff.stats.resolved} resolved`);
7196
- if (diff.stats.persists > 0)
7197
- parts.push(`${diff.stats.persists} persists`);
7198
- if (diff.stats.regressions > 0)
7199
- parts.push(`${diff.stats.regressions} new issues`);
7200
- if (diff.stats.stalledInBaseline > 0)
7201
- parts.push(`${diff.stats.stalledInBaseline} stalled in baseline`);
7202
- if (diff.stats.bypassed > 0)
7203
- parts.push(`${diff.stats.bypassed} bypassed`);
7204
- if (diff.stats.newPath > 0)
7205
- parts.push(`${diff.stats.newPath} new path steps`);
7206
- if (parts.length > 0) {
7207
- lines.push(`- **Summary**: ${parts.join(", ")}`);
7208
- }
7209
- lines.push("", "| # | Route | Baseline (Run N-1) | Current (Run N) | Status | Notes |", "|---|-------|--------------------|-----------------|--------|-------|");
7210
- for (const step of diff.steps) {
7211
- const route = journeyStepRoute(step);
7212
- const baseAction = step.baselineStep ? `Step ${step.baselineStep.step_number}: ${escapeMarkdownCell(step.baselineStep.action)}` : "\u2014";
7213
- const currAction = step.currentStep ? `Step ${step.currentStep.step_number}: ${escapeMarkdownCell(step.currentStep.action)}` : "\u2014";
7214
- const statusBadge = `\`${step.status}\``;
7215
- const notes = escapeMarkdownCell(step.summary);
7216
- lines.push(`| ${step.index} | \`${escapeMarkdownCell(route)}\` | ${baseAction} | ${currAction} | ${statusBadge} | ${notes} |`);
7217
- }
7218
- return lines.join("\n");
7219
- }
7220
- function escapeMarkdownCell(text) {
7221
- return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
7222
- }
7223
- var PROBLEM_TYPES2, STALL_KEYWORDS;
7224
- var init_journey_diff = __esm({
7225
- "../../packages/local-store/dist/journey-diff.js"() {
6982
+ var DIRECTIONS_DIR, DIRECTION_STATUSES, directionStatusSchema, directionSizeSchema, directionReachSchema, codeRefSchema, directionFrontmatterSchema, union;
6983
+ var init_directions = __esm({
6984
+ "../../packages/local-store/dist/directions.js"() {
7226
6985
  "use strict";
7227
- init_revisit();
7228
- init_walkthrough_canonical2();
7229
- PROBLEM_TYPES2 = /* @__PURE__ */ new Set(["hinders", "missing", "incorrect"]);
7230
- STALL_KEYWORDS = [
7231
- "blocked",
7232
- "unable to proceed",
7233
- "stalled",
7234
- "halted",
7235
- "stuck",
7236
- "cannot proceed",
7237
- "could not proceed",
7238
- "failed to load",
7239
- "timed out"
6986
+ init_atomic();
6987
+ init_markdown();
6988
+ init_paths();
6989
+ DIRECTIONS_DIR = "directions";
6990
+ DIRECTION_STATUSES = [
6991
+ "proposed",
6992
+ "accepted",
6993
+ "done",
6994
+ "rejected"
7240
6995
  ];
6996
+ directionStatusSchema = z25.enum(DIRECTION_STATUSES);
6997
+ directionSizeSchema = z25.enum(["small", "medium", "large"]);
6998
+ directionReachSchema = z25.object({
6999
+ scenario: z25.string().min(1),
7000
+ effect: z25.enum(["helps", "risk"]),
7001
+ reason: z25.string().min(1)
7002
+ });
7003
+ codeRefSchema = z25.object({
7004
+ path: z25.string().min(1),
7005
+ symbol: z25.string().min(1)
7006
+ });
7007
+ directionFrontmatterSchema = z25.object({
7008
+ id: z25.string().uuid(),
7009
+ slug: z25.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
7010
+ title: z25.string().min(1),
7011
+ status: directionStatusSchema.default("proposed"),
7012
+ size: directionSizeSchema,
7013
+ /** Finding ids this direction would resolve. */
7014
+ absorbs: z25.array(z25.string().min(1)).default([]),
7015
+ /** Predicted effect on other scenarios — never evidence. */
7016
+ reach: z25.array(directionReachSchema).default([]),
7017
+ /** Scenarios to re-run once the direction lands. */
7018
+ verify: z25.array(z25.string().min(1)).default([]),
7019
+ builds_on: z25.array(codeRefSchema).default([]),
7020
+ /** Runs (or `project:<timestamp>`) whose analysis proposed it. */
7021
+ seen_in_runs: z25.array(z25.string().min(1)).default([]),
7022
+ advised_by: z25.string().regex(/^advisor@\S+$/),
7023
+ created_at: z25.string(),
7024
+ updated_at: z25.string()
7025
+ });
7026
+ union = (a, b, key) => {
7027
+ const seen = /* @__PURE__ */ new Map();
7028
+ for (const x of [...a, ...b])
7029
+ seen.set(key(x), x);
7030
+ return [...seen.values()];
7031
+ };
7241
7032
  }
7242
7033
  });
7243
7034
 
7244
7035
  // ../../packages/local-store/dist/walkthrough-canonical.js
7245
- import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
7246
- import { promises as fs12 } from "fs";
7247
- import path9 from "path";
7036
+ import { createHash as createHash2, randomUUID as randomUUID6 } from "crypto";
7037
+ import { promises as fs13 } from "fs";
7038
+ import path10 from "path";
7248
7039
  async function readStepsForRun(projectRoot, runId) {
7249
7040
  const runDirPath = runDir(projectRoot, runId);
7250
- const stepsDir = path9.join(runDirPath, "steps");
7041
+ const stepsDir = path10.join(runDirPath, "steps");
7251
7042
  try {
7252
- const names2 = await fs12.readdir(stepsDir);
7043
+ const names2 = await fs13.readdir(stepsDir);
7253
7044
  const jsonFiles = names2.filter((n) => n.endsWith(".json")).sort();
7254
7045
  if (jsonFiles.length > 0) {
7255
7046
  const out2 = [];
7256
7047
  for (const name of jsonFiles) {
7257
7048
  try {
7258
- const raw2 = await fs12.readFile(path9.join(stepsDir, name), "utf8");
7049
+ const raw2 = await fs13.readFile(path10.join(stepsDir, name), "utf8");
7259
7050
  const parsed = JSON.parse(raw2);
7260
7051
  if (parsed.step_number == null) {
7261
7052
  const numFromName = Number.parseInt(name.replace(/\.json$/, ""), 10);
@@ -7269,10 +7060,10 @@ async function readStepsForRun(projectRoot, runId) {
7269
7060
  }
7270
7061
  } catch {
7271
7062
  }
7272
- const stepsPath = path9.join(runDirPath, "steps.jsonl");
7063
+ const stepsPath = path10.join(runDirPath, "steps.jsonl");
7273
7064
  let raw;
7274
7065
  try {
7275
- raw = await fs12.readFile(stepsPath, "utf8");
7066
+ raw = await fs13.readFile(stepsPath, "utf8");
7276
7067
  } catch (err) {
7277
7068
  if (err.code === "ENOENT")
7278
7069
  return [];
@@ -7297,7 +7088,7 @@ async function readFindingsForRun(projectRoot, runId) {
7297
7088
  const findingsDir = entityDirPath(projectRoot, "finding");
7298
7089
  let names2;
7299
7090
  try {
7300
- names2 = await fs12.readdir(findingsDir);
7091
+ names2 = await fs13.readdir(findingsDir);
7301
7092
  } catch (err) {
7302
7093
  if (err.code === "ENOENT")
7303
7094
  return [];
@@ -7307,8 +7098,8 @@ async function readFindingsForRun(projectRoot, runId) {
7307
7098
  for (const name of names2) {
7308
7099
  if (!name.endsWith(".md") || name.startsWith("."))
7309
7100
  continue;
7310
- const filePath = path9.join(findingsDir, name);
7311
- const raw = await fs12.readFile(filePath, "utf8");
7101
+ const filePath = path10.join(findingsDir, name);
7102
+ const raw = await fs13.readFile(filePath, "utf8");
7312
7103
  try {
7313
7104
  const { frontmatter, body } = parseFrontmatter(raw);
7314
7105
  const fm = frontmatter;
@@ -7339,7 +7130,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7339
7130
  const findingsDir = entityDirPath(projectRoot, "finding");
7340
7131
  let allNames;
7341
7132
  try {
7342
- allNames = await fs12.readdir(findingsDir);
7133
+ allNames = await fs13.readdir(findingsDir);
7343
7134
  } catch {
7344
7135
  allNames = [];
7345
7136
  }
@@ -7347,7 +7138,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7347
7138
  if (!name.endsWith(".md") || name.startsWith("."))
7348
7139
  continue;
7349
7140
  try {
7350
- const raw = await fs12.readFile(path9.join(findingsDir, name), "utf8");
7141
+ const raw = await fs13.readFile(path10.join(findingsDir, name), "utf8");
7351
7142
  const { frontmatter } = parseFrontmatter(raw);
7352
7143
  const fm = frontmatter;
7353
7144
  if (!fm.scenario || fm.scenario !== scenarioSlug)
@@ -7358,14 +7149,14 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7358
7149
  if (fm.canonical_id) {
7359
7150
  priorIndex.set(fp, fm.canonical_id);
7360
7151
  } else if (!priorIndex.has(fp)) {
7361
- priorIndex.set(fp, randomUUID5());
7152
+ priorIndex.set(fp, randomUUID6());
7362
7153
  }
7363
7154
  } catch {
7364
7155
  }
7365
7156
  }
7366
7157
  for (const f of currentFindings) {
7367
7158
  const fp = findingFingerprint(f.frontmatter.scenario, f.frontmatter.type, f.frontmatter.ui_fact);
7368
- const canonicalId = f.frontmatter.canonical_id ?? priorIndex.get(fp) ?? randomUUID5();
7159
+ const canonicalId = f.frontmatter.canonical_id ?? priorIndex.get(fp) ?? randomUUID6();
7369
7160
  result.set(f.frontmatter.id, canonicalId);
7370
7161
  if (!f.frontmatter.canonical_id) {
7371
7162
  try {
@@ -7373,9 +7164,9 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7373
7164
  for (const name of allNames) {
7374
7165
  if (!name.endsWith(".md") || name.startsWith("."))
7375
7166
  continue;
7376
- const candidatePath = path9.join(findingsDir, name);
7167
+ const candidatePath = path10.join(findingsDir, name);
7377
7168
  try {
7378
- const candidateRaw = await fs12.readFile(candidatePath, "utf8");
7169
+ const candidateRaw = await fs13.readFile(candidatePath, "utf8");
7379
7170
  const { frontmatter: cFm } = parseFrontmatter(candidateRaw);
7380
7171
  if (cFm.id === f.frontmatter.id) {
7381
7172
  targetPath = candidatePath;
@@ -7386,7 +7177,7 @@ async function resolveCanonicalIds(projectRoot, scenarioSlug, currentRunId, curr
7386
7177
  }
7387
7178
  if (targetPath) {
7388
7179
  await withFileLock(targetPath, async () => {
7389
- const raw = await fs12.readFile(targetPath, "utf8");
7180
+ const raw = await fs13.readFile(targetPath, "utf8");
7390
7181
  const updated = raw.replace(/^(---\n[\s\S]*?)(---\n)/m, `$1canonical_id: ${canonicalId}
7391
7182
  $2`);
7392
7183
  if (updated !== raw) {
@@ -7411,7 +7202,7 @@ function extractRecommendationFromBody(body) {
7411
7202
  }
7412
7203
  async function readAnalysis(projectRoot, runId) {
7413
7204
  try {
7414
- const raw = JSON.parse(await fs12.readFile(path9.join(runDir(projectRoot, runId), "advice.json"), "utf8"));
7205
+ const raw = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, runId), "advice.json"), "utf8"));
7415
7206
  return {
7416
7207
  advisor: `advisor@${raw.advisor?.version ?? "unknown"}`,
7417
7208
  directions: (raw.directions ?? []).map((d) => ({
@@ -7441,9 +7232,13 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7441
7232
  } : null;
7442
7233
  const outlineEntity = scenario ? await readEntity(projectRoot, "outline", scenario.outline) : null;
7443
7234
  const outline = outlineEntity ? {
7235
+ title: outlineEntity.frontmatter.title,
7444
7236
  human_tests: outlineEntity.frontmatter.human_tests ?? []
7445
7237
  } : null;
7238
+ const personaEntity = scenario?.persona ? await readEntity(projectRoot, "persona", scenario.persona).catch(() => null) : null;
7239
+ const personaName = personaEntity?.frontmatter.name ?? null;
7446
7240
  const steps = await readStepsForRun(projectRoot, manifest.run_id);
7241
+ const history = await scenarioRunHistory(projectRoot, manifest.scenario_slug, manifest.run_id);
7447
7242
  const findings2 = await readFindingsForRun(projectRoot, manifest.run_id);
7448
7243
  const analysis = await readAnalysis(projectRoot, manifest.run_id);
7449
7244
  const canonicalSteps = steps.map((s, idx) => {
@@ -7455,7 +7250,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7455
7250
  // Step files record the path relative to the run directory
7456
7251
  // (`screenshots/step-1.png`); older ones recorded the bare file
7457
7252
  // name. Normalize on the file name so the prefix is never doubled.
7458
- screenshot_ref: s.screenshot ? path9.join("screenshots", path9.basename(s.screenshot)) : null,
7253
+ screenshot_ref: s.screenshot ? path10.join("screenshots", path10.basename(s.screenshot)) : null,
7459
7254
  persona_reasoning: s.note,
7460
7255
  grounding: s.grounding ?? null,
7461
7256
  action: s.action,
@@ -7463,9 +7258,15 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7463
7258
  ...typeof s.surface === "string" && s.surface ? { surface: s.surface } : {}
7464
7259
  };
7465
7260
  });
7261
+ const checks = await runChecks(projectRoot, manifest, canonicalSteps, history.previousStartedAt);
7466
7262
  const canonicalMap = await resolveCanonicalIds(projectRoot, manifest.scenario_slug, manifest.run_id, findings2);
7263
+ const stepUrl2 = new Map(canonicalSteps.map((st) => [st.id, st.url ?? null]));
7467
7264
  const canonicalFindings = findings2.map((f) => ({
7468
7265
  id: f.frontmatter.id,
7266
+ // Additive keys (D3): the short title and the page it was seen on,
7267
+ // so reports never fall back to ids or bare step numbers.
7268
+ title: f.frontmatter.title ?? null,
7269
+ page_url: f.frontmatter.page_url ?? (f.frontmatter.walkthrough_step_id ? stepUrl2.get(f.frontmatter.walkthrough_step_id) : null) ?? null,
7469
7270
  canonical_id: canonicalMap.get(f.frontmatter.id) ?? null,
7470
7271
  type: f.frontmatter.type,
7471
7272
  severity: f.frontmatter.severity ?? null,
@@ -7497,7 +7298,10 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7497
7298
  slug: manifest.scenario_slug,
7498
7299
  persona_slug: scenario?.persona ?? "",
7499
7300
  outline_slug: scenario?.outline ?? "",
7500
- brief: scenario?.brief ?? ""
7301
+ brief: scenario?.brief ?? "",
7302
+ ...personaName ? { persona_name: personaName } : {},
7303
+ ...personaEntity?.frontmatter.role ? { persona_role: personaEntity.frontmatter.role } : {},
7304
+ ...outline?.title ? { outline_title: outline.title } : {}
7501
7305
  },
7502
7306
  target: {
7503
7307
  url: manifest.target_url,
@@ -7523,7 +7327,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7523
7327
  steps: canonicalSteps,
7524
7328
  human_tests: humanTests,
7525
7329
  human_test_overall: {
7526
- question: outline ? `Would ${scenario?.persona ?? "the persona"} come back and use this voluntarily?` : "Would the persona come back and use this voluntarily?",
7330
+ question: outline ? `Would ${personaName ?? scenario?.persona ?? "the persona"} come back and use this voluntarily?` : "Would the persona come back and use this voluntarily?",
7527
7331
  answer: null,
7528
7332
  reasoning: null
7529
7333
  },
@@ -7534,21 +7338,65 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7534
7338
  // Consumers MUST preserve unknown keys (per design D3).
7535
7339
  ...inputs.what_worked && inputs.what_worked.length > 0 ? { what_worked: inputs.what_worked } : {},
7536
7340
  ...inputs.open_questions && inputs.open_questions.length > 0 ? { open_questions: inputs.open_questions } : {},
7341
+ ...inputs.setup_notes && inputs.setup_notes.length > 0 ? { setup_notes: inputs.setup_notes } : {},
7537
7342
  ...manifest.blocked ? { blocked: manifest.blocked } : {},
7538
- ...analysis ? { analysis } : {}
7343
+ ...analysis ? { analysis } : {},
7344
+ run_number: history.runNumber,
7345
+ ...checks ? { checks } : {}
7539
7346
  };
7540
7347
  }
7541
- async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, currentFindings) {
7542
- const allRuns = await listRunsInternal(projectRoot);
7543
- let baselineRunId = null;
7544
- let baselineManifest = null;
7545
- for (let i = allRuns.length - 1; i >= 0; i--) {
7546
- const rid = allRuns[i];
7547
- if (rid === currentRunId)
7348
+ async function runChecks(projectRoot, manifest, steps, previousStartedAt) {
7349
+ const places = manifest.revisit ?? [];
7350
+ const fixes = [];
7351
+ if (places.length > 0) {
7352
+ const { entities } = await listEntities(projectRoot, "finding");
7353
+ const byId = new Map(entities.map((e) => [e.frontmatter.id, e.frontmatter]));
7354
+ for (const place of revisitCoverage(places, steps).places) {
7355
+ for (const id of place.finding_ids) {
7356
+ const f = byId.get(id);
7357
+ if (!f || f.type === "helps")
7358
+ continue;
7359
+ fixes.push({
7360
+ canonical_id: f.canonical_id ?? null,
7361
+ title: f.title ?? f.slug,
7362
+ fix: f.triage_note ?? null,
7363
+ page: place.url,
7364
+ reached: place.reached
7365
+ });
7366
+ }
7367
+ }
7368
+ }
7369
+ 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) : [];
7370
+ return fixes.length > 0 || directions.length > 0 ? { fixes, directions } : null;
7371
+ }
7372
+ async function scenarioRunHistory(projectRoot, scenarioSlug, runId) {
7373
+ let runNumber = 1;
7374
+ let previousStartedAt = null;
7375
+ for (const rid of await listRunsInternal(projectRoot)) {
7376
+ if (rid >= runId)
7377
+ continue;
7378
+ try {
7379
+ const m = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, rid), "walkthrough.json"), "utf8"));
7380
+ if (m.scenario_slug === scenarioSlug && m.status === "completed") {
7381
+ runNumber++;
7382
+ previousStartedAt = m.created_at;
7383
+ }
7384
+ } catch {
7385
+ }
7386
+ }
7387
+ return { runNumber, previousStartedAt };
7388
+ }
7389
+ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, currentFindings) {
7390
+ const allRuns = await listRunsInternal(projectRoot);
7391
+ let baselineRunId = null;
7392
+ let baselineManifest = null;
7393
+ for (let i = allRuns.length - 1; i >= 0; i--) {
7394
+ const rid = allRuns[i];
7395
+ if (rid === currentRunId)
7548
7396
  continue;
7549
7397
  try {
7550
- const mPath = path9.join(runDir(projectRoot, rid), "walkthrough.json");
7551
- const raw = await fs12.readFile(mPath, "utf8");
7398
+ const mPath = path10.join(runDir(projectRoot, rid), "walkthrough.json");
7399
+ const raw = await fs13.readFile(mPath, "utf8");
7552
7400
  const m = JSON.parse(raw);
7553
7401
  if (m.scenario_slug === scenarioSlug && m.status === "completed" && !m.blocked) {
7554
7402
  baselineRunId = rid;
@@ -7570,15 +7418,21 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7570
7418
  const uncertain = [];
7571
7419
  const matches = await readMatches(projectRoot, currentRunId);
7572
7420
  const recheckedFixed = new Set((matches?.rechecked_fixed ?? []).map((r) => r.canonical_id));
7421
+ const recheckOf = new Map((matches?.rechecked_fixed ?? []).map((r) => [r.canonical_id, r]));
7422
+ const matchNote = new Map((matches?.matches ?? []).filter((m) => m.canonical_id && m.reasoning).map((m) => [m.canonical_id, m.reasoning]));
7423
+ const recurrence = new Map(currentFindings.filter((f) => f.canonical_id).map((f) => [f.canonical_id, f]));
7573
7424
  if (recheckedFixed.size > 0) {
7574
7425
  const prior = await priorCanonicals(projectRoot, scenarioSlug, currentRunId);
7575
7426
  for (const p of prior) {
7576
7427
  if (!recheckedFixed.has(p.canonical_id))
7577
7428
  continue;
7429
+ const recheck = recheckOf.get(p.canonical_id);
7578
7430
  resolved.push({
7579
7431
  canonical_id: p.canonical_id,
7580
7432
  title: p.title,
7581
- type: p.type
7433
+ type: p.type,
7434
+ ...recheck?.reasoning ? { note: recheck.reasoning } : {},
7435
+ ...recheck ? { step_ids: recheck.step_ids } : {}
7582
7436
  });
7583
7437
  await setCanonicalFixStatus(projectRoot, scenarioSlug, p.canonical_id, "verified_resolved");
7584
7438
  }
@@ -7595,11 +7449,20 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7595
7449
  });
7596
7450
  continue;
7597
7451
  }
7452
+ const fixedBefore = bf.frontmatter.triage_status === "addressed";
7598
7453
  if (currentCanonicalIds.has(cid)) {
7454
+ const note = matchNote.get(cid);
7599
7455
  persists.push({
7600
7456
  canonical_id: cid,
7601
7457
  title: bf.frontmatter.title ?? bf.frontmatter.slug,
7602
- type: bf.frontmatter.type
7458
+ type: bf.frontmatter.type,
7459
+ ...note ? { note } : {},
7460
+ step_ids: recurrence.get(cid)?.linked_step_ids ?? [],
7461
+ ...fixedBefore ? { fixed_before: true } : {},
7462
+ ...bf.frontmatter.severity && recurrence.get(cid)?.severity ? {
7463
+ severity_before: bf.frontmatter.severity,
7464
+ severity_now: recurrence.get(cid)?.severity
7465
+ } : {}
7603
7466
  });
7604
7467
  await updateFixStatus(projectRoot, bf.frontmatter.slug, "verified_persists");
7605
7468
  } else if (recheckedFixed.has(cid)) {
@@ -7607,7 +7470,8 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7607
7470
  notObserved.push({
7608
7471
  canonical_id: cid,
7609
7472
  title: bf.frontmatter.title ?? bf.frontmatter.slug,
7610
- type: bf.frontmatter.type
7473
+ type: bf.frontmatter.type,
7474
+ ...fixedBefore ? { fixed_before: true } : {}
7611
7475
  });
7612
7476
  }
7613
7477
  }
@@ -7629,9 +7493,9 @@ async function compareWithBaseline(projectRoot, scenarioSlug, currentRunId, curr
7629
7493
  async function updateFixStatus(projectRoot, slug, status2) {
7630
7494
  try {
7631
7495
  const findingsDir = entityDirPath(projectRoot, "finding");
7632
- const filePath = path9.join(findingsDir, `${slug}.md`);
7496
+ const filePath = path10.join(findingsDir, `${slug}.md`);
7633
7497
  await withFileLock(filePath, async () => {
7634
- const raw = await fs12.readFile(filePath, "utf8");
7498
+ const raw = await fs13.readFile(filePath, "utf8");
7635
7499
  const updated = raw.replace(/^(fix_status:\s*).*$/m, `$1${status2}`);
7636
7500
  if (updated !== raw) {
7637
7501
  await atomicWriteFile(filePath, updated);
@@ -7645,210 +7509,19 @@ fix_status: ${status2}`);
7645
7509
  }
7646
7510
  }
7647
7511
  async function listRunsInternal(projectRoot) {
7648
- const dir = path9.join(projectRoot, "iamu", ".runs");
7512
+ const dir = path10.join(projectRoot, "iamu", ".runs");
7649
7513
  try {
7650
- const names2 = await fs12.readdir(dir);
7514
+ const names2 = await fs13.readdir(dir);
7651
7515
  return names2.filter((n) => !n.startsWith(".")).sort();
7652
7516
  } catch {
7653
7517
  return [];
7654
7518
  }
7655
7519
  }
7656
- function renderCanonicalMarkdown(canonical) {
7657
- const lines = [
7658
- "<!-- GENERATED: do not edit by hand. Regen via complete_walkthrough or local-store renderer. -->",
7659
- "",
7660
- `# Walkthrough report: ${canonical.scenario.slug}`,
7661
- "",
7662
- ...canonical.blocked ? [
7663
- "> **Blocked \u2014 the walk could not start as it was set up.**",
7664
- `> ${canonical.blocked.reason}`,
7665
- ">",
7666
- "> 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.",
7667
- ""
7668
- ] : [],
7669
- `- Run ID: ${canonical.run_id}`,
7670
- `- Target URL: ${canonical.target.url}`
7671
- ];
7672
- if (canonical.environment) {
7673
- lines.push(`- Environment: ${canonical.environment}`);
7674
- }
7675
- if (canonical.build) {
7676
- const commit = canonical.build.git_sha?.slice(0, 7);
7677
- const buildDetails = [
7678
- canonical.build.label,
7679
- canonical.build.git_branch,
7680
- canonical.build.git_dirty && canonical.build.worktree_hash ? `${commit ?? "HEAD"} + uncommitted changes (${canonical.build.worktree_hash.slice(0, 6)}\u2026)` : commit,
7681
- canonical.build.ci?.provider
7682
- ].filter(Boolean).join(" \xB7 ");
7683
- if (buildDetails) {
7684
- lines.push(`- Build: ${buildDetails}`);
7685
- }
7686
- }
7687
- if (canonical.viewport) {
7688
- lines.push(`- Viewport: ${canonical.viewport.device} (${canonical.viewport.width}\xD7${canonical.viewport.height})`);
7689
- }
7690
- 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}`);
7691
- const models = canonical.models;
7692
- if (models) {
7693
- const parts = [
7694
- models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
7695
- models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
7696
- models.matcher ? `Matcher: ${models.matcher}` : ""
7697
- ].filter(Boolean);
7698
- if (parts.length > 0) {
7699
- lines.push(`- Models: ${parts.join(" \xB7 ")}`);
7700
- }
7701
- if (typeof models.velocity_sec_per_step === "number") {
7702
- lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
7703
- }
7704
- }
7705
- const what = canonical.what_worked;
7706
- if (what && what.length > 0) {
7707
- lines.push("", "## What worked");
7708
- for (const w of what)
7709
- lines.push(`- ${w}`);
7710
- }
7711
- const open2 = canonical.open_questions;
7712
- if (open2 && open2.length > 0) {
7713
- lines.push("", "## Open questions");
7714
- for (const q of open2)
7715
- lines.push(`- ${q}`);
7716
- }
7717
- const verification = canonical.verification;
7718
- if (verification) {
7719
- lines.push("", "## Since this scenario's last run", "", `Baseline: ${verification.baseline_run_id}`);
7720
- const group = (label, items, note = "") => {
7721
- if (items.length === 0)
7722
- return;
7723
- lines.push("", `**${label}** (${items.length})${note}`);
7724
- for (const i of items)
7725
- lines.push(`- ${i.title}`);
7726
- };
7727
- group("Resolved", verification.resolved, " \u2014 re-checked in this run");
7728
- group("Persists", verification.persists);
7729
- group("New since that run", verification.new_findings);
7730
- group("Not seen this run", verification.not_observed, " \u2014 not evidence of a fix");
7731
- group("Uncertain", verification.uncertain);
7732
- }
7733
- const diff = canonical.journey_diff;
7734
- if (diff) {
7735
- lines.push("", renderJourneyDiffMarkdown(diff));
7736
- }
7737
- lines.push("", "## Findings");
7738
- if (canonical.findings.length === 0) {
7739
- lines.push("", "_No findings recorded._");
7740
- } else {
7741
- const severityRank = ["critical", "major", "minor", "observation"];
7742
- const rank = (value, order) => {
7743
- const i = order.indexOf(value ?? "");
7744
- return i === -1 ? order.length : i;
7745
- };
7746
- const typeRank = ["incorrect", "hinders", "missing", "irrelevant", "helps"];
7747
- const ordered = [...canonical.findings].sort((a, b) => rank(a.severity, severityRank) - rank(b.severity, severityRank) || rank(a.type, typeRank) - rank(b.type, typeRank));
7748
- for (const f of ordered) {
7749
- lines.push("", `### ${f.type} / ${f.severity ?? "unranked"} \u2014 ${f.id}`);
7750
- if (f.ui_fact === null || f.consequence === null) {
7751
- lines.push("", "> _Legacy finding: predates the UI-fact / consequence depth gate._");
7752
- }
7753
- if (f.ui_fact)
7754
- lines.push("", "**UI fact.** ", f.ui_fact);
7755
- if (f.consequence)
7756
- lines.push("", "**Consequence.** ", f.consequence);
7757
- if (f.recommendation)
7758
- lines.push("", "**Recommendation.** ", f.recommendation);
7759
- for (const stepId of f.linked_step_ids) {
7760
- const step = canonical.steps.find((st) => st.id === stepId);
7761
- const label = step ? `Step ${step.step_number}` : stepId;
7762
- lines.push("", step?.screenshot_ref ? `**Where.** ${label}
7763
-
7764
- ![${label}](${step.screenshot_ref})` : `**Where.** ${label}`);
7765
- }
7766
- if (f.linked_human_test_id) {
7767
- lines.push("", `Human test: ${f.linked_human_test_id}`);
7768
- }
7769
- }
7770
- }
7771
- const analysis = canonical.analysis;
7772
- if (analysis && analysis.directions.length > 0) {
7773
- lines.push("", "## Directions", "", `_Structural changes proposed by ${analysis.advisor} after the walk. Reach is a prediction until the listed scenarios are re-run._`);
7774
- for (const d of analysis.directions) {
7775
- lines.push("", `### ${d.title} (${d.size})`, "", d.change);
7776
- lines.push("", `Absorbs ${d.absorbs.length} finding${d.absorbs.length === 1 ? "" : "s"}.`);
7777
- const helps = d.reach.filter((r) => r.effect === "helps");
7778
- const risks = d.reach.filter((r) => r.effect === "risk");
7779
- for (const r of helps)
7780
- lines.push(`- Likely helps ${r.scenario}: ${r.reason}`);
7781
- for (const r of risks)
7782
- lines.push(`- Could hurt ${r.scenario}: ${r.reason}`);
7783
- if (d.verify.length > 0)
7784
- lines.push("", `Re-run to verify: ${d.verify.join(", ")}`);
7785
- }
7786
- }
7787
- if (analysis && analysis.observations.length > 0) {
7788
- lines.push("", "## Also worth knowing", "");
7789
- for (const o of analysis.observations) {
7790
- const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
7791
- lines.push(`- **${label}: ${o.title}.** ${o.detail}`);
7792
- if (o.suggested_scenario) {
7793
- lines.push(` Suggested scenario: ${o.suggested_scenario}`);
7794
- }
7795
- }
7796
- }
7797
- lines.push("", "## Human tests");
7798
- if (canonical.human_tests.length === 0) {
7799
- lines.push("", "_No human tests on the outline._");
7800
- } else {
7801
- for (const ht of canonical.human_tests) {
7802
- lines.push("", `- **${ht.question}**`);
7803
- if (ht.verdict === null) {
7804
- if (canonical.blocked) {
7805
- lines.push(` - Verdict: _skipped (walk blocked)_`);
7806
- } else {
7807
- lines.push(` - Verdict: _pending judge_ (walker does not self-grade)`);
7808
- }
7809
- } else {
7810
- lines.push(` - Verdict: **${ht.verdict}**`);
7811
- if (ht.reasoning)
7812
- lines.push(` - Reasoning: ${ht.reasoning}`);
7813
- }
7814
- }
7815
- }
7816
- lines.push("", "## Human test overall", "", `> ${canonical.human_test_overall.question}`);
7817
- if (canonical.human_test_overall.answer === null) {
7818
- if (canonical.blocked) {
7819
- lines.push("", "_Skipped (walk blocked)._");
7820
- } else {
7821
- lines.push("", "_Pending judge stage._");
7822
- }
7823
- } else {
7824
- lines.push("", canonical.human_test_overall.answer);
7825
- if (canonical.human_test_overall.reasoning)
7826
- lines.push("", canonical.human_test_overall.reasoning);
7827
- }
7828
- lines.push("", "## Steps");
7829
- if (canonical.steps.length === 0) {
7830
- lines.push("", "_No steps recorded._");
7831
- } else {
7832
- for (const s of canonical.steps) {
7833
- lines.push("", `### ${s.step_number}. ${s.screen_title ?? "(untitled)"} \u2014 ${s.id}`);
7834
- const where = [s.surface, s.url].filter(Boolean).join(" \u2014 ");
7835
- if (where)
7836
- lines.push("", `Page: ${where}`);
7837
- if (s.persona_reasoning)
7838
- lines.push("", `> ${s.persona_reasoning.replace(/\n/g, "\n> ")}`);
7839
- if (s.grounding)
7840
- lines.push("", `Grounding: ${s.grounding}`);
7841
- if (s.screenshot_ref)
7842
- lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
7843
- }
7844
- }
7845
- return `${lines.join("\n")}
7846
- `;
7847
- }
7848
7520
  var init_walkthrough_canonical2 = __esm({
7849
7521
  "../../packages/local-store/dist/walkthrough-canonical.js"() {
7850
7522
  "use strict";
7851
- init_journey_diff();
7523
+ init_revisit();
7524
+ init_directions();
7852
7525
  init_atomic();
7853
7526
  init_file_lock();
7854
7527
  init_dist();
@@ -7870,8 +7543,8 @@ __export(walkthrough_mirror_exports, {
7870
7543
  walkthroughBundleSchema: () => walkthroughBundleSchema
7871
7544
  });
7872
7545
  import { createHash as createHash3 } from "crypto";
7873
- import { promises as fs13 } from "fs";
7874
- import path10 from "path";
7546
+ import { promises as fs14 } from "fs";
7547
+ import path11 from "path";
7875
7548
  function toCloudRunUuid(runId) {
7876
7549
  if (UUID_RE.test(runId))
7877
7550
  return runId.toLowerCase();
@@ -7921,18 +7594,18 @@ function buildPushUrl(cloudApiUrl, cloudProjectId) {
7921
7594
  return `${base}/api/projects/${cloudProjectId}/walkthroughs/push`;
7922
7595
  }
7923
7596
  async function readLocalManifest(projectRoot, runId) {
7924
- const p = path10.join(runDir(projectRoot, runId), "walkthrough.json");
7597
+ const p = path11.join(runDir(projectRoot, runId), "walkthrough.json");
7925
7598
  try {
7926
- const raw = await fs13.readFile(p, "utf8");
7599
+ const raw = await fs14.readFile(p, "utf8");
7927
7600
  return JSON.parse(raw);
7928
7601
  } catch {
7929
7602
  return null;
7930
7603
  }
7931
7604
  }
7932
7605
  async function readLocalReport(projectRoot, runId) {
7933
- const p = path10.join(runDir(projectRoot, runId), "report.md");
7606
+ const p = path11.join(runDir(projectRoot, runId), "report.md");
7934
7607
  try {
7935
- return await fs13.readFile(p, "utf8");
7608
+ return await fs14.readFile(p, "utf8");
7936
7609
  } catch (err) {
7937
7610
  if (err.code === "ENOENT")
7938
7611
  return "";
@@ -7940,10 +7613,10 @@ async function readLocalReport(projectRoot, runId) {
7940
7613
  }
7941
7614
  }
7942
7615
  async function readScreenshotBytes(projectRoot, runId) {
7943
- const dir = path10.join(runDir(projectRoot, runId), SCREENSHOTS_DIR);
7616
+ const dir = path11.join(runDir(projectRoot, runId), SCREENSHOTS_DIR);
7944
7617
  let names2;
7945
7618
  try {
7946
- names2 = await fs13.readdir(dir);
7619
+ names2 = await fs14.readdir(dir);
7947
7620
  } catch (err) {
7948
7621
  if (err.code === "ENOENT")
7949
7622
  return /* @__PURE__ */ new Map();
@@ -7959,15 +7632,15 @@ async function readScreenshotBytes(projectRoot, runId) {
7959
7632
  const contentType = EXT_TO_CONTENT_TYPE[ext === ".jpeg" ? ".jpeg" : ext];
7960
7633
  if (!contentType)
7961
7634
  continue;
7962
- const bytes = await fs13.readFile(path10.join(dir, name));
7635
+ const bytes = await fs14.readFile(path11.join(dir, name));
7963
7636
  out.set(seq2, { bytes, filename: name, contentType });
7964
7637
  }
7965
7638
  return out;
7966
7639
  }
7967
7640
  async function readVerdictsJson(projectRoot, runId) {
7968
- const filePath = path10.join(runDir(projectRoot, runId), "verdicts.json");
7641
+ const filePath = path11.join(runDir(projectRoot, runId), "verdicts.json");
7969
7642
  try {
7970
- const raw = await fs13.readFile(filePath, "utf-8");
7643
+ const raw = await fs14.readFile(filePath, "utf-8");
7971
7644
  return JSON.parse(raw);
7972
7645
  } catch {
7973
7646
  return null;
@@ -7976,9 +7649,9 @@ async function readVerdictsJson(projectRoot, runId) {
7976
7649
  async function readProvenance(projectRoot, runId, verdicts) {
7977
7650
  if (verdicts?.provenance)
7978
7651
  return verdicts.provenance;
7979
- const filePath = path10.join(runDir(projectRoot, runId), "canonical.json");
7652
+ const filePath = path11.join(runDir(projectRoot, runId), "canonical.json");
7980
7653
  try {
7981
- const raw = await fs13.readFile(filePath, "utf-8");
7654
+ const raw = await fs14.readFile(filePath, "utf-8");
7982
7655
  const data = JSON.parse(raw);
7983
7656
  return data.provenance ?? null;
7984
7657
  } catch {
@@ -7987,7 +7660,7 @@ async function readProvenance(projectRoot, runId, verdicts) {
7987
7660
  }
7988
7661
  async function readCanonicalExtras(projectRoot, runId) {
7989
7662
  try {
7990
- const data = JSON.parse(await fs13.readFile(path10.join(runDir(projectRoot, runId), "canonical.json"), "utf-8"));
7663
+ const data = JSON.parse(await fs14.readFile(path11.join(runDir(projectRoot, runId), "canonical.json"), "utf-8"));
7991
7664
  const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
7992
7665
  const verification = bundleVerificationSchema.safeParse(data.verification);
7993
7666
  let blocked;
@@ -8736,7 +8409,7 @@ var init_refs = __esm({
8736
8409
  });
8737
8410
 
8738
8411
  // ../../packages/local-store/dist/sync/types.js
8739
- import { z as z25 } from "zod";
8412
+ import { z as z26 } from "zod";
8740
8413
  function toRef(d) {
8741
8414
  return { type: d.type, id: d.id, label: d.label };
8742
8415
  }
@@ -8801,17 +8474,17 @@ var init_types = __esm({
8801
8474
  DEFAULT_TIMEOUT_MS4 = 3e4;
8802
8475
  MAX_BATCH_BYTES = 6 * 1024 * 1024;
8803
8476
  BASE_HASH_BYTES = 96;
8804
- importConflictSchema = z25.object({
8805
- type: z25.string(),
8806
- id: z25.string(),
8807
- base_hash: z25.string().nullish(),
8808
- cloud_hash: z25.string(),
8809
- cloud_updated_at: z25.string().nullish(),
8810
- cloud_changed_by: z25.string().nullish()
8477
+ importConflictSchema = z26.object({
8478
+ type: z26.string(),
8479
+ id: z26.string(),
8480
+ base_hash: z26.string().nullish(),
8481
+ cloud_hash: z26.string(),
8482
+ cloud_updated_at: z26.string().nullish(),
8483
+ cloud_changed_by: z26.string().nullish()
8811
8484
  });
8812
- importResponseSchema = z25.object({
8813
- imported: z25.number().int().min(0),
8814
- conflicts: z25.array(importConflictSchema).optional()
8485
+ importResponseSchema = z26.object({
8486
+ imported: z26.number().int().min(0),
8487
+ conflicts: z26.array(importConflictSchema).optional()
8815
8488
  });
8816
8489
  }
8817
8490
  });
@@ -9062,7 +8735,7 @@ var init_push = __esm({
9062
8735
  });
9063
8736
 
9064
8737
  // ../../packages/local-store/dist/sync/context.js
9065
- import { promises as fs14 } from "fs";
8738
+ import { promises as fs15 } from "fs";
9066
8739
  async function syncContext(target) {
9067
8740
  const { projectRoot } = target;
9068
8741
  const summary = zeroContext();
@@ -9070,7 +8743,7 @@ async function syncContext(target) {
9070
8743
  const conflicts = [];
9071
8744
  let raw;
9072
8745
  try {
9073
- raw = await fs14.readFile(contextPath(projectRoot), "utf8");
8746
+ raw = await fs15.readFile(contextPath(projectRoot), "utf8");
9074
8747
  } catch (err) {
9075
8748
  if (err.code === "ENOENT")
9076
8749
  return { summary, errors, conflicts };
@@ -9151,7 +8824,7 @@ var init_context3 = __esm({
9151
8824
  // ../../packages/local-store/dist/git-branches.js
9152
8825
  import { execFile } from "child_process";
9153
8826
  import { realpathSync } from "fs";
9154
- import path11 from "path";
8827
+ import path12 from "path";
9155
8828
  function defaultGitExec(cwd, args, input) {
9156
8829
  return new Promise((resolve4) => {
9157
8830
  let settled = false;
@@ -9210,7 +8883,7 @@ async function otherBranchesWithFiles(cwd, absPaths, opts = {}) {
9210
8883
  return out;
9211
8884
  return null;
9212
8885
  }
9213
- const top = path11.resolve(topRes.stdout.trim());
8886
+ const top = path12.resolve(topRes.stdout.trim());
9214
8887
  if (!top)
9215
8888
  return out;
9216
8889
  const refsRes = await git2(cwd, [
@@ -9248,8 +8921,8 @@ async function otherBranchesWithFiles(cwd, absPaths, opts = {}) {
9248
8921
  } catch {
9249
8922
  }
9250
8923
  const rel = absPaths.map((abs) => {
9251
- const fromCwd = path11.relative(cwd, abs);
9252
- return path11.relative(top, path11.resolve(base, fromCwd)).split(path11.sep).join("/");
8924
+ const fromCwd = path12.relative(cwd, abs);
8925
+ return path12.relative(top, path12.resolve(base, fromCwd)).split(path12.sep).join("/");
9253
8926
  });
9254
8927
  const queries = [];
9255
8928
  const lines = [];
@@ -9287,8 +8960,8 @@ var init_git_branches = __esm({
9287
8960
  });
9288
8961
 
9289
8962
  // ../../packages/local-store/dist/deletions.js
9290
- import { promises as fs15 } from "fs";
9291
- import path12 from "path";
8963
+ import { promises as fs16 } from "fs";
8964
+ import path13 from "path";
9292
8965
  function describeDeletionCount(type2, n) {
9293
8966
  const nouns = {
9294
8967
  persona: ["persona", "personas"],
@@ -9315,7 +8988,7 @@ function summarizeDeletions(items) {
9315
8988
  }
9316
8989
  async function readPushedEntities(projectRoot) {
9317
8990
  try {
9318
- const raw = await fs15.readFile(pushedEntitiesPath(projectRoot), "utf8");
8991
+ const raw = await fs16.readFile(pushedEntitiesPath(projectRoot), "utf8");
9319
8992
  const parsed = JSON.parse(raw);
9320
8993
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9321
8994
  return {};
@@ -9354,11 +9027,11 @@ async function rememberPushedEntities(projectRoot, known) {
9354
9027
  }
9355
9028
  async function readLenient(projectRoot, relPath) {
9356
9029
  try {
9357
- const raw = await fs15.readFile(path12.join(projectRoot, relPath), "utf8");
9030
+ const raw = await fs16.readFile(path13.join(projectRoot, relPath), "utf8");
9358
9031
  const fm = parseFrontmatter(raw).frontmatter;
9359
9032
  if (typeof fm.id !== "string" || fm.id.length === 0)
9360
9033
  return null;
9361
- const slug = typeof fm.slug === "string" ? fm.slug : path12.basename(relPath, ".md");
9034
+ const slug = typeof fm.slug === "string" ? fm.slug : path13.basename(relPath, ".md");
9362
9035
  const run = typeof fm.run === "string" ? fm.run : void 0;
9363
9036
  return { id: fm.id, slug, run };
9364
9037
  } catch {
@@ -9780,7 +9453,7 @@ var init_deletions2 = __esm({
9780
9453
  });
9781
9454
 
9782
9455
  // ../../packages/local-store/dist/sync/scan.js
9783
- import { promises as fs16 } from "fs";
9456
+ import { promises as fs17 } from "fs";
9784
9457
  function blockedRef(refs, blocked) {
9785
9458
  if (!refs)
9786
9459
  return null;
@@ -9876,7 +9549,7 @@ async function scanPending(projectRoot) {
9876
9549
  continue;
9877
9550
  let bytes;
9878
9551
  try {
9879
- bytes = await fs16.readFile(entity.absPath, "utf8");
9552
+ bytes = await fs17.readFile(entity.absPath, "utf8");
9880
9553
  } catch {
9881
9554
  continue;
9882
9555
  }
@@ -9937,7 +9610,7 @@ var init_scan = __esm({
9937
9610
  });
9938
9611
 
9939
9612
  // ../../packages/local-store/dist/sync/walkthroughs.js
9940
- import { promises as fs17 } from "fs";
9613
+ import { promises as fs18 } from "fs";
9941
9614
  import nodePath from "path";
9942
9615
  async function syncWalkthroughs(projectRoot, deps, unpushableScenarios = /* @__PURE__ */ new Set()) {
9943
9616
  const summary = zeroWalkthroughs();
@@ -9965,7 +9638,7 @@ async function syncWalkthroughs(projectRoot, deps, unpushableScenarios = /* @__P
9965
9638
  const manifestPath = nodePath.join(runDir(projectRoot, runId), "walkthrough.json");
9966
9639
  let manifestRaw;
9967
9640
  try {
9968
- manifestRaw = await fs17.readFile(manifestPath, "utf8");
9641
+ manifestRaw = await fs18.readFile(manifestPath, "utf8");
9969
9642
  } catch {
9970
9643
  continue;
9971
9644
  }
@@ -10304,8 +9977,8 @@ var init_disconnect = __esm({
10304
9977
  });
10305
9978
 
10306
9979
  // ../../packages/local-store/dist/status.js
10307
- import { promises as fs18 } from "fs";
10308
- import path13 from "path";
9980
+ import { promises as fs19 } from "fs";
9981
+ import path14 from "path";
10309
9982
  async function countType(projectRoot, type2) {
10310
9983
  const result = await listEntities(projectRoot, type2);
10311
9984
  const contentHashes = /* @__PURE__ */ new Map();
@@ -10315,7 +9988,7 @@ async function countType(projectRoot, type2) {
10315
9988
  if (typeof id !== "string" || typeof slug !== "string")
10316
9989
  continue;
10317
9990
  try {
10318
- const raw = await fs18.readFile(entity.absPath, "utf8");
9991
+ const raw = await fs19.readFile(entity.absPath, "utf8");
10319
9992
  contentHashes.set(id, hashNormalizedContent(raw));
10320
9993
  } catch {
10321
9994
  }
@@ -10409,7 +10082,7 @@ async function countContext(projectRoot, opts = {
10409
10082
  return base;
10410
10083
  let pending = true;
10411
10084
  try {
10412
- const bytes = await fs18.readFile(contextPath(projectRoot), "utf8");
10085
+ const bytes = await fs19.readFile(contextPath(projectRoot), "utf8");
10413
10086
  const hash = hashNormalizedContent(bytes);
10414
10087
  pending = opts.lastPush["context:root"] !== hash;
10415
10088
  } catch {
@@ -10428,10 +10101,10 @@ async function countWalkthroughs(projectRoot, opts = { cloudLinked: false, lastP
10428
10101
  let pushed = 0;
10429
10102
  let pending = 0;
10430
10103
  for (const runId of valid) {
10431
- const manifestPath = path13.join(projectRoot, "iamu", ".runs", runId, "walkthrough.json");
10104
+ const manifestPath = path14.join(projectRoot, "iamu", ".runs", runId, "walkthrough.json");
10432
10105
  let completed = false;
10433
10106
  try {
10434
- const raw = await fs18.readFile(manifestPath, "utf8");
10107
+ const raw = await fs19.readFile(manifestPath, "utf8");
10435
10108
  const manifest = JSON.parse(raw);
10436
10109
  completed = manifest.status === "completed";
10437
10110
  } catch {
@@ -10451,7 +10124,7 @@ async function getProjectStatus(projectRoot, opts = {}) {
10451
10124
  const config = await readConfig(projectRoot);
10452
10125
  const iamuPath = iamuDir(projectRoot);
10453
10126
  try {
10454
- await fs18.access(iamuPath);
10127
+ await fs19.access(iamuPath);
10455
10128
  } catch {
10456
10129
  throw new Error(`status: iamu/ directory missing at ${iamuPath}`);
10457
10130
  }
@@ -10633,22 +10306,22 @@ var init_status = __esm({
10633
10306
  });
10634
10307
 
10635
10308
  // ../../packages/local-store/dist/prune.js
10636
- import { promises as fs19 } from "fs";
10637
- import path14 from "path";
10309
+ import { promises as fs20 } from "fs";
10310
+ import path15 from "path";
10638
10311
  async function getDirectoryStats(dirPath) {
10639
10312
  let bytes = 0;
10640
10313
  let count = 0;
10641
10314
  try {
10642
- const entries = await fs19.readdir(dirPath, { withFileTypes: true });
10315
+ const entries = await fs20.readdir(dirPath, { withFileTypes: true });
10643
10316
  for (const entry of entries) {
10644
- const fullPath = path14.join(dirPath, entry.name);
10317
+ const fullPath = path15.join(dirPath, entry.name);
10645
10318
  if (entry.isDirectory()) {
10646
10319
  const sub = await getDirectoryStats(fullPath);
10647
10320
  bytes += sub.bytes;
10648
10321
  count += sub.count;
10649
10322
  } else if (entry.isFile()) {
10650
10323
  try {
10651
- const s = await fs19.stat(fullPath);
10324
+ const s = await fs20.stat(fullPath);
10652
10325
  bytes += s.size;
10653
10326
  count += 1;
10654
10327
  } catch {
@@ -10664,12 +10337,12 @@ async function getScreenshotsStats(screenshotsDir) {
10664
10337
  let count = 0;
10665
10338
  const paths = [];
10666
10339
  try {
10667
- const entries = await fs19.readdir(screenshotsDir, { withFileTypes: true });
10340
+ const entries = await fs20.readdir(screenshotsDir, { withFileTypes: true });
10668
10341
  for (const entry of entries) {
10669
10342
  if (entry.isFile() && (entry.name.endsWith(".png") || entry.name.endsWith(".jpg") || entry.name.endsWith(".jpeg") || entry.name.endsWith(".webp"))) {
10670
- const fullPath = path14.join(screenshotsDir, entry.name);
10343
+ const fullPath = path15.join(screenshotsDir, entry.name);
10671
10344
  try {
10672
- const s = await fs19.stat(fullPath);
10345
+ const s = await fs20.stat(fullPath);
10673
10346
  bytes += s.size;
10674
10347
  count += 1;
10675
10348
  paths.push(fullPath);
@@ -10689,8 +10362,8 @@ async function pruneRuns(projectRoot, opts = {}) {
10689
10362
  let timestamp2 = null;
10690
10363
  let scenarioSlug;
10691
10364
  try {
10692
- const manifestPath = path14.join(dirPath, "walkthrough.json");
10693
- const raw = await fs19.readFile(manifestPath, "utf-8");
10365
+ const manifestPath = path15.join(dirPath, "walkthrough.json");
10366
+ const raw = await fs20.readFile(manifestPath, "utf-8");
10694
10367
  const parsed = JSON.parse(raw);
10695
10368
  if (parsed.scenario_slug)
10696
10369
  scenarioSlug = parsed.scenario_slug;
@@ -10702,7 +10375,7 @@ async function pruneRuns(projectRoot, opts = {}) {
10702
10375
  }
10703
10376
  if (!timestamp2 || Number.isNaN(timestamp2.getTime())) {
10704
10377
  try {
10705
- const st = await fs19.stat(dirPath);
10378
+ const st = await fs20.stat(dirPath);
10706
10379
  timestamp2 = st.mtime;
10707
10380
  } catch {
10708
10381
  timestamp2 = /* @__PURE__ */ new Date();
@@ -10737,12 +10410,12 @@ async function pruneRuns(projectRoot, opts = {}) {
10737
10410
  let totalScreenshotsDeleted = 0;
10738
10411
  let totalBytesReclaimed = 0;
10739
10412
  for (const c of candidates) {
10740
- const screenshotsDir = path14.join(c.dirPath, "screenshots");
10413
+ const screenshotsDir = path15.join(c.dirPath, "screenshots");
10741
10414
  const shotStats = await getScreenshotsStats(screenshotsDir);
10742
10415
  if (opts.screenshotsOnly) {
10743
10416
  if (!opts.dryRun) {
10744
10417
  for (const shotPath of shotStats.paths) {
10745
- await fs19.unlink(shotPath).catch(() => {
10418
+ await fs20.unlink(shotPath).catch(() => {
10746
10419
  });
10747
10420
  }
10748
10421
  }
@@ -10759,7 +10432,7 @@ async function pruneRuns(projectRoot, opts = {}) {
10759
10432
  } else {
10760
10433
  const dirStats = await getDirectoryStats(c.dirPath);
10761
10434
  if (!opts.dryRun) {
10762
- await fs19.rm(c.dirPath, { recursive: true, force: true });
10435
+ await fs20.rm(c.dirPath, { recursive: true, force: true });
10763
10436
  }
10764
10437
  prunedRuns.push({
10765
10438
  runId: c.runId,
@@ -10791,8 +10464,8 @@ var init_prune = __esm({
10791
10464
 
10792
10465
  // ../../packages/local-store/dist/explore-map.js
10793
10466
  import { randomBytes as randomBytes2 } from "crypto";
10794
- import { promises as fs20 } from "fs";
10795
- import path15 from "path";
10467
+ import { promises as fs21 } from "fs";
10468
+ import path16 from "path";
10796
10469
  function templateToken(token) {
10797
10470
  if (token === "")
10798
10471
  return token;
@@ -10871,7 +10544,7 @@ function fnv1a(s) {
10871
10544
  return h.toString(16).padStart(8, "0");
10872
10545
  }
10873
10546
  function exploreDir(projectRoot) {
10874
- return path15.join(cacheDir(projectRoot), "explore");
10547
+ return path16.join(cacheDir(projectRoot), "explore");
10875
10548
  }
10876
10549
  function composeExploreId(now, randomHex) {
10877
10550
  const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
@@ -10883,17 +10556,17 @@ function isValidExploreId(id) {
10883
10556
  function mapPath(projectRoot, id) {
10884
10557
  if (!isValidExploreId(id))
10885
10558
  throw new Error(`Not an explore map id: ${id}`);
10886
- return path15.join(exploreDir(projectRoot), id, "map.json");
10559
+ return path16.join(exploreDir(projectRoot), id, "map.json");
10887
10560
  }
10888
10561
  async function writeExploreMap(projectRoot, map2) {
10889
10562
  const file = mapPath(projectRoot, map2.id);
10890
- await fs20.mkdir(path15.dirname(file), { recursive: true });
10563
+ await fs21.mkdir(path16.dirname(file), { recursive: true });
10891
10564
  await atomicWriteFile(file, `${JSON.stringify(map2, null, 2)}
10892
10565
  `);
10893
10566
  return file;
10894
10567
  }
10895
10568
  async function readExploreMap(projectRoot, id) {
10896
- const raw = await fs20.readFile(mapPath(projectRoot, id), "utf8");
10569
+ const raw = await fs21.readFile(mapPath(projectRoot, id), "utf8");
10897
10570
  const map2 = JSON.parse(raw);
10898
10571
  if (map2?.schema !== EXPLORE_MAP_SCHEMA || !Array.isArray(map2.surfaces)) {
10899
10572
  throw new Error(`Unreadable explore map: ${id}`);
@@ -10902,7 +10575,7 @@ async function readExploreMap(projectRoot, id) {
10902
10575
  }
10903
10576
  async function listExploreMaps(projectRoot) {
10904
10577
  try {
10905
- const entries = await fs20.readdir(exploreDir(projectRoot), {
10578
+ const entries = await fs21.readdir(exploreDir(projectRoot), {
10906
10579
  withFileTypes: true
10907
10580
  });
10908
10581
  return entries.filter((e) => e.isDirectory() && isValidExploreId(e.name)).map((e) => e.name).sort();
@@ -11003,10 +10676,10 @@ async function readAllRunSteps(projectRoot) {
11003
10676
  function matchSurfaceCapabilities(place, capabilities) {
11004
10677
  const normPlace = place.length > 1 ? place.replace(/\/+$/, "") : place;
11005
10678
  const matchedCaps = /* @__PURE__ */ new Map();
11006
- for (const cap of capabilities) {
11007
- if (!cap.paths || cap.paths.length === 0)
10679
+ for (const cap2 of capabilities) {
10680
+ if (!cap2.paths || cap2.paths.length === 0)
11008
10681
  continue;
11009
- for (const rawPath of cap.paths) {
10682
+ for (const rawPath of cap2.paths) {
11010
10683
  if (!rawPath)
11011
10684
  continue;
11012
10685
  const cleanPath = rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : rawPath;
@@ -11017,9 +10690,9 @@ function matchSurfaceCapabilities(place, capabilities) {
11017
10690
  isMatch = normPlace === cleanPath || normPlace.startsWith(`${cleanPath}/`) || normPlace.startsWith(`${cleanPath}?`) || normPlace.startsWith(`${cleanPath}#`);
11018
10691
  }
11019
10692
  if (isMatch) {
11020
- const prevLen = matchedCaps.get(cap.slug) ?? -1;
10693
+ const prevLen = matchedCaps.get(cap2.slug) ?? -1;
11021
10694
  if (cleanPath.length > prevLen) {
11022
- matchedCaps.set(cap.slug, cleanPath.length);
10695
+ matchedCaps.set(cap2.slug, cleanPath.length);
11023
10696
  }
11024
10697
  }
11025
10698
  }
@@ -11122,94 +10795,831 @@ function groupUnassignedByPrefix(surfaces) {
11122
10795
  g.examples.push(pathOnly);
11123
10796
  }
11124
10797
  }
11125
- return [...groups.values()].sort((a, b) => b.surfaces - a.surfaces || a.prefix.localeCompare(b.prefix));
10798
+ return [...groups.values()].sort((a, b) => b.surfaces - a.surfaces || a.prefix.localeCompare(b.prefix));
10799
+ }
10800
+ async function readCapabilityCoverage(projectRoot, coverage) {
10801
+ const [capsResult, outlinesResult] = await Promise.all([
10802
+ listEntities(projectRoot, "capability"),
10803
+ listEntities(projectRoot, "outline")
10804
+ ]);
10805
+ const caps = capsResult.entities.map((e) => ({
10806
+ slug: e.frontmatter.slug,
10807
+ title: e.frontmatter.title,
10808
+ area: e.frontmatter.area ?? null,
10809
+ paths: e.frontmatter.paths ?? []
10810
+ }));
10811
+ const outlines = outlinesResult.entities.map((e) => ({
10812
+ primary_capability: e.frontmatter.primary_capability,
10813
+ additional_capabilities: e.frontmatter.additional_capabilities ?? []
10814
+ }));
10815
+ return summarizeCapabilityCoverage(coverage.surfaces, caps, outlines);
10816
+ }
10817
+ function actionSignature(a) {
10818
+ 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;
10819
+ return `${a.role} "${a.name}" \u2192 ${where}`;
10820
+ }
10821
+ function diffExploreMaps(a, b) {
10822
+ const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
10823
+ const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
10824
+ const ref = (s) => ({
10825
+ key: s.key,
10826
+ label: s.label
10827
+ });
10828
+ const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
10829
+ const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
10830
+ const changed = [];
10831
+ let common2 = 0;
10832
+ for (const sa of a.surfaces) {
10833
+ const sb = bByKey.get(sa.key);
10834
+ if (!sb)
10835
+ continue;
10836
+ common2++;
10837
+ const setA = new Set(sa.actions.map(actionSignature));
10838
+ const setB = new Set(sb.actions.map(actionSignature));
10839
+ const inA = [...setA].filter((x) => !setB.has(x)).sort();
10840
+ const inB = [...setB].filter((x) => !setA.has(x)).sort();
10841
+ if (inA.length > 0 || inB.length > 0) {
10842
+ changed.push({ ...ref(sa), only_a: inA, only_b: inB });
10843
+ }
10844
+ }
10845
+ const union2 = common2 + onlyA.length + onlyB.length;
10846
+ return {
10847
+ a: a.id,
10848
+ b: b.id,
10849
+ common: common2,
10850
+ only_a: onlyA,
10851
+ only_b: onlyB,
10852
+ surface_overlap: union2 === 0 ? 1 : common2 / union2,
10853
+ changed_actions: changed
10854
+ };
10855
+ }
10856
+ var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
10857
+ var init_explore_map = __esm({
10858
+ "../../packages/local-store/dist/explore-map.js"() {
10859
+ "use strict";
10860
+ init_atomic();
10861
+ init_entity();
10862
+ init_paths();
10863
+ init_revisit();
10864
+ init_runs();
10865
+ init_walkthrough_canonical2();
10866
+ EXPLORE_MAP_SCHEMA = 1;
10867
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10868
+ DATE = /^\d{4}-\d{2}-\d{2}$/;
10869
+ MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
10870
+ }
10871
+ });
10872
+
10873
+ // ../../packages/local-store/dist/journey-diff.js
10874
+ function isProblemFinding(type2) {
10875
+ return PROBLEM_TYPES2.has(type2.toLowerCase());
10876
+ }
10877
+ function levenshteinDistance(a, b) {
10878
+ if (a === b)
10879
+ return 0;
10880
+ if (!a.length)
10881
+ return b.length;
10882
+ if (!b.length)
10883
+ return a.length;
10884
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
10885
+ for (let i = 1; i <= a.length; i++) {
10886
+ let prev = i;
10887
+ for (let j = 1; j <= b.length; j++) {
10888
+ const val = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], prev, row[j]) + 1;
10889
+ row[j - 1] = prev;
10890
+ prev = val;
10891
+ }
10892
+ row[b.length] = prev;
10893
+ }
10894
+ return row[b.length];
10895
+ }
10896
+ function normalizeAction(action) {
10897
+ if (!action)
10898
+ return "";
10899
+ return action.toLowerCase().replace(/['"`]/g, "").replace(/[^\p{L}\p{N}\s/_-]+/gu, " ").replace(/\s+/g, " ").trim();
10900
+ }
10901
+ function actionTokens(action) {
10902
+ const norm = normalizeAction(action);
10903
+ if (!norm)
10904
+ return /* @__PURE__ */ new Set();
10905
+ return new Set(norm.split(/\s+/).filter(Boolean));
10906
+ }
10907
+ function actionSimilarity(a, b) {
10908
+ const normA = normalizeAction(a);
10909
+ const normB = normalizeAction(b);
10910
+ if (!normA && !normB)
10911
+ return 1;
10912
+ if (!normA || !normB)
10913
+ return 0;
10914
+ if (normA === normB)
10915
+ return 1;
10916
+ const tokensA = actionTokens(normA);
10917
+ const tokensB = actionTokens(normB);
10918
+ let intersection = 0;
10919
+ for (const t of tokensA) {
10920
+ if (tokensB.has(t))
10921
+ intersection++;
10922
+ }
10923
+ if (intersection === 0) {
10924
+ if (tokensA.size <= 1 && tokensB.size <= 1) {
10925
+ const maxLen = Math.max(normA.length, normB.length);
10926
+ const levDist = levenshteinDistance(normA, normB);
10927
+ return levDist <= 2 && maxLen > 0 ? 1 - levDist / maxLen : 0;
10928
+ }
10929
+ return 0;
10930
+ }
10931
+ const union2 = tokensA.size + tokensB.size - intersection;
10932
+ const jaccard = union2 > 0 ? intersection / union2 : 0;
10933
+ const minSize = Math.min(tokensA.size, tokensB.size);
10934
+ const subsetRatio = minSize > 0 ? intersection / minSize : 0;
10935
+ if (normA.includes(normB) || normB.includes(normA) || subsetRatio === 1) {
10936
+ return Math.max(0.85, 0.7 + 0.3 * jaccard);
10937
+ }
10938
+ return 0.5 * jaccard + 0.5 * subsetRatio;
10939
+ }
10940
+ function routeSimilarity(urlA, surfaceA, urlB, surfaceB) {
10941
+ const normUrlA = normalizePlaceKey(urlA);
10942
+ const normUrlB = normalizePlaceKey(urlB);
10943
+ const hasUrlA = normUrlA !== null;
10944
+ const hasUrlB = normUrlB !== null;
10945
+ let urlScore = null;
10946
+ if (hasUrlA && hasUrlB) {
10947
+ if (normUrlA === normUrlB) {
10948
+ urlScore = 1;
10949
+ } else {
10950
+ const pathA = normUrlA.split("?")[0];
10951
+ const pathB = normUrlB.split("?")[0];
10952
+ urlScore = pathA === pathB ? 0.8 : 0;
10953
+ }
10954
+ }
10955
+ const sA = surfaceA?.trim();
10956
+ const sB = surfaceB?.trim();
10957
+ let surfaceScore = null;
10958
+ if (sA && sB) {
10959
+ if (surfaceKey(sA) === surfaceKey(sB)) {
10960
+ surfaceScore = 1;
10961
+ } else if (surfaceMatches(sA, sB) || surfaceMatches(sB, sA)) {
10962
+ surfaceScore = 0.8;
10963
+ } else {
10964
+ surfaceScore = 0;
10965
+ }
10966
+ }
10967
+ if (urlScore !== null && surfaceScore !== null) {
10968
+ if (urlScore >= 0.8 && surfaceScore >= 0.8)
10969
+ return 1;
10970
+ if (urlScore >= 0.8)
10971
+ return 0.85;
10972
+ if (surfaceScore >= 0.8)
10973
+ return 0.75;
10974
+ return 0;
10975
+ }
10976
+ if (urlScore !== null)
10977
+ return urlScore;
10978
+ if (surfaceScore !== null)
10979
+ return surfaceScore;
10980
+ return 0.5;
10981
+ }
10982
+ function stepSimilarity(a, b) {
10983
+ const rSim = routeSimilarity(a.url, a.surface, b.url, b.surface);
10984
+ const aSim = actionSimilarity(a.action, b.action);
10985
+ if (aSim < 0.25 && a.action && b.action) {
10986
+ return Math.min(0.2, rSim * 0.2);
10987
+ }
10988
+ if (rSim === 0) {
10989
+ return aSim >= 0.8 ? aSim * 0.4 : 0.1;
10990
+ }
10991
+ return 0.5 * rSim + 0.5 * aSim;
10992
+ }
10993
+ function findingMatchesStep(finding, stepNumber2) {
10994
+ if (finding.linked_step_ids && finding.linked_step_ids.length > 0) {
10995
+ return finding.linked_step_ids.some((id) => id === `step-${stepNumber2}` || id === String(stepNumber2));
10996
+ }
10997
+ if (finding.walkthrough_step_id) {
10998
+ const raw = finding.walkthrough_step_id.trim();
10999
+ if (raw === `step-${stepNumber2}` || raw === String(stepNumber2)) {
11000
+ return true;
11001
+ }
11002
+ }
11003
+ return false;
11004
+ }
11005
+ function isStallNote(note) {
11006
+ if (!note)
11007
+ return false;
11008
+ const lower = note.toLowerCase();
11009
+ return STALL_KEYWORDS.some((kw) => lower.includes(kw));
11010
+ }
11011
+ function alignJourneySteps(scenario, baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings) {
11012
+ const m = baselineSteps.length;
11013
+ const n = currentSteps.length;
11014
+ const GAP_PENALTY = -0.7;
11015
+ const MATCH_THRESHOLD = 0.45;
11016
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
11017
+ for (let i2 = 0; i2 <= m; i2++)
11018
+ dp[i2][0] = i2 * GAP_PENALTY;
11019
+ for (let j2 = 0; j2 <= n; j2++)
11020
+ dp[0][j2] = j2 * GAP_PENALTY;
11021
+ for (let i2 = 1; i2 <= m; i2++) {
11022
+ for (let j2 = 1; j2 <= n; j2++) {
11023
+ const sim = stepSimilarity(baselineSteps[i2 - 1], currentSteps[j2 - 1]);
11024
+ const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
11025
+ const match2 = dp[i2 - 1][j2 - 1] + matchScore;
11026
+ const delP = dp[i2 - 1][j2] + GAP_PENALTY;
11027
+ const insC = dp[i2][j2 - 1] + GAP_PENALTY;
11028
+ dp[i2][j2] = Math.max(match2, delP, insC);
11029
+ }
11030
+ }
11031
+ const alignedPairs = [];
11032
+ let i = m;
11033
+ let j = n;
11034
+ while (i > 0 || j > 0) {
11035
+ if (i > 0 && j > 0) {
11036
+ const sim = stepSimilarity(baselineSteps[i - 1], currentSteps[j - 1]);
11037
+ const matchScore = sim >= MATCH_THRESHOLD ? sim * 2 - 0.2 : -1.5;
11038
+ const scoreFromMatch = dp[i - 1][j - 1] + matchScore;
11039
+ if (Math.abs(dp[i][j] - scoreFromMatch) < 1e-6 && sim >= MATCH_THRESHOLD) {
11040
+ alignedPairs.push({
11041
+ baseline: baselineSteps[i - 1],
11042
+ current: currentSteps[j - 1]
11043
+ });
11044
+ i--;
11045
+ j--;
11046
+ continue;
11047
+ }
11048
+ }
11049
+ if (j > 0 && Math.abs(dp[i][j] - (dp[i][j - 1] + GAP_PENALTY)) < 1e-6) {
11050
+ alignedPairs.push({
11051
+ baseline: null,
11052
+ current: currentSteps[j - 1]
11053
+ });
11054
+ j--;
11055
+ } else if (i > 0 && Math.abs(dp[i][j] - (dp[i - 1][j] + GAP_PENALTY)) < 1e-6) {
11056
+ alignedPairs.push({
11057
+ baseline: baselineSteps[i - 1],
11058
+ current: null
11059
+ });
11060
+ i--;
11061
+ } else if (i > 0 && j > 0) {
11062
+ alignedPairs.push({
11063
+ baseline: baselineSteps[i - 1],
11064
+ current: currentSteps[j - 1]
11065
+ });
11066
+ i--;
11067
+ j--;
11068
+ } else if (i > 0) {
11069
+ alignedPairs.push({
11070
+ baseline: baselineSteps[i - 1],
11071
+ current: null
11072
+ });
11073
+ i--;
11074
+ } else {
11075
+ alignedPairs.push({
11076
+ baseline: null,
11077
+ current: currentSteps[j - 1]
11078
+ });
11079
+ j--;
11080
+ }
11081
+ }
11082
+ alignedPairs.reverse();
11083
+ const maxBaselineStepNum = baselineSteps.length > 0 ? Math.max(...baselineSteps.map((s) => s.step_number)) : -1;
11084
+ const alignedSteps = [];
11085
+ const stats = {
11086
+ totalAlignedSteps: alignedPairs.length,
11087
+ same: 0,
11088
+ resolved: 0,
11089
+ persists: 0,
11090
+ regressions: 0,
11091
+ stalledInBaseline: 0,
11092
+ bypassed: 0,
11093
+ newPath: 0
11094
+ };
11095
+ for (let idx = 0; idx < alignedPairs.length; idx++) {
11096
+ const pair = alignedPairs[idx];
11097
+ const bStep = pair.baseline;
11098
+ const cStep = pair.current;
11099
+ const bFindings = bStep ? baselineFindings.filter((f) => findingMatchesStep(f, bStep.step_number)) : [];
11100
+ const cFindings = cStep ? currentFindings.filter((f) => findingMatchesStep(f, cStep.step_number)) : [];
11101
+ const bProblems = bFindings.filter((f) => isProblemFinding(f.type));
11102
+ const cProblems = cFindings.filter((f) => isProblemFinding(f.type));
11103
+ let status2 = "same";
11104
+ let summary = "Clean";
11105
+ if (bStep && cStep) {
11106
+ const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
11107
+ if (bProblems.length > 0 && cProblems.length === 0) {
11108
+ status2 = "resolved";
11109
+ stats.resolved++;
11110
+ const probDesc = bProblems[0].ui_fact || bProblems[0].title || bProblems[0].id;
11111
+ summary = wasStall ? `Previous stall resolved \u2014 journey unblocked (${probDesc})` : `Resolved previous issue: ${probDesc}`;
11112
+ } else if (bProblems.length > 0 && cProblems.length > 0) {
11113
+ status2 = "persists";
11114
+ stats.persists++;
11115
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11116
+ summary = `Problem persists: ${probDesc}`;
11117
+ } else if (bProblems.length === 0 && cProblems.length > 0) {
11118
+ status2 = "regression";
11119
+ stats.regressions++;
11120
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11121
+ summary = `New issue reported: ${probDesc}`;
11122
+ } else {
11123
+ status2 = "same";
11124
+ stats.same++;
11125
+ summary = "Clean";
11126
+ }
11127
+ } else if (bStep && !cStep) {
11128
+ const wasStall = bStep.step_number === maxBaselineStepNum && (bProblems.length > 0 || isStallNote(bStep.note));
11129
+ if (wasStall) {
11130
+ status2 = "stalled";
11131
+ stats.stalledInBaseline++;
11132
+ const probDesc = bProblems[0]?.ui_fact || bProblems[0]?.title || "journey halted";
11133
+ summary = `Stalled in baseline: ${probDesc}`;
11134
+ } else {
11135
+ status2 = "bypassed";
11136
+ stats.bypassed++;
11137
+ summary = "Bypassed in current run";
11138
+ }
11139
+ } else if (!bStep && cStep) {
11140
+ if (cProblems.length > 0) {
11141
+ status2 = "regression";
11142
+ stats.regressions++;
11143
+ const probDesc = cProblems[0].ui_fact || cProblems[0].title || cProblems[0].id;
11144
+ summary = `New step with issue: ${probDesc}`;
11145
+ } else {
11146
+ status2 = "new_path";
11147
+ stats.newPath++;
11148
+ summary = "New path / unblocked progress";
11149
+ }
11150
+ }
11151
+ alignedSteps.push({
11152
+ index: idx + 1,
11153
+ status: status2,
11154
+ baselineStep: bStep,
11155
+ currentStep: cStep,
11156
+ baselineFindings: bFindings,
11157
+ currentFindings: cFindings,
11158
+ summary
11159
+ });
11160
+ }
11161
+ return {
11162
+ scenario,
11163
+ baselineRunId,
11164
+ currentRunId,
11165
+ steps: alignedSteps,
11166
+ stats
11167
+ };
11168
+ }
11169
+ function toStepFindingInfo(f) {
11170
+ return {
11171
+ id: f.frontmatter.id,
11172
+ title: f.frontmatter.title ?? f.slug,
11173
+ type: f.frontmatter.type,
11174
+ severity: f.frontmatter.severity,
11175
+ ui_fact: f.frontmatter.ui_fact,
11176
+ consequence: f.frontmatter.consequence,
11177
+ canonical_id: f.frontmatter.canonical_id,
11178
+ walkthrough_step_id: f.frontmatter.walkthrough_step_id
11179
+ };
11180
+ }
11181
+ function toJourneyStepInfo(s) {
11182
+ return {
11183
+ step_number: s.step_number,
11184
+ action: s.action,
11185
+ url: s.url ?? void 0,
11186
+ surface: s.surface ?? void 0,
11187
+ note: s.note ?? void 0
11188
+ };
11189
+ }
11190
+ function journeyStepRoute(step) {
11191
+ return step.currentStep?.url && normalizePlaceKey(step.currentStep.url) || step.baselineStep?.url && normalizePlaceKey(step.baselineStep.url) || step.currentStep?.surface || step.baselineStep?.surface || "\u2014";
11192
+ }
11193
+ async function computeJourneyDiff(projectRoot, baselineRunId, currentRunId, scenarioSlug) {
11194
+ const [bStepsRaw, bFindingsRaw, cStepsRaw, cFindingsRaw] = await Promise.all([
11195
+ readStepsForRun(projectRoot, baselineRunId),
11196
+ readFindingsForRun(projectRoot, baselineRunId),
11197
+ readStepsForRun(projectRoot, currentRunId),
11198
+ readFindingsForRun(projectRoot, currentRunId)
11199
+ ]);
11200
+ const baselineSteps = bStepsRaw.map(toJourneyStepInfo);
11201
+ const baselineFindings = bFindingsRaw.map(toStepFindingInfo);
11202
+ const currentSteps = cStepsRaw.map(toJourneyStepInfo);
11203
+ const currentFindings = cFindingsRaw.map(toStepFindingInfo);
11204
+ return alignJourneySteps(scenarioSlug ?? "", baselineRunId, currentRunId, baselineSteps, baselineFindings, currentSteps, currentFindings);
11205
+ }
11206
+ function renderJourneyDiffMarkdown(diff) {
11207
+ const lines = [
11208
+ "### Chronological journey diff",
11209
+ "",
11210
+ `Baseline: \`${diff.baselineRunId}\` vs Current: \`${diff.currentRunId}\``
11211
+ ];
11212
+ const parts = [];
11213
+ if (diff.stats.same > 0)
11214
+ parts.push(`${diff.stats.same} unchanged`);
11215
+ if (diff.stats.resolved > 0)
11216
+ parts.push(`${diff.stats.resolved} resolved`);
11217
+ if (diff.stats.persists > 0)
11218
+ parts.push(`${diff.stats.persists} persists`);
11219
+ if (diff.stats.regressions > 0)
11220
+ parts.push(`${diff.stats.regressions} new issues`);
11221
+ if (diff.stats.stalledInBaseline > 0)
11222
+ parts.push(`${diff.stats.stalledInBaseline} stalled in baseline`);
11223
+ if (diff.stats.bypassed > 0)
11224
+ parts.push(`${diff.stats.bypassed} bypassed`);
11225
+ if (diff.stats.newPath > 0)
11226
+ parts.push(`${diff.stats.newPath} new path steps`);
11227
+ if (parts.length > 0) {
11228
+ lines.push(`- **Summary**: ${parts.join(", ")}`);
11229
+ }
11230
+ lines.push("", "| # | Route | Baseline (Run N-1) | Current (Run N) | Status | Notes |", "|---|-------|--------------------|-----------------|--------|-------|");
11231
+ for (const step of diff.steps) {
11232
+ const route = journeyStepRoute(step);
11233
+ const baseAction = step.baselineStep ? `Step ${step.baselineStep.step_number}: ${escapeMarkdownCell(step.baselineStep.action)}` : "\u2014";
11234
+ const currAction = step.currentStep ? `Step ${step.currentStep.step_number}: ${escapeMarkdownCell(step.currentStep.action)}` : "\u2014";
11235
+ const statusBadge = `\`${step.status}\``;
11236
+ const notes = escapeMarkdownCell(step.summary);
11237
+ lines.push(`| ${step.index} | \`${escapeMarkdownCell(route)}\` | ${baseAction} | ${currAction} | ${statusBadge} | ${notes} |`);
11238
+ }
11239
+ return lines.join("\n");
11240
+ }
11241
+ function escapeMarkdownCell(text) {
11242
+ return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
11243
+ }
11244
+ var PROBLEM_TYPES2, STALL_KEYWORDS;
11245
+ var init_journey_diff = __esm({
11246
+ "../../packages/local-store/dist/journey-diff.js"() {
11247
+ "use strict";
11248
+ init_revisit();
11249
+ init_walkthrough_canonical2();
11250
+ PROBLEM_TYPES2 = /* @__PURE__ */ new Set(["hinders", "missing", "incorrect"]);
11251
+ STALL_KEYWORDS = [
11252
+ "blocked",
11253
+ "unable to proceed",
11254
+ "stalled",
11255
+ "halted",
11256
+ "stuck",
11257
+ "cannot proceed",
11258
+ "could not proceed",
11259
+ "failed to load",
11260
+ "timed out"
11261
+ ];
11262
+ }
11263
+ });
11264
+
11265
+ // ../../packages/local-store/dist/report-markdown.js
11266
+ function runVerdict(canonical) {
11267
+ const judged = (canonical?.human_tests ?? []).map((t) => t.verdict).filter((v) => v === "yes" || v === "no" || v === "partial");
11268
+ if (judged.length === 0)
11269
+ return null;
11270
+ if (judged.every((v) => v === "yes"))
11271
+ return "yes";
11272
+ if (judged.every((v) => v === "no"))
11273
+ return "no";
11274
+ return "partial";
11275
+ }
11276
+ function rank(value, order) {
11277
+ const i = order.indexOf(value ?? "");
11278
+ return i === -1 ? order.length : i;
11279
+ }
11280
+ function pagePath(url) {
11281
+ if (!url)
11282
+ return null;
11283
+ try {
11284
+ return new URL(url).pathname;
11285
+ } catch {
11286
+ return url;
11287
+ }
11288
+ }
11289
+ function dayOf(value) {
11290
+ const m = /^(\d{4}-\d{2}-\d{2})/.exec(value ?? "");
11291
+ return m?.[1] ?? null;
11292
+ }
11293
+ function stepNumbers(ids) {
11294
+ const nums = (ids ?? []).map((id) => /\d+/.exec(id)?.[0]).filter((n) => Boolean(n));
11295
+ if (nums.length === 0)
11296
+ return "";
11297
+ return ` (step${nums.length === 1 ? "" : "s"} ${nums.join(", ")})`;
11298
+ }
11299
+ function titleOf(f) {
11300
+ if (f.title)
11301
+ return f.title;
11302
+ const fact = f.ui_fact ?? f.id;
11303
+ return /^(.+?[.!?])(\s|$)/.exec(fact)?.[1] ?? fact;
11304
+ }
11305
+ function renderHumanTest(c, lines) {
11306
+ lines.push("", "## The human test");
11307
+ const verdict = runVerdict(c);
11308
+ const label = verdict ? `**${cap(verdict)}**` : c.blocked ? "_skipped (walk blocked)_" : "_pending judge_";
11309
+ lines.push("", `**${c.human_test_overall.question}** ${label}`);
11310
+ if (c.human_test_overall.answer) {
11311
+ lines.push("", c.human_test_overall.answer);
11312
+ if (c.human_test_overall.reasoning) {
11313
+ lines.push("", c.human_test_overall.reasoning);
11314
+ }
11315
+ }
11316
+ if (c.human_tests.length === 0) {
11317
+ lines.push("", "_No human tests on the outline._");
11318
+ return;
11319
+ }
11320
+ lines.push("");
11321
+ for (const ht of c.human_tests) {
11322
+ const v = ht.verdict ? `**${cap(ht.verdict.replace("_", " "))}.**` : "_Pending._";
11323
+ lines.push(`- ${stripAnswerHint(ht.question)} ${v}${ht.reasoning ? ` ${ht.reasoning}` : ""}`);
11324
+ }
11325
+ }
11326
+ function severityShift(before, now) {
11327
+ if (!before || !now || before === now)
11328
+ return "";
11329
+ const b = rank(before, SEVERITY_ORDER);
11330
+ const n = rank(now, SEVERITY_ORDER);
11331
+ if (b === n)
11332
+ return "";
11333
+ return `${n > b ? "less severe" : "worse"}, ${before} \u2192 ${now}`;
11334
+ }
11335
+ function checkOutcomes(c) {
11336
+ const checks = c.checks;
11337
+ if (!checks)
11338
+ return [];
11339
+ const v = c.verification;
11340
+ const fixed = new Set(v?.resolved.map((r) => r.canonical_id) ?? []);
11341
+ const seen = new Set(c.findings.map((f) => f.canonical_id).filter(Boolean));
11342
+ return checks.fixes.map((f) => ({
11343
+ ...f,
11344
+ outcome: f.canonical_id && fixed.has(f.canonical_id) ? "fixed" : f.canonical_id && seen.has(f.canonical_id) ? "came_back" : "unconfirmed"
11345
+ }));
11346
+ }
11347
+ function renderChecks(c, checks, lines) {
11348
+ lines.push("", "## What this run checks");
11349
+ const fixes = checkOutcomes(c);
11350
+ if (fixes.length > 0) {
11351
+ const count = (o) => fixes.filter((f) => f.outcome === o).length;
11352
+ 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.`, "");
11353
+ const order = ["came_back", "fixed", "unconfirmed"];
11354
+ const sorted = [...fixes].sort((a, b) => order.indexOf(a.outcome) - order.indexOf(b.outcome));
11355
+ for (const f of sorted) {
11356
+ const where = [
11357
+ pagePath(f.page) ? `\`${pagePath(f.page)}\`` : "",
11358
+ f.reached ? "" : "not reached"
11359
+ ].filter(Boolean).join(", ");
11360
+ lines.push(`- ${OUTCOME_TEXT[f.outcome]}: **${f.title}**${f.fix ? `. Fix: ${f.fix}` : ""}${where ? ` (${where})` : ""}`);
11361
+ }
11362
+ }
11363
+ if (checks.directions.length > 0) {
11364
+ lines.push("", `Directions completed since the last run: ${checks.directions.map((d) => `**${d}**`).join(", ")}.`);
11365
+ }
11366
+ }
11367
+ function sinceRow(item, outcome) {
11368
+ const qualifiers = [
11369
+ outcome === "Persists" ? severityShift(item.severity_before, item.severity_now) : "",
11370
+ outcome === "Persists" && item.fixed_before ? "the fix did not hold" : ""
11371
+ ].filter(Boolean);
11372
+ const now = outcome === "Resolved" || outcome === "Persists" ? `**${outcome}**${qualifiers.length > 0 ? ` (${qualifiers.join("; ")})` : ""}` : outcome;
11373
+ 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();
11374
+ return `| ${cell(item.title)} | ${now} | ${cell(note) || "\u2014"} |`;
11375
+ }
11376
+ function renderSince(v, newProblems, newStrengths, lines) {
11377
+ const day = dayOf(v.baseline_run_id);
11378
+ lines.push("", "## Since the last run", "", `Compared with the run of ${day ?? v.baseline_run_id}.`);
11379
+ const rows = [
11380
+ ...v.resolved.map((i) => sinceRow(i, "Resolved")),
11381
+ ...v.persists.map((i) => sinceRow(i, "Persists")),
11382
+ ...v.not_observed.map((i) => sinceRow(i, "Not seen")),
11383
+ ...v.uncertain.map((i) => sinceRow(i, "Unclear"))
11384
+ ];
11385
+ if (rows.length > 0) {
11386
+ lines.push("", "| Earlier finding | Now | Notes |", "|---|---|---|", ...rows);
11387
+ }
11388
+ const added = [
11389
+ newProblems > 0 ? `${newProblems} problem${newProblems === 1 ? "" : "s"}` : "",
11390
+ newStrengths > 0 ? `${newStrengths} strength${newStrengths === 1 ? "" : "s"}` : ""
11391
+ ].filter(Boolean);
11392
+ lines.push("", added.length > 0 ? `New this run: ${added.join(" and ")}, marked _new_ below.` : "Nothing new this run.");
11393
+ }
11394
+ function renderFinding(c, f, n, since, lines) {
11395
+ lines.push("", `### ${n}. ${titleOf(f)}`);
11396
+ const steps = f.linked_step_ids.map((id) => c.steps.find((s) => s.id === id)).filter((s) => Boolean(s));
11397
+ const page = pagePath(f.page_url ?? steps[0]?.url);
11398
+ const where = [
11399
+ page ? `\`${page}\`` : "",
11400
+ steps.length > 0 ? `step ${steps.map((s) => s.step_number).join(", ")}` : ""
11401
+ ].filter(Boolean).join(" ");
11402
+ const meta = [
11403
+ f.severity ? `**${cap(f.severity)}**` : "",
11404
+ f.type,
11405
+ f.task_impact ? TASK_IMPACT[f.task_impact] : "",
11406
+ where,
11407
+ since ? `_${since}_` : ""
11408
+ ].filter(Boolean);
11409
+ lines.push("", meta.join(" \xB7 "));
11410
+ if (f.ui_fact === null || f.consequence === null) {
11411
+ lines.push("", "> _Legacy finding: predates the UI-fact / consequence depth gate._");
11412
+ }
11413
+ if (f.ui_fact)
11414
+ lines.push("", f.ui_fact);
11415
+ if (f.consequence) {
11416
+ const who = c.scenario.persona_name;
11417
+ lines.push("", `> ${f.consequence}${who ? ` \u2014 ${who}` : ""}`);
11418
+ }
11419
+ if (f.recommendation)
11420
+ lines.push("", `**Recommendation:** ${f.recommendation}`);
11421
+ for (const s of steps) {
11422
+ if (s.screenshot_ref) {
11423
+ lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
11424
+ }
11425
+ }
11426
+ }
11427
+ function renderAnalysis(analysis, lines) {
11428
+ if (analysis.directions.length > 0) {
11429
+ lines.push("", "## Directions", "", `_Structural changes proposed by ${analysis.advisor} after the walk. Reach is a prediction until the listed scenarios are re-run._`);
11430
+ for (const d of analysis.directions) {
11431
+ lines.push("", `### ${d.title} (${d.size})`, "", d.change);
11432
+ lines.push("", `Absorbs ${d.absorbs.length} finding${d.absorbs.length === 1 ? "" : "s"}.`);
11433
+ for (const r of d.reach.filter((x) => x.effect === "helps"))
11434
+ lines.push(`- Likely helps ${r.scenario}: ${r.reason}`);
11435
+ for (const r of d.reach.filter((x) => x.effect === "risk"))
11436
+ lines.push(`- Could hurt ${r.scenario}: ${r.reason}`);
11437
+ if (d.verify.length > 0)
11438
+ lines.push("", `Re-run to verify: ${d.verify.join(", ")}`);
11439
+ }
11440
+ }
11441
+ if (analysis.observations.length > 0) {
11442
+ lines.push("", "## Also worth knowing", "");
11443
+ for (const o of analysis.observations) {
11444
+ const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
11445
+ lines.push(`- **${label}: ${o.title}.** ${o.detail}`);
11446
+ if (o.suggested_scenario) {
11447
+ lines.push(` Suggested scenario: ${o.suggested_scenario}`);
11448
+ }
11449
+ }
11450
+ }
11126
11451
  }
11127
- async function readCapabilityCoverage(projectRoot, coverage) {
11128
- const [capsResult, outlinesResult] = await Promise.all([
11129
- listEntities(projectRoot, "capability"),
11130
- listEntities(projectRoot, "outline")
11131
- ]);
11132
- const caps = capsResult.entities.map((e) => ({
11133
- slug: e.frontmatter.slug,
11134
- title: e.frontmatter.title,
11135
- area: e.frontmatter.area ?? null,
11136
- paths: e.frontmatter.paths ?? []
11137
- }));
11138
- const outlines = outlinesResult.entities.map((e) => ({
11139
- primary_capability: e.frontmatter.primary_capability,
11140
- additional_capabilities: e.frontmatter.additional_capabilities ?? []
11141
- }));
11142
- return summarizeCapabilityCoverage(coverage.surfaces, caps, outlines);
11452
+ function renderRunDetails(c, lines) {
11453
+ lines.push("", "## Run details", "", `- Run ID: ${c.run_id}`, `- Target URL: ${c.target.url}`);
11454
+ if (c.environment)
11455
+ lines.push(`- Environment: ${c.environment}`);
11456
+ if (c.build) {
11457
+ const commit = c.build.git_sha?.slice(0, 7);
11458
+ const build = [
11459
+ c.build.label,
11460
+ c.build.git_branch,
11461
+ c.build.git_dirty && c.build.worktree_hash ? `${commit ?? "HEAD"} + uncommitted changes (${c.build.worktree_hash.slice(0, 6)}\u2026)` : commit,
11462
+ c.build.ci?.provider
11463
+ ].filter(Boolean).join(" \xB7 ");
11464
+ if (build)
11465
+ lines.push(`- Build: ${build}`);
11466
+ }
11467
+ if (c.viewport) {
11468
+ lines.push(`- Viewport: ${c.viewport.device} (${c.viewport.width}\xD7${c.viewport.height})`);
11469
+ }
11470
+ lines.push(`- Started: ${c.provenance.started_at}`, `- Completed: ${c.provenance.completed_at}`, `- Walker prompt: ${c.provenance.walker_prompt.id}@${c.provenance.walker_prompt.version}`);
11471
+ const models = c.models;
11472
+ if (models) {
11473
+ const parts = [
11474
+ models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
11475
+ models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
11476
+ models.matcher ? `Matcher: ${models.matcher}` : ""
11477
+ ].filter(Boolean);
11478
+ if (parts.length > 0)
11479
+ lines.push(`- Models: ${parts.join(" \xB7 ")}`);
11480
+ if (typeof models.velocity_sec_per_step === "number") {
11481
+ lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
11482
+ }
11483
+ }
11484
+ const diff = c.journey_diff;
11485
+ if (diff)
11486
+ lines.push("", renderJourneyDiffMarkdown(diff));
11143
11487
  }
11144
- function actionSignature(a) {
11145
- 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;
11146
- return `${a.role} "${a.name}" \u2192 ${where}`;
11488
+ function renderSteps(c, lines) {
11489
+ lines.push("", "## Steps");
11490
+ if (c.steps.length === 0) {
11491
+ lines.push("", "_No steps recorded._");
11492
+ return;
11493
+ }
11494
+ for (const s of c.steps) {
11495
+ lines.push("", `### ${s.step_number}. ${s.screen_title ?? "(untitled)"} \u2014 ${s.id}`);
11496
+ const where = [s.surface, s.url].filter(Boolean).join(" \u2014 ");
11497
+ if (where)
11498
+ lines.push("", `Page: ${where}`);
11499
+ if (s.persona_reasoning)
11500
+ lines.push("", `> ${s.persona_reasoning.replace(/\n/g, "\n> ")}`);
11501
+ if (s.grounding)
11502
+ lines.push("", `Grounding: ${s.grounding}`);
11503
+ if (s.screenshot_ref)
11504
+ lines.push("", `![Step ${s.step_number}](${s.screenshot_ref})`);
11505
+ }
11147
11506
  }
11148
- function diffExploreMaps(a, b) {
11149
- const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
11150
- const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
11151
- const ref = (s) => ({
11152
- key: s.key,
11153
- label: s.label
11154
- });
11155
- const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
11156
- const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
11157
- const changed = [];
11158
- let common2 = 0;
11159
- for (const sa of a.surfaces) {
11160
- const sb = bByKey.get(sa.key);
11161
- if (!sb)
11162
- continue;
11163
- common2++;
11164
- const setA = new Set(sa.actions.map(actionSignature));
11165
- const setB = new Set(sb.actions.map(actionSignature));
11166
- const inA = [...setA].filter((x) => !setB.has(x)).sort();
11167
- const inB = [...setB].filter((x) => !setA.has(x)).sort();
11168
- if (inA.length > 0 || inB.length > 0) {
11169
- changed.push({ ...ref(sa), only_a: inA, only_b: inB });
11507
+ function renderCanonicalMarkdown(canonical) {
11508
+ const c = canonical;
11509
+ const persona = c.scenario.persona_name ?? c.scenario.persona_slug;
11510
+ const situation = c.scenario.outline_title ?? c.scenario.outline_slug;
11511
+ const heading = persona && situation ? `${persona} \xB7 ${situation}` : c.scenario.slug;
11512
+ const runNumber = c.run_number;
11513
+ const subtitle = [
11514
+ dayOf(c.provenance.completed_at),
11515
+ runNumber ? `run ${runNumber} of this scenario` : "",
11516
+ `${c.steps.length} step${c.steps.length === 1 ? "" : "s"}`,
11517
+ c.target.url
11518
+ ].filter(Boolean).join(" \xB7 ");
11519
+ const lines = [
11520
+ "<!-- GENERATED: do not edit by hand. Regen via complete_walkthrough or local-store renderer. -->",
11521
+ "",
11522
+ `# ${heading}`,
11523
+ "",
11524
+ subtitle
11525
+ ];
11526
+ if (c.scenario.persona_role) {
11527
+ lines.push("", `${persona}, ${c.scenario.persona_role}.`);
11528
+ }
11529
+ if (c.blocked) {
11530
+ 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.");
11531
+ }
11532
+ const checks = c.checks;
11533
+ if (checks)
11534
+ renderChecks(c, checks, lines);
11535
+ renderHumanTest(c, lines);
11536
+ const verification = c.verification;
11537
+ const newIds = new Set(verification?.new_findings.map((n) => n.id) ?? []);
11538
+ 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));
11539
+ const strengths = c.findings.filter((f) => f.type === "helps");
11540
+ if (verification) {
11541
+ renderSince(verification, problems.filter((f) => newIds.has(f.id)).length, strengths.filter((f) => newIds.has(f.id)).length, lines);
11542
+ }
11543
+ lines.push("", "## Findings");
11544
+ if (problems.length === 0) {
11545
+ lines.push("", "_No problems recorded._");
11546
+ } else {
11547
+ const bySeverity = SEVERITY_ORDER.map((s) => {
11548
+ const n = problems.filter((f) => f.severity === s).length;
11549
+ return n > 0 ? `${n} ${s}` : "";
11550
+ }).filter(Boolean);
11551
+ lines.push("", `${problems.length} problem${problems.length === 1 ? "" : "s"} (${bySeverity.join(", ")}), worst first.`);
11552
+ const persisting = new Set(verification?.persists.map((p) => p.canonical_id) ?? []);
11553
+ problems.forEach((f, i) => {
11554
+ const since = newIds.has(f.id) ? "new" : f.canonical_id && persisting.has(f.canonical_id) ? "persists" : null;
11555
+ renderFinding(c, f, i + 1, since, lines);
11556
+ });
11557
+ }
11558
+ const analysis = c.analysis;
11559
+ if (analysis)
11560
+ renderAnalysis(analysis, lines);
11561
+ const what = c.what_worked ?? [];
11562
+ if (strengths.length > 0 || what.length > 0) {
11563
+ lines.push("", "## What worked", "");
11564
+ for (const f of strengths) {
11565
+ lines.push(`- **${titleOf(f)}.** ${f.ui_fact ?? ""}`.trimEnd());
11170
11566
  }
11567
+ for (const w of what)
11568
+ lines.push(`- ${w}`);
11171
11569
  }
11172
- const union2 = common2 + onlyA.length + onlyB.length;
11173
- return {
11174
- a: a.id,
11175
- b: b.id,
11176
- common: common2,
11177
- only_a: onlyA,
11178
- only_b: onlyB,
11179
- surface_overlap: union2 === 0 ? 1 : common2 / union2,
11180
- changed_actions: changed
11181
- };
11570
+ const open2 = c.open_questions ?? [];
11571
+ if (open2.length > 0) {
11572
+ lines.push("", "## Open questions", "");
11573
+ for (const q of open2)
11574
+ lines.push(`- ${q}`);
11575
+ }
11576
+ const setup = c.setup_notes ?? [];
11577
+ if (setup.length > 0) {
11578
+ lines.push("", "## Test setup notes", "", "_Doubts about the test, not the product: for whoever stages the scenario._", "");
11579
+ for (const n of setup)
11580
+ lines.push(`- ${n}`);
11581
+ }
11582
+ renderSteps(c, lines);
11583
+ renderRunDetails(c, lines);
11584
+ return `${lines.join("\n")}
11585
+ `;
11182
11586
  }
11183
- var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
11184
- var init_explore_map = __esm({
11185
- "../../packages/local-store/dist/explore-map.js"() {
11587
+ var SEVERITY_ORDER, TYPE_ORDER, TASK_IMPACT, cap, stripAnswerHint, cell, OUTCOME_TEXT;
11588
+ var init_report_markdown = __esm({
11589
+ "../../packages/local-store/dist/report-markdown.js"() {
11186
11590
  "use strict";
11187
- init_atomic();
11188
- init_entity();
11189
- init_paths();
11190
- init_revisit();
11191
- init_runs();
11192
- init_walkthrough_canonical2();
11193
- EXPLORE_MAP_SCHEMA = 1;
11194
- UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11195
- DATE = /^\d{4}-\d{2}-\d{2}$/;
11196
- MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
11591
+ init_journey_diff();
11592
+ SEVERITY_ORDER = ["critical", "major", "minor", "observation"];
11593
+ TYPE_ORDER = ["incorrect", "hinders", "missing", "irrelevant", "helps"];
11594
+ TASK_IMPACT = {
11595
+ blocks: "blocks the task",
11596
+ impairs: "impairs the task",
11597
+ slows: "slows the task"
11598
+ };
11599
+ cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
11600
+ stripAnswerHint = (q) => q.replace(/\s*\((?:yes|no|partial)(?:\s*\/\s*(?:yes|no|partial))+\)\s*$/i, "");
11601
+ cell = (s) => s.replace(/\|/g, "\\|").replace(/\n+/g, " ");
11602
+ OUTCOME_TEXT = {
11603
+ fixed: "**confirmed fixed**",
11604
+ came_back: "**came back**",
11605
+ unconfirmed: "not confirmed"
11606
+ };
11197
11607
  }
11198
11608
  });
11199
11609
 
11200
11610
  // ../../packages/local-store/dist/human-test-link.js
11201
- import { promises as fs21 } from "fs";
11202
- import path16 from "path";
11611
+ import { promises as fs22 } from "fs";
11612
+ import path17 from "path";
11203
11613
  function humanTestLinks(verdicts) {
11204
11614
  const best = /* @__PURE__ */ new Map();
11205
11615
  for (const v of verdicts) {
11206
- const rank = VERDICT_RANK[v.verdict];
11207
- if (rank === void 0)
11616
+ const rank2 = VERDICT_RANK[v.verdict];
11617
+ if (rank2 === void 0)
11208
11618
  continue;
11209
11619
  for (const findingId of v.grounding_finding_ids) {
11210
11620
  const seen = best.get(findingId);
11211
- if (!seen || rank < seen.rank) {
11212
- best.set(findingId, { test: v.human_test_id, rank });
11621
+ if (!seen || rank2 < seen.rank) {
11622
+ best.set(findingId, { test: v.human_test_id, rank: rank2 });
11213
11623
  }
11214
11624
  }
11215
11625
  }
@@ -11222,7 +11632,7 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
11222
11632
  const dir = entityDirPath(projectRoot, "finding");
11223
11633
  let names2;
11224
11634
  try {
11225
- names2 = await fs21.readdir(dir);
11635
+ names2 = await fs22.readdir(dir);
11226
11636
  } catch {
11227
11637
  return [];
11228
11638
  }
@@ -11230,11 +11640,11 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
11230
11640
  for (const name of names2) {
11231
11641
  if (!name.endsWith(".md") || name.startsWith("."))
11232
11642
  continue;
11233
- const file = path16.join(dir, name);
11643
+ const file = path17.join(dir, name);
11234
11644
  let raw;
11235
11645
  let fm;
11236
11646
  try {
11237
- raw = await fs21.readFile(file, "utf8");
11647
+ raw = await fs22.readFile(file, "utf8");
11238
11648
  fm = parseFrontmatter(raw).frontmatter;
11239
11649
  } catch {
11240
11650
  continue;
@@ -11465,14 +11875,14 @@ var init_cloud_client = __esm({
11465
11875
  });
11466
11876
 
11467
11877
  // ../../packages/local-store/dist/last-pull.js
11468
- import { promises as fs22 } from "fs";
11469
- import path17 from "path";
11878
+ import { promises as fs23 } from "fs";
11879
+ import path18 from "path";
11470
11880
  function lastPullPath(projectRoot) {
11471
- return path17.join(cacheDir(projectRoot), "last-pull.json");
11881
+ return path18.join(cacheDir(projectRoot), "last-pull.json");
11472
11882
  }
11473
11883
  async function readLastPull(projectRoot) {
11474
11884
  try {
11475
- const raw = await fs22.readFile(lastPullPath(projectRoot), "utf8");
11885
+ const raw = await fs23.readFile(lastPullPath(projectRoot), "utf8");
11476
11886
  const parsed = JSON.parse(raw);
11477
11887
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11478
11888
  return parsed;
@@ -11578,7 +11988,7 @@ var init_types2 = __esm({
11578
11988
  });
11579
11989
 
11580
11990
  // ../../packages/local-store/dist/pull/frontmatter.js
11581
- import { randomUUID as randomUUID6 } from "crypto";
11991
+ import { randomUUID as randomUUID7 } from "crypto";
11582
11992
  function resolveBody(row, fallback) {
11583
11993
  if (typeof row.bodyMarkdown === "string") {
11584
11994
  return { text: row.bodyMarkdown, fromCloud: true };
@@ -11620,7 +12030,7 @@ function buildOutlineFrontmatter(row, slug, capabilityIdToSlug) {
11620
12030
  missingCaps.push(capId);
11621
12031
  }
11622
12032
  const humanTests = (row.humanTests ?? []).map((h) => ({
11623
- id: h.id ?? randomUUID6().slice(0, 8),
12033
+ id: h.id ?? randomUUID7().slice(0, 8),
11624
12034
  question: h.question
11625
12035
  }));
11626
12036
  const frontmatter = {
@@ -11787,7 +12197,7 @@ var init_frontmatter2 = __esm({
11787
12197
  });
11788
12198
 
11789
12199
  // ../../packages/local-store/dist/pull/write.js
11790
- import { promises as fs23 } from "fs";
12200
+ import { promises as fs24 } from "fs";
11791
12201
  async function loadLocalIndex(projectRoot, type2) {
11792
12202
  const byId = /* @__PURE__ */ new Map();
11793
12203
  const bySlug = /* @__PURE__ */ new Map();
@@ -11818,7 +12228,7 @@ async function classifyExisting(projectRoot, type2, cloudId, localSlug, targetSl
11818
12228
  if (localSlug !== targetSlug)
11819
12229
  return "untracked";
11820
12230
  const lastSynced = localIndex.lastPushById[cloudId];
11821
- const bytes = await fs23.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
12231
+ const bytes = await fs24.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
11822
12232
  return lastSynced && hashNormalizedContent(bytes) === lastSynced ? "fast_forward" : "diverged";
11823
12233
  }
11824
12234
  async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body, localIndex, mode, cloudVersion) {
@@ -11879,13 +12289,13 @@ async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body,
11879
12289
  }
11880
12290
  if (existingSlugForId && existingSlugForId !== slug) {
11881
12291
  try {
11882
- await fs23.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
12292
+ await fs24.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
11883
12293
  localIndex.bySlug.delete(existingSlugForId);
11884
12294
  } catch {
11885
12295
  }
11886
12296
  }
11887
12297
  await writeEntity(projectRoot, type2, frontmatter, text);
11888
- const bytes = await fs23.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
12298
+ const bytes = await fs24.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
11889
12299
  const hash = hashNormalizedContent(bytes);
11890
12300
  localIndex.byId.set(cloudId, slug);
11891
12301
  localIndex.bySlug.set(slug, cloudId);
@@ -11939,7 +12349,7 @@ var init_write = __esm({
11939
12349
  });
11940
12350
 
11941
12351
  // ../../packages/local-store/dist/pull/entities.js
11942
- import { promises as fs24 } from "fs";
12352
+ import { promises as fs25 } from "fs";
11943
12353
  function buildContextFrontmatter(payload) {
11944
12354
  const cloudCtx = payload.context ?? {};
11945
12355
  const glossary = (payload.glossary ?? []).map((g) => ({
@@ -11994,7 +12404,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
11994
12404
  const frontmatter = buildContextFrontmatter(payload);
11995
12405
  let hasLocal = false;
11996
12406
  try {
11997
- await fs24.access(contextPath(projectRoot));
12407
+ await fs25.access(contextPath(projectRoot));
11998
12408
  hasLocal = true;
11999
12409
  } catch {
12000
12410
  hasLocal = false;
@@ -12024,7 +12434,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
12024
12434
  frontmatter,
12025
12435
  notes_body: existingNotes
12026
12436
  });
12027
- const bytes = await fs24.readFile(contextPath(projectRoot), "utf8");
12437
+ const bytes = await fs25.readFile(contextPath(projectRoot), "utf8");
12028
12438
  const hash = hashNormalizedContent(bytes);
12029
12439
  await recordLastPull(projectRoot, "context:root", hash);
12030
12440
  await recordLastPush(projectRoot, "context:root", hash);
@@ -12539,14 +12949,14 @@ var init_repair = __esm({
12539
12949
  });
12540
12950
 
12541
12951
  // ../../packages/local-store/dist/session-state.js
12542
- import { randomUUID as randomUUID7 } from "crypto";
12952
+ import { randomUUID as randomUUID8 } from "crypto";
12543
12953
  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";
12544
12954
  import os2 from "os";
12545
- import path18 from "path";
12955
+ import path19 from "path";
12546
12956
  function quarantineCorruptedSessionState(file, raw) {
12547
12957
  try {
12548
- const dir = path18.dirname(file);
12549
- const quarantine = path18.join(dir, `session-state.corrupted.${Date.now()}.json`);
12958
+ const dir = path19.dirname(file);
12959
+ const quarantine = path19.join(dir, `session-state.corrupted.${Date.now()}.json`);
12550
12960
  atomicWriteFileSync(quarantine, raw);
12551
12961
  } catch {
12552
12962
  }
@@ -12555,8 +12965,8 @@ function withSessionStateLock(filePath, fn) {
12555
12965
  const lockPath = `${filePath}.lock`;
12556
12966
  const deadline = Date.now() + 5e3;
12557
12967
  const staleMs = 15e3;
12558
- const token = randomUUID7();
12559
- mkdirSync2(path18.dirname(lockPath), { recursive: true });
12968
+ const token = randomUUID8();
12969
+ mkdirSync2(path19.dirname(lockPath), { recursive: true });
12560
12970
  const tryCreate = () => {
12561
12971
  try {
12562
12972
  const fd = openSync2(lockPath, "wx", 420);
@@ -12630,242 +13040,67 @@ function readSessionState(projectRoot) {
12630
13040
  const file = sessionStatePath(projectRoot);
12631
13041
  if (!existsSync2(file))
12632
13042
  return null;
12633
- try {
12634
- const raw = readFileSync(file, "utf-8");
12635
- if (!raw.trim())
12636
- return null;
12637
- const parsed = JSON.parse(raw);
12638
- if (parsed && typeof parsed === "object" && parsed.version === 1) {
12639
- return parsed;
12640
- }
12641
- quarantineCorruptedSessionState(file, raw);
12642
- return null;
12643
- } catch {
12644
- try {
12645
- const raw = readFileSync(file, "utf-8");
12646
- if (raw.trim()) {
12647
- quarantineCorruptedSessionState(file, raw);
12648
- }
12649
- } catch {
12650
- }
12651
- return null;
12652
- }
12653
- }
12654
- function updateSessionState(projectRoot, patch) {
12655
- const file = sessionStatePath(projectRoot);
12656
- const cacheDir2 = path18.dirname(file);
12657
- if (!existsSync2(cacheDir2)) {
12658
- mkdirSync2(cacheDir2, { recursive: true });
12659
- }
12660
- return withSessionStateLock(file, () => {
12661
- const existing = readSessionState(projectRoot);
12662
- const now = (/* @__PURE__ */ new Date()).toISOString();
12663
- const cleanRunPatch = patch.active_run ? Object.fromEntries(Object.entries(patch.active_run).filter(([_, v]) => v !== void 0)) : void 0;
12664
- const cleanFindingPatch = patch.active_finding ? Object.fromEntries(Object.entries(patch.active_finding).filter(([_, v]) => v !== void 0)) : void 0;
12665
- const next = {
12666
- version: 1,
12667
- updated_at: now,
12668
- active_scenario: patch.active_scenario ? { slug: patch.active_scenario.slug, updated_at: now } : existing?.active_scenario,
12669
- active_run: patch.active_run ? {
12670
- id: patch.active_run.id ?? existing?.active_run?.id ?? "",
12671
- scenario_slug: patch.active_run.scenario_slug ?? existing?.active_run?.scenario_slug ?? "",
12672
- status: patch.active_run.status ?? existing?.active_run?.status ?? "running",
12673
- ...existing?.active_run ?? {},
12674
- ...cleanRunPatch,
12675
- updated_at: now
12676
- } : existing?.active_run,
12677
- active_finding: patch.active_finding ? {
12678
- slug: patch.active_finding.slug,
12679
- ...existing?.active_finding ?? {},
12680
- ...cleanFindingPatch,
12681
- updated_at: now
12682
- } : existing?.active_finding,
12683
- active_persona: patch.active_persona ? { slug: patch.active_persona.slug, updated_at: now } : existing?.active_persona
12684
- };
12685
- atomicWriteFileSync(file, JSON.stringify(next, null, 2) + "\n");
12686
- return next;
12687
- });
12688
- }
12689
- var init_session_state = __esm({
12690
- "../../packages/local-store/dist/session-state.js"() {
12691
- "use strict";
12692
- init_atomic();
12693
- init_paths();
12694
- }
12695
- });
12696
-
12697
- // ../../packages/local-store/dist/directions.js
12698
- import { randomUUID as randomUUID8 } from "crypto";
12699
- import { promises as fs25 } from "fs";
12700
- import path19 from "path";
12701
- import { z as z26 } from "zod";
12702
- function directionsDir(projectRoot) {
12703
- return path19.join(iamuDir(projectRoot), DIRECTIONS_DIR);
12704
- }
12705
- function directionPath(projectRoot, slug) {
12706
- return path19.join(directionsDir(projectRoot), `${slug}.md`);
12707
- }
12708
- function renderBody(texts) {
12709
- return [
12710
- "## Problem",
12711
- "",
12712
- texts.problem.trim(),
12713
- "",
12714
- "## Change",
12715
- "",
12716
- texts.change.trim(),
12717
- "",
12718
- "## Leading practice",
12719
- "",
12720
- texts.leading_practice.trim(),
12721
- ""
12722
- ].join("\n");
12723
- }
12724
- function section(body, heading) {
12725
- const re = new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, "m");
12726
- return re.exec(body)?.[1]?.trim() ?? "";
12727
- }
12728
- function parseDirection(raw, file) {
12729
- const { frontmatter, body } = parseFrontmatter(raw);
12730
- return {
12731
- frontmatter: directionFrontmatterSchema.parse(frontmatter),
12732
- texts: {
12733
- problem: section(body, "Problem"),
12734
- change: section(body, "Change"),
12735
- leading_practice: section(body, "Leading practice")
12736
- },
12737
- path: file
12738
- };
12739
- }
12740
- async function readDirection(projectRoot, slug) {
12741
- const file = directionPath(projectRoot, slug);
12742
- let raw;
12743
- try {
12744
- raw = await fs25.readFile(file, "utf8");
12745
- } catch (err) {
12746
- if (err.code === "ENOENT")
12747
- return null;
12748
- throw err;
12749
- }
12750
- return parseDirection(raw, file);
12751
- }
12752
- async function listDirections(projectRoot) {
12753
- let names2;
12754
- try {
12755
- names2 = await fs25.readdir(directionsDir(projectRoot));
12756
- } catch (err) {
12757
- if (err.code === "ENOENT")
12758
- return [];
12759
- throw err;
12760
- }
12761
- const out = [];
12762
- for (const name of names2.sort()) {
12763
- if (!name.endsWith(".md"))
12764
- continue;
12765
- const file = path19.join(directionsDir(projectRoot), name);
13043
+ try {
13044
+ const raw = readFileSync(file, "utf-8");
13045
+ if (!raw.trim())
13046
+ return null;
13047
+ const parsed = JSON.parse(raw);
13048
+ if (parsed && typeof parsed === "object" && parsed.version === 1) {
13049
+ return parsed;
13050
+ }
13051
+ quarantineCorruptedSessionState(file, raw);
13052
+ return null;
13053
+ } catch {
12766
13054
  try {
12767
- out.push(parseDirection(await fs25.readFile(file, "utf8"), file));
13055
+ const raw = readFileSync(file, "utf-8");
13056
+ if (raw.trim()) {
13057
+ quarantineCorruptedSessionState(file, raw);
13058
+ }
12768
13059
  } catch {
12769
13060
  }
13061
+ return null;
12770
13062
  }
12771
- return out.sort((a, b) => b.frontmatter.updated_at.localeCompare(a.frontmatter.updated_at));
12772
- }
12773
- async function writeDirection(projectRoot, fm, texts) {
12774
- const file = directionPath(projectRoot, fm.slug);
12775
- const validated = directionFrontmatterSchema.parse(fm);
12776
- await atomicWriteFile(file, serializeFrontmatter(validated, renderBody(texts)));
12777
- return file;
12778
13063
  }
12779
- async function upsertDirection(projectRoot, proposal, source, advisedBy, now = (/* @__PURE__ */ new Date()).toISOString()) {
12780
- const existing = await readDirection(projectRoot, proposal.slug);
12781
- const texts = {
12782
- problem: proposal.problem,
12783
- change: proposal.change,
12784
- leading_practice: proposal.leading_practice
12785
- };
12786
- if (existing?.frontmatter.status === "rejected") {
12787
- return { outcome: "previously_rejected" };
13064
+ function updateSessionState(projectRoot, patch) {
13065
+ const file = sessionStatePath(projectRoot);
13066
+ const cacheDir2 = path19.dirname(file);
13067
+ if (!existsSync2(cacheDir2)) {
13068
+ mkdirSync2(cacheDir2, { recursive: true });
12788
13069
  }
12789
- const prev = existing?.frontmatter;
12790
- const fm = {
12791
- id: prev?.id ?? randomUUID8(),
12792
- slug: proposal.slug,
12793
- title: proposal.title,
12794
- status: prev?.status ?? "proposed",
12795
- size: proposal.size,
12796
- absorbs: union(prev?.absorbs ?? [], proposal.absorbs, (x) => x),
12797
- reach: union(prev?.reach ?? [], proposal.reach, (r) => `${r.scenario}:${r.effect}`),
12798
- verify: union(prev?.verify ?? [], proposal.verify, (x) => x),
12799
- builds_on: union(prev?.builds_on ?? [], proposal.builds_on, (r) => `${r.path}#${r.symbol}`),
12800
- seen_in_runs: union(prev?.seen_in_runs ?? [], [source], (x) => x),
12801
- advised_by: advisedBy,
12802
- created_at: prev?.created_at ?? now,
12803
- updated_at: now
12804
- };
12805
- const file = await writeDirection(projectRoot, fm, texts);
12806
- return {
12807
- outcome: existing ? "updated" : "created",
12808
- direction: { frontmatter: fm, texts, path: file }
12809
- };
12810
- }
12811
- async function setDirectionStatus(projectRoot, slug, status2, now = (/* @__PURE__ */ new Date()).toISOString()) {
12812
- const existing = await readDirection(projectRoot, slug);
12813
- if (!existing)
12814
- return null;
12815
- const fm = { ...existing.frontmatter, status: status2, updated_at: now };
12816
- await writeDirection(projectRoot, fm, existing.texts);
12817
- return { ...existing, frontmatter: fm };
13070
+ return withSessionStateLock(file, () => {
13071
+ const existing = readSessionState(projectRoot);
13072
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13073
+ const cleanRunPatch = patch.active_run ? Object.fromEntries(Object.entries(patch.active_run).filter(([_, v]) => v !== void 0)) : void 0;
13074
+ const cleanFindingPatch = patch.active_finding ? Object.fromEntries(Object.entries(patch.active_finding).filter(([_, v]) => v !== void 0)) : void 0;
13075
+ const next = {
13076
+ version: 1,
13077
+ updated_at: now,
13078
+ active_scenario: patch.active_scenario ? { slug: patch.active_scenario.slug, updated_at: now } : existing?.active_scenario,
13079
+ active_run: patch.active_run ? {
13080
+ id: patch.active_run.id ?? existing?.active_run?.id ?? "",
13081
+ scenario_slug: patch.active_run.scenario_slug ?? existing?.active_run?.scenario_slug ?? "",
13082
+ status: patch.active_run.status ?? existing?.active_run?.status ?? "running",
13083
+ ...existing?.active_run ?? {},
13084
+ ...cleanRunPatch,
13085
+ updated_at: now
13086
+ } : existing?.active_run,
13087
+ active_finding: patch.active_finding ? {
13088
+ slug: patch.active_finding.slug,
13089
+ ...existing?.active_finding ?? {},
13090
+ ...cleanFindingPatch,
13091
+ updated_at: now
13092
+ } : existing?.active_finding,
13093
+ active_persona: patch.active_persona ? { slug: patch.active_persona.slug, updated_at: now } : existing?.active_persona
13094
+ };
13095
+ atomicWriteFileSync(file, JSON.stringify(next, null, 2) + "\n");
13096
+ return next;
13097
+ });
12818
13098
  }
12819
- var DIRECTIONS_DIR, DIRECTION_STATUSES, directionStatusSchema, directionSizeSchema, directionReachSchema, codeRefSchema, directionFrontmatterSchema, union;
12820
- var init_directions = __esm({
12821
- "../../packages/local-store/dist/directions.js"() {
13099
+ var init_session_state = __esm({
13100
+ "../../packages/local-store/dist/session-state.js"() {
12822
13101
  "use strict";
12823
13102
  init_atomic();
12824
- init_markdown();
12825
13103
  init_paths();
12826
- DIRECTIONS_DIR = "directions";
12827
- DIRECTION_STATUSES = [
12828
- "proposed",
12829
- "accepted",
12830
- "done",
12831
- "rejected"
12832
- ];
12833
- directionStatusSchema = z26.enum(DIRECTION_STATUSES);
12834
- directionSizeSchema = z26.enum(["small", "medium", "large"]);
12835
- directionReachSchema = z26.object({
12836
- scenario: z26.string().min(1),
12837
- effect: z26.enum(["helps", "risk"]),
12838
- reason: z26.string().min(1)
12839
- });
12840
- codeRefSchema = z26.object({
12841
- path: z26.string().min(1),
12842
- symbol: z26.string().min(1)
12843
- });
12844
- directionFrontmatterSchema = z26.object({
12845
- id: z26.string().uuid(),
12846
- slug: z26.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
12847
- title: z26.string().min(1),
12848
- status: directionStatusSchema.default("proposed"),
12849
- size: directionSizeSchema,
12850
- /** Finding ids this direction would resolve. */
12851
- absorbs: z26.array(z26.string().min(1)).default([]),
12852
- /** Predicted effect on other scenarios — never evidence. */
12853
- reach: z26.array(directionReachSchema).default([]),
12854
- /** Scenarios to re-run once the direction lands. */
12855
- verify: z26.array(z26.string().min(1)).default([]),
12856
- builds_on: z26.array(codeRefSchema).default([]),
12857
- /** Runs (or `project:<timestamp>`) whose analysis proposed it. */
12858
- seen_in_runs: z26.array(z26.string().min(1)).default([]),
12859
- advised_by: z26.string().regex(/^advisor@\S+$/),
12860
- created_at: z26.string(),
12861
- updated_at: z26.string()
12862
- });
12863
- union = (a, b, key) => {
12864
- const seen = /* @__PURE__ */ new Map();
12865
- for (const x of [...a, ...b])
12866
- seen.set(key(x), x);
12867
- return [...seen.values()];
12868
- };
12869
13104
  }
12870
13105
  });
12871
13106
 
@@ -12994,7 +13229,7 @@ async function readContextSummary(projectRoot) {
12994
13229
  return null;
12995
13230
  }
12996
13231
  }
12997
- function pagePath(url) {
13232
+ function pagePath2(url) {
12998
13233
  try {
12999
13234
  return new URL(url).pathname || "/";
13000
13235
  } catch {
@@ -13018,7 +13253,7 @@ async function walkedCoverage(projectRoot) {
13018
13253
  const entry = out.get(slug) ?? { paths: /* @__PURE__ */ new Set(), runs: 0 };
13019
13254
  entry.runs++;
13020
13255
  for (const step of await readStepsForRun(projectRoot, name)) {
13021
- const p = typeof step.url === "string" ? pagePath(step.url) : null;
13256
+ const p = typeof step.url === "string" ? pagePath2(step.url) : null;
13022
13257
  if (p)
13023
13258
  entry.paths.add(p);
13024
13259
  }
@@ -13673,6 +13908,7 @@ var init_dist2 = __esm({
13673
13908
  init_revisit();
13674
13909
  init_explore_map();
13675
13910
  init_walkthrough_canonical2();
13911
+ init_report_markdown();
13676
13912
  init_journey_diff();
13677
13913
  init_finding_match();
13678
13914
  init_guidance2();
@@ -13707,15 +13943,15 @@ function resolveApiUrl(configured) {
13707
13943
  assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
13708
13944
  return url;
13709
13945
  }
13710
- function parseStore(raw, path43) {
13946
+ function parseStore(raw, path44) {
13711
13947
  let parsed;
13712
13948
  try {
13713
13949
  parsed = JSON.parse(raw);
13714
13950
  } catch (err) {
13715
- throw new CredentialsFileError(path43, "not valid JSON", err);
13951
+ throw new CredentialsFileError(path44, "not valid JSON", err);
13716
13952
  }
13717
13953
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13718
- throw new CredentialsFileError(path43, "expected a JSON object");
13954
+ throw new CredentialsFileError(path44, "expected a JSON object");
13719
13955
  }
13720
13956
  return parsed;
13721
13957
  }
@@ -13723,26 +13959,26 @@ function isNotFound(err) {
13723
13959
  return typeof err === "object" && err !== null && err.code === "ENOENT";
13724
13960
  }
13725
13961
  async function readCredentialsStore() {
13726
- const path43 = getCredentialsPath();
13962
+ const path44 = getCredentialsPath();
13727
13963
  let raw;
13728
13964
  try {
13729
- raw = await readFile(path43, "utf-8");
13965
+ raw = await readFile(path44, "utf-8");
13730
13966
  } catch (err) {
13731
13967
  if (isNotFound(err)) return {};
13732
13968
  warnUnreadableStoreOnce(err);
13733
13969
  throw err;
13734
13970
  }
13735
13971
  try {
13736
- return parseStore(raw, path43);
13972
+ return parseStore(raw, path44);
13737
13973
  } catch (err) {
13738
13974
  warnUnreadableStoreOnce(err);
13739
13975
  throw err;
13740
13976
  }
13741
13977
  }
13742
13978
  async function writeCredentialsStore(store) {
13743
- const path43 = getCredentialsPath();
13744
- await mkdir(dirname(path43), { recursive: true, mode: 448 });
13745
- await atomicWriteFile(path43, `${JSON.stringify(store, null, 2)}
13979
+ const path44 = getCredentialsPath();
13980
+ await mkdir(dirname(path44), { recursive: true, mode: 448 });
13981
+ await atomicWriteFile(path44, `${JSON.stringify(store, null, 2)}
13746
13982
  `, {
13747
13983
  mode: 384
13748
13984
  });
@@ -13762,16 +13998,16 @@ function resolveApiKey(apiUrl) {
13762
13998
  } catch {
13763
13999
  return null;
13764
14000
  }
13765
- const path43 = getCredentialsPath();
14001
+ const path44 = getCredentialsPath();
13766
14002
  let raw;
13767
14003
  try {
13768
- raw = readFileSync2(path43, "utf-8");
14004
+ raw = readFileSync2(path44, "utf-8");
13769
14005
  } catch (err) {
13770
14006
  if (!isNotFound(err)) warnUnreadableStoreOnce(err);
13771
14007
  return null;
13772
14008
  }
13773
14009
  try {
13774
- const entry = parseStore(raw, path43)[url];
14010
+ const entry = parseStore(raw, path44)[url];
13775
14011
  if (typeof entry !== "object" || entry === null) return null;
13776
14012
  const token = entry.token;
13777
14013
  return typeof token === "string" && token ? token : null;
@@ -13810,12 +14046,12 @@ var init_credentials = __esm({
13810
14046
  * @param reason - What is wrong with it.
13811
14047
  * @param cause - Underlying parse error, if any.
13812
14048
  */
13813
- constructor(path43, reason, cause) {
14049
+ constructor(path44, reason, cause) {
13814
14050
  super(
13815
- `Credentials file ${path43} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
14051
+ `Credentials file ${path44} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
13816
14052
  {
13817
14053
  cause,
13818
- suggestedAction: `Delete ${path43} and run \`iamu auth login\`, or set IAMU_TOKEN.`
14054
+ suggestedAction: `Delete ${path44} and run \`iamu auth login\`, or set IAMU_TOKEN.`
13819
14055
  }
13820
14056
  );
13821
14057
  }
@@ -14701,8 +14937,8 @@ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
14701
14937
  import { fileURLToPath as fileURLToPath2 } from "url";
14702
14938
  import chalk6 from "chalk";
14703
14939
  function getVersion() {
14704
- if ("0.4.0-beta.0") {
14705
- return "0.4.0-beta.0";
14940
+ if ("0.4.0-beta.1") {
14941
+ return "0.4.0-beta.1";
14706
14942
  }
14707
14943
  try {
14708
14944
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -14809,16 +15045,16 @@ async function needsWrite(filePath, content) {
14809
15045
  return true;
14810
15046
  }
14811
15047
  }
14812
- function isCliWrittenSkill(path43) {
15048
+ function isCliWrittenSkill(path44) {
14813
15049
  try {
14814
- return /^generatedBy:/m.test(readFileSync4(path43, "utf-8"));
15050
+ return /^generatedBy:/m.test(readFileSync4(path44, "utf-8"));
14815
15051
  } catch {
14816
15052
  return false;
14817
15053
  }
14818
15054
  }
14819
- function isCliWrittenPrompt(path43) {
15055
+ function isCliWrittenPrompt(path44) {
14820
15056
  try {
14821
- const content = readFileSync4(path43, "utf-8");
15057
+ const content = readFileSync4(path44, "utf-8");
14822
15058
  return content.includes("iamu") || /^generatedBy:/m.test(content);
14823
15059
  } catch {
14824
15060
  return false;
@@ -14830,8 +15066,8 @@ function planSkillFiles(hosts, projectRoot, version2) {
14830
15066
  for (const host of hosts) {
14831
15067
  if (pluginProvidesSkills(host, projectRoot)) {
14832
15068
  for (const name of skillTemplates.keys()) {
14833
- const path43 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
14834
- if (isCliWrittenSkill(path43)) files.push({ host, path: path43, content: null });
15069
+ const path44 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
15070
+ if (isCliWrittenSkill(path44)) files.push({ host, path: path44, content: null });
14835
15071
  }
14836
15072
  } else if (host.supportsSkills) {
14837
15073
  const skillsDir = host.getSkillsDir(projectRoot);
@@ -16388,8 +16624,17 @@ function lead(text) {
16388
16624
  }
16389
16625
  function severityCell(severity) {
16390
16626
  const s = (severity ?? "\u2014").toUpperCase();
16391
- const cell = padRight(s, 8);
16392
- return severity === "critical" ? chalk12.red(cell) : severity === "major" ? chalk12.yellow(cell) : chalk12.dim(cell);
16627
+ const cell2 = padRight(s, 8);
16628
+ return severity === "critical" ? chalk12.red(cell2) : severity === "major" ? chalk12.yellow(cell2) : chalk12.dim(cell2);
16629
+ }
16630
+ function severityMix(r) {
16631
+ if (r.problems === 0) return chalk12.green("no problems");
16632
+ const { critical, major, minor } = r.bySeverity;
16633
+ return [
16634
+ critical > 0 ? chalk12.red(`${critical} critical`) : "",
16635
+ major > 0 ? chalk12.yellow(`${major} major`) : "",
16636
+ minor > 0 ? chalk12.dim(`${minor} minor`) : ""
16637
+ ].filter(Boolean).join(" ");
16393
16638
  }
16394
16639
  function deltaCell(r) {
16395
16640
  const parts = [
@@ -16407,6 +16652,13 @@ function renderRunView(v, width = 100) {
16407
16652
  );
16408
16653
  const meta = [shortDate(v.completedAt), `${v.steps} steps`, v.targetUrl].filter(Boolean).join(" \xB7 ");
16409
16654
  lines.push(` ${chalk12.dim(meta)}`);
16655
+ if (v.checks) {
16656
+ const parts = [
16657
+ 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` : "",
16658
+ v.checks.directions.length > 0 ? `after ${v.checks.directions.length} direction${v.checks.directions.length === 1 ? "" : "s"} landed` : ""
16659
+ ].filter(Boolean);
16660
+ lines.push(` ${chalk12.dim(`This run ${parts.join(", ")}`)}`);
16661
+ }
16410
16662
  lines.push("");
16411
16663
  const color = verdictColor(v.verdict.label);
16412
16664
  const question = v.verdict.question ?? "Would they come back?";
@@ -16434,12 +16686,21 @@ function renderRunView(v, width = 100) {
16434
16686
  );
16435
16687
  for (const item of s.items) {
16436
16688
  const mark = SINCE_MARKS[item.outcome];
16437
- lines.push(` ${mark} ${truncate(item.title, w - 20)}`);
16689
+ const qualifiers = [
16690
+ item.outcome === "persists" ? item.shift : "",
16691
+ item.outcome === "persists" && item.fixedBefore ? "fix did not hold" : ""
16692
+ ].filter(Boolean);
16693
+ const held = qualifiers.length > 0 ? (item.fixedBefore ? chalk12.red : chalk12.dim)(
16694
+ ` (${qualifiers.join("; ")})`
16695
+ ) : "";
16696
+ lines.push(
16697
+ ` ${mark} ${truncate(item.title, w - 18 - stripAnsi(held).length)}${held}`
16698
+ );
16438
16699
  }
16439
16700
  }
16440
16701
  lines.push("");
16441
16702
  const problems = [...v.findings].sort(
16442
- (a, b) => SEVERITY_ORDER.indexOf(a.severity ?? "") - SEVERITY_ORDER.indexOf(b.severity ?? "") || TYPE_ORDER.indexOf(a.type) - TYPE_ORDER.indexOf(b.type)
16703
+ (a, b) => SEVERITY_ORDER2.indexOf(a.severity ?? "") - SEVERITY_ORDER2.indexOf(b.severity ?? "") || TYPE_ORDER2.indexOf(a.type) - TYPE_ORDER2.indexOf(b.type)
16443
16704
  );
16444
16705
  const strengthNote = v.strengths.length > 0 ? `, ${v.strengths.length} strength${v.strengths.length === 1 ? "" : "s"}` : "";
16445
16706
  lines.push(
@@ -16450,15 +16711,23 @@ function renderRunView(v, width = 100) {
16450
16711
  24,
16451
16712
  Math.max(0, ...problems.map((f) => (f.where ?? "").length))
16452
16713
  );
16453
- const titleWidth = Math.max(24, w - 4 - 3 - 9 - 10 - whereWidth - 2 - 9);
16714
+ const tagWidth = problems.some((f) => f.persists) ? 9 : 0;
16715
+ const titleWidth = Math.max(
16716
+ 24,
16717
+ w - 4 - 3 - 9 - 10 - whereWidth - 2 - tagWidth
16718
+ );
16454
16719
  problems.forEach((f, i) => {
16455
16720
  const where = chalk12.dim(
16456
16721
  padRight(truncate(f.where ?? "", whereWidth), whereWidth)
16457
16722
  );
16458
16723
  const tag = f.persists ? chalk12.red(" persists") : "";
16724
+ const titleLines = wrap(f.title, titleWidth, "");
16459
16725
  lines.push(
16460
- ` ${padRight(String(i + 1), 3)}${severityCell(f.severity)} ${padRight(chalk12.dim(f.type), 9)} ${padRight(truncate(f.title, titleWidth), titleWidth)} ${where}${tag}`
16726
+ ` ${padRight(String(i + 1), 3)}${severityCell(f.severity)} ${padRight(chalk12.dim(f.type), 9)} ${padRight(titleLines[0] ?? "", titleWidth)} ${where}${tag}`
16461
16727
  );
16728
+ for (const more of titleLines.slice(1)) {
16729
+ lines.push(`${" ".repeat(26)}${more}`);
16730
+ }
16462
16731
  if (f.recommendation && (f.severity === "critical" || f.severity === "major")) {
16463
16732
  const fix = wrap(`\u2192 ${lead(f.recommendation)}`, w - 30, " ".repeat(26));
16464
16733
  if (fix.length > 2) {
@@ -16484,8 +16753,13 @@ function renderRunView(v, width = 100) {
16484
16753
  );
16485
16754
  }
16486
16755
  }
16487
- if (v.observations.length > 0) {
16756
+ if (v.observations.length > 0 || v.setupNotes.length > 0) {
16488
16757
  lines.push("", ` ${chalk12.bold("Also worth knowing")}`);
16758
+ for (const n of v.setupNotes) {
16759
+ lines.push(
16760
+ ` ${chalk12.dim("\xB7")} ${chalk12.dim("Test setup:")} ${truncate(n, w - 22)}`
16761
+ );
16762
+ }
16489
16763
  for (const o of v.observations) {
16490
16764
  const label = o.kind === "coverage_gap" ? "Not walked" : "Feature risk";
16491
16765
  lines.push(
@@ -16520,7 +16794,7 @@ function renderOverview(v, width = 100) {
16520
16794
  }
16521
16795
  for (const r of shown) {
16522
16796
  lines.push(
16523
- ` ${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)}`
16797
+ ` ${padRight(chalk12.dim(shortDate(r.completedAt)), 13)} ${verdictColor(r.label)(padRight(r.label, 8))} ${padRight(severityMix(r), 24)} ${deltaCell(r)}`
16524
16798
  );
16525
16799
  }
16526
16800
  if (s.latestAnswer) {
@@ -16549,7 +16823,7 @@ function renderOverview(v, width = 100) {
16549
16823
  );
16550
16824
  return lines.map((l) => l.trimEnd());
16551
16825
  }
16552
- var SINCE_MARKS, SEVERITY_ORDER, TYPE_ORDER;
16826
+ var SINCE_MARKS, SEVERITY_ORDER2, TYPE_ORDER2;
16553
16827
  var init_report_view = __esm({
16554
16828
  "src/ui/report-view.ts"() {
16555
16829
  "use strict";
@@ -16560,8 +16834,8 @@ var init_report_view = __esm({
16560
16834
  not_seen: chalk12.dim(padRight("\u25CB not seen", 12)),
16561
16835
  uncertain: chalk12.dim(padRight("? unclear", 12))
16562
16836
  };
16563
- SEVERITY_ORDER = ["critical", "major", "minor", "observation"];
16564
- TYPE_ORDER = ["incorrect", "hinders", "missing", "irrelevant"];
16837
+ SEVERITY_ORDER2 = ["critical", "major", "minor", "observation"];
16838
+ TYPE_ORDER2 = ["incorrect", "hinders", "missing", "irrelevant"];
16565
16839
  }
16566
16840
  });
16567
16841
 
@@ -16950,9 +17224,7 @@ ${formatIssues(fileResult.error)}`,
16950
17224
  console.log(
16951
17225
  ` ${chalk14.dim("verdicts:")} ${path23.relative(projectRoot, verdictsPath)}`
16952
17226
  );
16953
- console.log(
16954
- ` ${chalk14.dim("report:")} ${path23.relative(projectRoot, reportPath)}`
16955
- );
17227
+ console.log(` ${chalk14.dim("report:")} ${reportPath}`);
16956
17228
  printCloudRunInfo(cloudInfo);
16957
17229
  await promptViewResults(projectRoot, { ...opts, runId });
16958
17230
  }
@@ -17008,7 +17280,10 @@ function sinceItems(v) {
17008
17280
  return groups.flatMap(
17009
17281
  ([outcome, items]) => (items ?? []).map((i) => ({
17010
17282
  title: i.title ?? i.canonical_id ?? "untitled",
17011
- outcome
17283
+ outcome,
17284
+ note: i.note ?? null,
17285
+ fixedBefore: Boolean(i.fixed_before),
17286
+ shift: outcome === "persists" ? severityShift(i.severity_before, i.severity_now) : ""
17012
17287
  }))
17013
17288
  );
17014
17289
  }
@@ -17037,11 +17312,8 @@ async function readCompletedRuns(projectRoot, runIds, scenario) {
17037
17312
  return out;
17038
17313
  }
17039
17314
  function verdictLabel(c) {
17040
- const judged = (c?.human_tests ?? []).map((t) => t.verdict).filter((v) => v === "yes" || v === "no" || v === "partial");
17041
- if (judged.length === 0) return "PENDING";
17042
- if (judged.every((v) => v === "yes")) return "YES";
17043
- if (judged.every((v) => v === "no")) return "NO";
17044
- return "PARTIAL";
17315
+ const v = runVerdict(c);
17316
+ return v ? v.toUpperCase() : "PENDING";
17045
17317
  }
17046
17318
  function testLabel(verdict) {
17047
17319
  return verdict === "yes" ? "YES" : verdict === "no" ? "NO" : verdict === "partial" ? "PARTIAL" : "PENDING";
@@ -17095,6 +17367,17 @@ async function openDirections(projectRoot) {
17095
17367
  (d) => d.frontmatter.status === "proposed" || d.frontmatter.status === "accepted"
17096
17368
  );
17097
17369
  }
17370
+ function checksOf(c) {
17371
+ const checks = c?.checks;
17372
+ if (!c || !checks) return null;
17373
+ const fixes = checkOutcomes(c);
17374
+ return {
17375
+ fixes: fixes.length,
17376
+ fixed: fixes.filter((f) => f.outcome === "fixed").length,
17377
+ cameBack: fixes.filter((f) => f.outcome === "came_back").length,
17378
+ directions: checks.directions
17379
+ };
17380
+ }
17098
17381
  async function buildRunView(projectRoot, run) {
17099
17382
  const c = run.canonical;
17100
17383
  const slugs = slugsOf(run);
@@ -17110,12 +17393,13 @@ async function buildRunView(projectRoot, run) {
17110
17393
  const v = verificationOf(c);
17111
17394
  const persisting = new Set(v?.persists.map((p) => p.canonical_id) ?? []);
17112
17395
  const added = new Set(v?.new_findings.map((n) => n.id) ?? []);
17396
+ const stepUrl2 = new Map((c?.steps ?? []).map((st) => [st.id, st.url]));
17113
17397
  const findings2 = [];
17114
17398
  const strengths = [];
17115
17399
  for (const f of c?.findings ?? []) {
17116
17400
  const entity = byId.get(f.id);
17117
17401
  const fm = entity?.frontmatter;
17118
- const title = fm?.title ?? firstSentence(f.ui_fact ?? fm?.ui_fact ?? f.id);
17402
+ const title = f.title ?? fm?.title ?? firstSentence(f.ui_fact ?? fm?.ui_fact ?? f.id);
17119
17403
  if (f.type === "helps") {
17120
17404
  strengths.push(title);
17121
17405
  continue;
@@ -17129,7 +17413,7 @@ async function buildRunView(projectRoot, run) {
17129
17413
  recommendation: f.recommendation || null,
17130
17414
  isNew: added.has(f.id),
17131
17415
  where: whereOf(
17132
- fm?.page_url,
17416
+ f.page_url ?? fm?.page_url ?? stepUrl2.get(f.linked_step_ids?.[0] ?? ""),
17133
17417
  fm?.walkthrough_step_id ?? f.linked_step_ids?.[0]
17134
17418
  ),
17135
17419
  persists: Boolean(f.canonical_id && persisting.has(f.canonical_id))
@@ -17172,7 +17456,7 @@ async function buildRunView(projectRoot, run) {
17172
17456
  answer: overall?.answer ?? null
17173
17457
  },
17174
17458
  tests: tests.map((t) => ({
17175
- question: t.question,
17459
+ question: stripAnswerHint(t.question),
17176
17460
  label: testLabel(t.verdict)
17177
17461
  })),
17178
17462
  since: v ? {
@@ -17191,9 +17475,17 @@ async function buildRunView(projectRoot, run) {
17191
17475
  absorbs: d.absorbs?.length ?? 0,
17192
17476
  verify: d.verify?.length ?? 0
17193
17477
  })),
17194
- observations: analysis?.observations ?? []
17478
+ observations: analysis?.observations ?? [],
17479
+ checks: checksOf(c),
17480
+ setupNotes: c?.setup_notes ?? []
17195
17481
  };
17196
17482
  }
17483
+ function severityCounts(c) {
17484
+ const problems = (c?.findings ?? []).filter(isProblem);
17485
+ const critical = problems.filter((f) => f.severity === "critical").length;
17486
+ const major = problems.filter((f) => f.severity === "major").length;
17487
+ return { critical, major, minor: problems.length - critical - major };
17488
+ }
17197
17489
  async function buildOverview(projectRoot, runs) {
17198
17490
  const groups = /* @__PURE__ */ new Map();
17199
17491
  for (const r of runs) {
@@ -17220,6 +17512,7 @@ async function buildOverview(projectRoot, runs) {
17220
17512
  completedAt: r.manifest.completed_at ?? null,
17221
17513
  label: verdictLabel(r.canonical),
17222
17514
  problems: (r.canonical?.findings ?? []).filter(isProblem).length,
17515
+ bySeverity: severityCounts(r.canonical),
17223
17516
  resolved: v?.resolved.length ?? 0,
17224
17517
  persists: v?.persists.length ?? 0,
17225
17518
  added: v?.new_findings.filter(isProblem).length ?? 0
@@ -17271,6 +17564,10 @@ __export(html_report_exports, {
17271
17564
  });
17272
17565
  import { promises as fs31 } from "fs";
17273
17566
  import path25 from "path";
17567
+ function firstSentenceOf(text) {
17568
+ if (!text) return null;
17569
+ return /^(.+?[.!?])(\s|$)/.exec(text.trim())?.[1] ?? text.trim();
17570
+ }
17274
17571
  function esc(str2) {
17275
17572
  if (!str2) return "";
17276
17573
  return str2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -17377,6 +17674,70 @@ function renderStepScreenshotHtml(s) {
17377
17674
  </div>
17378
17675
  `;
17379
17676
  }
17677
+ function sortProblems(list2) {
17678
+ const r = (v, order) => {
17679
+ const i = order.indexOf(v ?? "");
17680
+ return i === -1 ? order.length : i;
17681
+ };
17682
+ return [...list2].sort(
17683
+ (a, b) => r(a.severity, SEVERITY_RANK) - r(b.severity, SEVERITY_RANK) || r(a.type, TYPE_RANK) - r(b.type, TYPE_RANK)
17684
+ );
17685
+ }
17686
+ function inlineMarkdown(text) {
17687
+ return esc(text).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\n+/g, "<br>");
17688
+ }
17689
+ function persistsLabel(i) {
17690
+ const qualifiers = [
17691
+ severityShift(i.severity_before, i.severity_now),
17692
+ i.fixed_before ? "fix did not hold" : ""
17693
+ ].filter(Boolean);
17694
+ return qualifiers.length > 0 ? `Persists (${qualifiers.join("; ")})` : "Persists";
17695
+ }
17696
+ function renderChecks2(fixes, directions) {
17697
+ const items = fixes.map(
17698
+ (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>`
17699
+ ).join("");
17700
+ 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>` : "";
17701
+ return `
17702
+ <section>
17703
+ <h2>What this run checks</h2>
17704
+ <div class="card">
17705
+ ${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>` : ""}
17706
+ ${landed}
17707
+ </div>
17708
+ </section>`;
17709
+ }
17710
+ function renderSinceTable(v) {
17711
+ const steps = (ids) => ids && ids.length > 0 ? ` (step${ids.length === 1 ? "" : "s"} ${ids.map((id) => /\d+/.exec(id)?.[0] ?? id).join(", ")})` : "";
17712
+ const td = 'style="padding:6px 8px;border-top:1px solid #E5E7EB;vertical-align:top"';
17713
+ 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>`;
17714
+ const rows = [
17715
+ ...v.resolved.map(
17716
+ (i) => row(i, "Resolved", `${i.note ?? ""}${steps(i.step_ids)}`)
17717
+ ),
17718
+ ...v.persists.map(
17719
+ (i) => row(i, persistsLabel(i), `${i.note ?? ""}${steps(i.step_ids)}`)
17720
+ ),
17721
+ ...(v.notObserved ?? []).map(
17722
+ (i) => row(
17723
+ i,
17724
+ "Not seen",
17725
+ `The walk did not pass this way${i.fixed_before ? " (fixed, awaiting a re-check)" : ""}. Not evidence of a fix.`
17726
+ )
17727
+ )
17728
+ ];
17729
+ if (rows.length === 0) return "";
17730
+ return `
17731
+ <section>
17732
+ <h2>Since the last run</h2>
17733
+ <div class="card">
17734
+ <table style="width:100%;border-collapse:collapse;font-size:13px;line-height:1.5">
17735
+ <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>
17736
+ <tbody>${rows.join("")}</tbody>
17737
+ </table>
17738
+ </div>
17739
+ </section>`;
17740
+ }
17380
17741
  function generateHtmlReport(data) {
17381
17742
  const {
17382
17743
  scenarioName,
@@ -17385,6 +17746,7 @@ function generateHtmlReport(data) {
17385
17746
  steps,
17386
17747
  findings: findings2,
17387
17748
  verdicts,
17749
+ overall,
17388
17750
  verification,
17389
17751
  whatWorked,
17390
17752
  openQuestions,
@@ -17395,16 +17757,17 @@ function generateHtmlReport(data) {
17395
17757
  runNumber
17396
17758
  } = data;
17397
17759
  const dateStr = formatDate(completedAt ?? startedAt);
17398
- const criticalCount = findings2.filter(
17399
- (f) => f.severity === "critical" || f.type === "incorrect"
17760
+ const problems = sortProblems(findings2.filter((f) => f.type !== "helps"));
17761
+ const strengths = findings2.filter((f) => f.type === "helps");
17762
+ const criticalCount = problems.filter(
17763
+ (f) => f.severity === "critical"
17400
17764
  ).length;
17401
- const majorCount = findings2.filter(
17402
- (f) => f.severity === "major" || f.severity === "important" || f.type === "hinders" || f.type === "missing"
17765
+ const majorCount = problems.filter((f) => f.severity === "major").length;
17766
+ const minorCount = problems.filter(
17767
+ (f) => f.severity !== "critical" && f.severity !== "major"
17403
17768
  ).length;
17404
- const minorCount = findings2.filter(
17405
- (f) => f.severity === "minor" || f.type === "friction" || !f.severity && f.type !== "helps"
17406
- ).length;
17407
- const helpsCount = findings2.filter((f) => f.type === "helps").length;
17769
+ const helpsCount = strengths.length;
17770
+ const newProblems = verification ? verification.newFindings.filter((n) => n.type !== "helps").length : 0;
17408
17771
  return `<!DOCTYPE html>
17409
17772
  <html lang="en">
17410
17773
  <head>
@@ -17879,16 +18242,27 @@ function generateHtmlReport(data) {
17879
18242
  <div class="verification-notice">
17880
18243
  Since last run: <strong>${verification.resolved.length} resolved</strong> \xB7
17881
18244
  <strong>${verification.persists.length} persists</strong> \xB7
17882
- <strong>${verification.newFindings.length} new</strong>
18245
+ <strong>${newProblems} new</strong>
17883
18246
  </div>` : ""}
17884
18247
  </div>
17885
18248
 
17886
- <!-- The Human Test Scorecard -->
17887
- ${verdicts && verdicts.length > 0 ? `
18249
+ ${data.checks ? renderChecks2(data.checks.fixes, data.checks.directions) : ""}
18250
+
18251
+ <!-- The Human Test: the run's outcome, first -->
18252
+ ${overall || verdicts && verdicts.length > 0 ? `
17888
18253
  <section>
17889
- <h2>The Human Test Checkpoints</h2>
18254
+ <h2>The Human Test</h2>
17890
18255
  <div class="card">
17891
- ${verdicts.map(
18256
+ ${overall ? `
18257
+ <div class="verdict-item">
18258
+ <div class="verdict-header">
18259
+ <span style="font-weight:600;font-size:15px">${esc(overall.question)}</span>
18260
+ <span class="badge badge-${esc(overall.label)}">${esc(overall.label)}</span>
18261
+ </div>
18262
+ ${overall.answer ? `<div class="prose-voice" style="margin-top:0.35rem;font-weight:500">${esc(overall.answer)}</div>` : ""}
18263
+ ${overall.reasoning ? `<div class="prose-voice" style="margin-top:0.35rem">${esc(overall.reasoning)}</div>` : ""}
18264
+ </div>` : ""}
18265
+ ${(verdicts ?? []).map(
17892
18266
  (v) => `
17893
18267
  <div class="verdict-item">
17894
18268
  <div class="verdict-header">
@@ -17903,14 +18277,46 @@ function generateHtmlReport(data) {
17903
18277
  </section>
17904
18278
  ` : ""}
17905
18279
 
18280
+ ${verification ? renderSinceTable(verification) : ""}
18281
+
18282
+ <!-- Findings, worst first -->
18283
+ ${problems.length > 0 ? `
18284
+ <section>
18285
+ <h2>Findings (${problems.length})</h2>
18286
+ ${problems.map(
18287
+ (f, i) => `
18288
+ <div class="finding-item">
18289
+ <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.35rem 0.75rem">
18290
+ <span class="finding-title">${i + 1}. ${esc(f.title)}${f.isNew ? ' <span class="badge">new</span>' : f.persists ? ' <span class="badge">persists</span>' : ""}</span>
18291
+ <span class="badge badge-${esc(f.severity ?? f.type)}">${esc(f.severity ?? f.type)}</span>
18292
+ </div>
18293
+ <div class="finding-meta">${esc([f.type, f.where].filter(Boolean).join(" \xB7 "))}</div>
18294
+ ${f.uiFact ? `<div class="finding-meta">${esc(f.uiFact)}</div>` : ""}
18295
+ ${f.consequence ? `<blockquote class="finding-meta prose-voice">${esc(f.consequence)}${persona?.name ? ` \u2014 ${esc(persona.name)}` : ""}</blockquote>` : ""}
18296
+ ${f.recommendation ? `<div class="finding-meta finding-rec"><strong>Recommendation:</strong> ${inlineMarkdown(f.recommendation)}</div>` : ""}
18297
+ </div>
18298
+ `
18299
+ ).join("")}
18300
+ </section>
18301
+ ` : ""}
18302
+
17906
18303
  <!-- What Worked & Open Questions -->
17907
- ${whatWorked && whatWorked.length > 0 || openQuestions && openQuestions.length > 0 ? `
18304
+ ${strengths.length > 0 || whatWorked && whatWorked.length > 0 || openQuestions && openQuestions.length > 0 || data.setupNotes && data.setupNotes.length > 0 ? `
17908
18305
  <section style="display:flex;gap:1.25rem;flex-wrap:wrap">
17909
- ${whatWorked && whatWorked.length > 0 ? `
18306
+ ${strengths.length > 0 || whatWorked && whatWorked.length > 0 ? `
17910
18307
  <div class="card" style="flex:1;min-width:260px">
17911
18308
  <div class="card-label">What Worked</div>
17912
18309
  <ul style="padding-left:1.25rem;font-size:13px;line-height:1.6">
17913
- ${whatWorked.map((w) => `<li style="margin-bottom:0.25rem">${esc(w)}</li>`).join("")}
18310
+ ${strengths.map((f) => `<li style="margin-bottom:0.25rem"><strong>${esc(f.title)}.</strong> ${esc(f.uiFact)}</li>`).join("")}
18311
+ ${(whatWorked ?? []).map((w) => `<li style="margin-bottom:0.25rem">${esc(w)}</li>`).join("")}
18312
+ </ul>
18313
+ </div>
18314
+ ` : ""}
18315
+ ${data.setupNotes && data.setupNotes.length > 0 ? `
18316
+ <div class="card" style="flex:1;min-width:260px">
18317
+ <div class="card-label">Test Setup Notes</div>
18318
+ <ul style="padding-left:1.25rem;font-size:13px;line-height:1.6">
18319
+ ${data.setupNotes.map((n) => `<li style="margin-bottom:0.25rem">${esc(n)}</li>`).join("")}
17914
18320
  </ul>
17915
18321
  </div>
17916
18322
  ` : ""}
@@ -17925,26 +18331,6 @@ function generateHtmlReport(data) {
17925
18331
  </section>
17926
18332
  ` : ""}
17927
18333
 
17928
- <!-- Findings Overview -->
17929
- ${findings2.length > 0 ? `
17930
- <section>
17931
- <h2>Findings (${findings2.length})</h2>
17932
- ${findings2.map(
17933
- (f) => `
17934
- <div class="finding-item">
17935
- <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.35rem 0.75rem">
17936
- <span class="finding-title">${esc(f.title)}</span>
17937
- <span class="badge badge-${esc(f.severity ?? f.type)}">${esc(f.severity ?? f.type)}</span>
17938
- </div>
17939
- ${f.uiFact ? `<div class="finding-meta"><strong>Observed:</strong> ${esc(f.uiFact)}</div>` : ""}
17940
- ${f.consequence ? `<div class="finding-meta"><strong>Consequence:</strong> ${esc(f.consequence)}</div>` : ""}
17941
- ${f.recommendation ? `<div class="finding-meta finding-rec"><strong>Recommendation:</strong> ${esc(f.recommendation)}</div>` : ""}
17942
- </div>
17943
- `
17944
- ).join("")}
17945
- </section>
17946
- ` : ""}
17947
-
17948
18334
  <!-- Step Timeline with Connected Gutter -->
17949
18335
  ${steps.length > 0 ? `
17950
18336
  <section>
@@ -18012,7 +18398,6 @@ async function buildReportDataFromRun(projectRoot, runId) {
18012
18398
  scenarioSlug
18013
18399
  );
18014
18400
  if (record) {
18015
- scenarioName = record.frontmatter.slug || scenarioSlug;
18016
18401
  scenarioNarrative = record.body?.trim() || null;
18017
18402
  }
18018
18403
  } catch {
@@ -18068,47 +18453,82 @@ async function buildReportDataFromRun(projectRoot, runId) {
18068
18453
  screenshotUrl
18069
18454
  });
18070
18455
  }
18456
+ const names2 = canonical?.scenario;
18457
+ if (names2?.outline_title) {
18458
+ scenarioName = `${names2.persona_name ?? personaData?.name ?? names2.persona_slug} \xB7 ${names2.outline_title}`;
18459
+ }
18460
+ const verification = canonical?.verification;
18461
+ const newIds = new Set((verification?.new_findings ?? []).map((n) => n.id));
18462
+ const persistingIds = new Set(
18463
+ (verification?.persists ?? []).map(
18464
+ (p) => p.canonical_id
18465
+ )
18466
+ );
18071
18467
  const findings2 = (canonical?.findings ?? []).map((f) => {
18072
- const rawF = f;
18073
- const title = typeof rawF.title === "string" && rawF.title.trim().length > 0 ? rawF.title : f.ui_fact || f.id;
18468
+ const step = rawSteps.find((st) => st.id === f.linked_step_ids[0]);
18469
+ let page = null;
18470
+ const url = f.page_url ?? step?.url;
18471
+ if (url) {
18472
+ try {
18473
+ page = new URL(url).pathname;
18474
+ } catch {
18475
+ page = url;
18476
+ }
18477
+ }
18074
18478
  return {
18075
18479
  id: f.id,
18076
- title,
18480
+ title: f.title?.trim() || firstSentenceOf(f.ui_fact) || f.id,
18077
18481
  type: f.type,
18078
18482
  severity: f.severity,
18079
18483
  uiFact: f.ui_fact,
18080
18484
  consequence: f.consequence,
18081
- recommendation: f.recommendation
18485
+ recommendation: f.recommendation,
18486
+ where: [page, step ? `step ${step.step_number}` : null].filter(Boolean).join(" \xB7 ") || null,
18487
+ isNew: newIds.has(f.id),
18488
+ persists: Boolean(f.canonical_id && persistingIds.has(f.canonical_id))
18082
18489
  };
18083
18490
  });
18084
18491
  const verdicts = (canonical?.human_tests ?? []).map(
18085
18492
  (ht) => ({
18086
- question: ht.question,
18493
+ question: stripAnswerHint(ht.question),
18087
18494
  rating: ht.verdict || "pending",
18088
18495
  reasoning: typeof ht.reasoning === "string" ? ht.reasoning : void 0
18089
18496
  })
18090
18497
  );
18091
- const verification = canonical?.verification;
18092
18498
  const verificationData = verification ? {
18093
18499
  resolved: verification.resolved ?? [],
18094
18500
  persists: verification.persists ?? [],
18501
+ notObserved: verification.not_observed ?? [],
18095
18502
  newFindings: verification.new_findings ?? []
18096
18503
  } : null;
18504
+ const verdict = runVerdict(canonical);
18505
+ const overall = canonical?.human_test_overall ? {
18506
+ question: canonical.human_test_overall.question,
18507
+ label: verdict ?? "pending",
18508
+ answer: canonical.human_test_overall.answer,
18509
+ reasoning: canonical.human_test_overall.reasoning
18510
+ } : null;
18097
18511
  return {
18098
18512
  scenarioName,
18099
18513
  scenarioNarrative,
18100
18514
  persona: personaData,
18101
18515
  walkthroughId: runId,
18102
- runNumber: manifest.run_number ?? null,
18516
+ runNumber: manifest.run_number ?? canonical?.run_number ?? null,
18103
18517
  startedAt: manifest.started_at ?? canonical?.provenance?.started_at ?? null,
18104
18518
  completedAt: manifest.completed_at ?? canonical?.provenance?.completed_at ?? null,
18105
18519
  targetUrl: manifest.target_url ?? canonical?.target?.url ?? null,
18106
18520
  steps,
18107
18521
  findings: findings2,
18108
18522
  verdicts,
18523
+ overall,
18109
18524
  verification: verificationData,
18110
18525
  whatWorked: canonical?.what_worked,
18111
- openQuestions: canonical?.open_questions
18526
+ openQuestions: canonical?.open_questions,
18527
+ setupNotes: canonical?.setup_notes,
18528
+ checks: (() => {
18529
+ const raw = canonical?.checks;
18530
+ return canonical && raw ? { fixes: checkOutcomes(canonical), directions: raw.directions } : null;
18531
+ })()
18112
18532
  };
18113
18533
  }
18114
18534
  async function createLocalHtmlReport(projectRoot, runId, destination) {
@@ -18118,11 +18538,19 @@ async function createLocalHtmlReport(projectRoot, runId, destination) {
18118
18538
  await atomicWriteFile(targetPath, html);
18119
18539
  return { filePath: targetPath, html };
18120
18540
  }
18541
+ var SEVERITY_RANK, TYPE_RANK, CHECK_LABEL;
18121
18542
  var init_html_report = __esm({
18122
18543
  "src/ui/html-report.ts"() {
18123
18544
  "use strict";
18124
18545
  init_dist2();
18125
18546
  init_evaluate();
18547
+ SEVERITY_RANK = ["critical", "major", "minor", "observation"];
18548
+ TYPE_RANK = ["incorrect", "hinders", "missing", "irrelevant"];
18549
+ CHECK_LABEL = {
18550
+ fixed: "Confirmed fixed",
18551
+ came_back: "Came back",
18552
+ unconfirmed: "Not confirmed"
18553
+ };
18126
18554
  }
18127
18555
  });
18128
18556
 
@@ -18131,6 +18559,7 @@ var report_exports = {};
18131
18559
  __export(report_exports, {
18132
18560
  report: () => report
18133
18561
  });
18562
+ import path26 from "path";
18134
18563
  import chalk15 from "chalk";
18135
18564
  async function report(scenario, opts) {
18136
18565
  const ctx = await resolveProject(opts);
@@ -18251,6 +18680,9 @@ function jsonRows(projectRoot, runs) {
18251
18680
  human_tests: r.canonical?.human_tests ?? [],
18252
18681
  human_test_overall: r.canonical?.human_test_overall,
18253
18682
  summary: await buildRunView(projectRoot, r),
18683
+ // Absolute, so a terminal can open them (relative paths are not links).
18684
+ run_dir: runDir(projectRoot, r.runId),
18685
+ report_path: path26.join(runDir(projectRoot, r.runId), "report.md"),
18254
18686
  ...cloudUrl ? { cloud_url: cloudUrl } : {}
18255
18687
  };
18256
18688
  })
@@ -18275,7 +18707,7 @@ __export(open_exports, {
18275
18707
  openDashboard: () => openDashboard
18276
18708
  });
18277
18709
  import { existsSync as existsSync8, promises as fs32 } from "fs";
18278
- import path26 from "path";
18710
+ import path27 from "path";
18279
18711
  import { isCancel as isCancel3, select as select3 } from "@clack/prompts";
18280
18712
  import chalk16 from "chalk";
18281
18713
  async function openDashboard(runId, opts) {
@@ -18371,7 +18803,7 @@ async function openLocalReport(projectRoot, runId, opts) {
18371
18803
  for (const rid of [...runs].reverse()) {
18372
18804
  try {
18373
18805
  const manifestRaw = await fs32.readFile(
18374
- path26.join(runDir(projectRoot, rid), "walkthrough.json"),
18806
+ path27.join(runDir(projectRoot, rid), "walkthrough.json"),
18375
18807
  "utf8"
18376
18808
  );
18377
18809
  const manifest = JSON.parse(manifestRaw);
@@ -20146,7 +20578,7 @@ var init_status2 = __esm({
20146
20578
 
20147
20579
  // src/commands/browse-actions.ts
20148
20580
  import { existsSync as existsSync11 } from "fs";
20149
- import path27 from "path";
20581
+ import path28 from "path";
20150
20582
  function byRole(page, role, name, opts) {
20151
20583
  const loc = page.getByRole(role, { name, exact: false });
20152
20584
  return opts.nth !== void 0 ? loc.nth(Number(opts.nth)) : loc.first();
@@ -20337,7 +20769,7 @@ async function fillIndexedElement(page, index, text, submit) {
20337
20769
  await fillOrSelect(loc.first(), text, submit);
20338
20770
  }
20339
20771
  async function setFilesOnElement(el, filePath) {
20340
- const resolved = path27.resolve(filePath);
20772
+ const resolved = path28.resolve(filePath);
20341
20773
  if (!existsSync11(resolved)) {
20342
20774
  throw new Error(`Upload file does not exist: "${resolved}"`);
20343
20775
  }
@@ -20677,7 +21109,7 @@ var init_browse_driver = __esm({
20677
21109
 
20678
21110
  // src/commands/step-numbering.ts
20679
21111
  import { open, readdir as readdir2, unlink } from "fs/promises";
20680
- import path28 from "path";
21112
+ import path29 from "path";
20681
21113
  function stepFileName(stepNumber2) {
20682
21114
  return `${String(stepNumber2).padStart(3, "0")}.json`;
20683
21115
  }
@@ -20699,7 +21131,7 @@ async function reserveStepFile(stepsDir, limit) {
20699
21131
  if (candidate > limit.maxSteps) {
20700
21132
  throw new Error(stepLimitMessage(limit.maxSteps, limit.runId));
20701
21133
  }
20702
- const stepPath = path28.join(stepsDir, stepFileName(candidate));
21134
+ const stepPath = path29.join(stepsDir, stepFileName(candidate));
20703
21135
  try {
20704
21136
  const handle = await open(stepPath, "wx");
20705
21137
  await handle.close();
@@ -20731,7 +21163,7 @@ __export(step_exports, {
20731
21163
  stepCommand: () => stepCommand
20732
21164
  });
20733
21165
  import { copyFile, mkdir as mkdir5, stat as stat2 } from "fs/promises";
20734
- import path29 from "path";
21166
+ import path30 from "path";
20735
21167
  import chalk22 from "chalk";
20736
21168
  function parseMaxSteps(val) {
20737
21169
  if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
@@ -20750,8 +21182,8 @@ async function recordStep(input) {
20750
21182
  if (!runDirPath) {
20751
21183
  throw new Error(`Run directory not found for run "${input.runId}"`);
20752
21184
  }
20753
- const stepsDir = path29.join(runDirPath, "steps");
20754
- const screenshotsDir = path29.join(runDirPath, "screenshots");
21185
+ const stepsDir = path30.join(runDirPath, "steps");
21186
+ const screenshotsDir = path30.join(runDirPath, "screenshots");
20755
21187
  await mkdir5(stepsDir, { recursive: true });
20756
21188
  await mkdir5(screenshotsDir, { recursive: true });
20757
21189
  const shotSource = input.shot ? await existingShot(input.shot) : null;
@@ -20765,11 +21197,11 @@ async function recordStep(input) {
20765
21197
  let finalScreenshotPath = null;
20766
21198
  if (shotSource) {
20767
21199
  const destName = `step-${stepNumber2}.png`;
20768
- finalScreenshotPath = path29.join(screenshotsDir, destName);
20769
- if (path29.resolve(finalScreenshotPath) !== shotSource) {
21200
+ finalScreenshotPath = path30.join(screenshotsDir, destName);
21201
+ if (path30.resolve(finalScreenshotPath) !== shotSource) {
20770
21202
  await copyFile(shotSource, finalScreenshotPath);
20771
21203
  }
20772
- screenshotRef = path29.join("screenshots", destName);
21204
+ screenshotRef = path30.join("screenshots", destName);
20773
21205
  }
20774
21206
  const parsedStep = runStepSchema.parse({
20775
21207
  step_number: stepNumber2,
@@ -20808,7 +21240,7 @@ async function recordStep(input) {
20808
21240
  }
20809
21241
  }
20810
21242
  async function existingShot(shot) {
20811
- const resolved = path29.resolve(shot);
21243
+ const resolved = path30.resolve(shot);
20812
21244
  const isFile = await stat2(resolved).then((s) => s.isFile()).catch(() => false);
20813
21245
  if (!isFile) {
20814
21246
  throw new Error(
@@ -20862,7 +21294,7 @@ async function stepCommand(runArg, opts) {
20862
21294
  {
20863
21295
  step_number: res.step.step_number,
20864
21296
  cancelled: true,
20865
- path: path29.relative(projectRoot, res.stepPath),
21297
+ path: path30.relative(projectRoot, res.stepPath),
20866
21298
  action: res.step.action,
20867
21299
  note: res.step.note,
20868
21300
  url: res.step.url,
@@ -20890,7 +21322,7 @@ async function stepCommand(runArg, opts) {
20890
21322
  JSON.stringify(
20891
21323
  {
20892
21324
  step_number: res.step.step_number,
20893
- path: path29.relative(projectRoot, res.stepPath),
21325
+ path: path30.relative(projectRoot, res.stepPath),
20894
21326
  action: res.step.action,
20895
21327
  note: res.step.note,
20896
21328
  url: res.step.url,
@@ -20919,7 +21351,7 @@ async function stepCommand(runArg, opts) {
20919
21351
  console.log(` ${chalk22.dim("shot:")} ${res.step.screenshot}`);
20920
21352
  }
20921
21353
  console.log(
20922
- ` ${chalk22.dim("file:")} ${path29.relative(projectRoot, res.stepPath)}`
21354
+ ` ${chalk22.dim("file:")} ${path30.relative(projectRoot, res.stepPath)}`
20923
21355
  );
20924
21356
  console.log();
20925
21357
  } else {
@@ -20980,21 +21412,21 @@ import {
20980
21412
  } from "fs";
20981
21413
  import { rm as rm2 } from "fs/promises";
20982
21414
  import { homedir as homedir3 } from "os";
20983
- import path30 from "path";
21415
+ import path31 from "path";
20984
21416
  import chalk23 from "chalk";
20985
21417
  function stateDir() {
20986
- return path30.join(
20987
- process.env.IAMU_HOME ?? path30.join(homedir3(), ".iamu"),
21418
+ return path31.join(
21419
+ process.env.IAMU_HOME ?? path31.join(homedir3(), ".iamu"),
20988
21420
  "browse"
20989
21421
  );
20990
21422
  }
20991
21423
  function profileDir() {
20992
- return path30.join(stateDir(), "profile");
21424
+ return path31.join(stateDir(), "profile");
20993
21425
  }
20994
21426
  function readBrowseState() {
20995
21427
  try {
20996
21428
  return JSON.parse(
20997
- readFileSync6(path30.join(stateDir(), "state.json"), "utf8")
21429
+ readFileSync6(path31.join(stateDir(), "state.json"), "utf8")
20998
21430
  );
20999
21431
  } catch {
21000
21432
  return null;
@@ -21002,7 +21434,7 @@ function readBrowseState() {
21002
21434
  }
21003
21435
  function writeState(state) {
21004
21436
  mkdirSync3(stateDir(), { recursive: true });
21005
- writeFileSync3(path30.join(stateDir(), "state.json"), JSON.stringify(state));
21437
+ writeFileSync3(path31.join(stateDir(), "state.json"), JSON.stringify(state));
21006
21438
  }
21007
21439
  function fail(message, opts) {
21008
21440
  if (opts?.json) {
@@ -21066,7 +21498,7 @@ async function requestedViewport(opts) {
21066
21498
  let manifest;
21067
21499
  try {
21068
21500
  manifest = JSON.parse(
21069
- readFileSync6(path30.join(dir, "walkthrough.json"), "utf8")
21501
+ readFileSync6(path31.join(dir, "walkthrough.json"), "utf8")
21070
21502
  );
21071
21503
  } catch {
21072
21504
  fail(
@@ -21172,7 +21604,7 @@ async function stop(opts, driver) {
21172
21604
  await driver.closeBrowser(state.port);
21173
21605
  if (state.pid) await reapBrowser(state.pid, profileDir(), driver);
21174
21606
  try {
21175
- unlinkSync3(path30.join(stateDir(), "state.json"));
21607
+ unlinkSync3(path31.join(stateDir(), "state.json"));
21176
21608
  } catch {
21177
21609
  }
21178
21610
  report2(opts, { stopped: true }, [` ${chalk23.green("\u2713")} Browser stopped`]);
@@ -21181,8 +21613,8 @@ function computePageFingerprint(url, bodySnippet) {
21181
21613
  return createHash4("sha256").update(`${url}|${bodySnippet.trim().replace(/\s+/g, " ")}`).digest("hex").slice(0, 16);
21182
21614
  }
21183
21615
  function checkAndRecordStagnation(projectRoot, runId, fingerprint, action) {
21184
- const cacheDir2 = path30.join(projectRoot, "iamu", ".cache");
21185
- const cacheFile = path30.join(cacheDir2, `stagnation-${runId}.json`);
21616
+ const cacheDir2 = path31.join(projectRoot, "iamu", ".cache");
21617
+ const cacheFile = path31.join(cacheDir2, `stagnation-${runId}.json`);
21186
21618
  let previous = null;
21187
21619
  try {
21188
21620
  if (existsSync12(cacheFile)) {
@@ -21352,8 +21784,8 @@ async function withPage(opts, driver, fn, target) {
21352
21784
  let contentSnippet;
21353
21785
  if (opts.shot) {
21354
21786
  await waitForVisualStability(page);
21355
- screenshot = path30.resolve(opts.shot);
21356
- mkdirSync3(path30.dirname(screenshot), { recursive: true });
21787
+ screenshot = path31.resolve(opts.shot);
21788
+ mkdirSync3(path31.dirname(screenshot), { recursive: true });
21357
21789
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
21358
21790
  if (typeof page.evaluate === "function") {
21359
21791
  contentSnippet = await page.evaluate(() => {
@@ -21518,7 +21950,7 @@ function printStep(opts, record, projectRoot, stagnationWarning, challengeWarnin
21518
21950
  if (opts.json) {
21519
21951
  const doc = {
21520
21952
  step_number: step.step_number,
21521
- path: path30.relative(projectRoot, record.stepPath),
21953
+ path: path31.relative(projectRoot, record.stepPath),
21522
21954
  action: step.action,
21523
21955
  note: step.note,
21524
21956
  url: step.url,
@@ -21621,10 +22053,10 @@ async function browseStep(args, opts, driver) {
21621
22053
  const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
21622
22054
  if (!runDirPath) return;
21623
22055
  const maxSteps = parseMaxSteps(opts.maxSteps);
21624
- if (await peekNextStepNumber(path30.join(runDirPath, "steps")) > maxSteps) {
22056
+ if (await peekNextStepNumber(path31.join(runDirPath, "steps")) > maxSteps) {
21625
22057
  return fail(stepLimitMessage(maxSteps, opts.run), opts);
21626
22058
  }
21627
- const pendingShot = path30.join(
22059
+ const pendingShot = path31.join(
21628
22060
  runDirPath,
21629
22061
  "screenshots",
21630
22062
  `.pending-${randomUUID9()}.png`
@@ -22476,10 +22908,10 @@ __export(preflight_exports, {
22476
22908
  });
22477
22909
  import { spawnSync } from "child_process";
22478
22910
  import { existsSync as existsSync14, promises as fs34 } from "fs";
22479
- import path31 from "path";
22911
+ import path32 from "path";
22480
22912
  import chalk25 from "chalk";
22481
22913
  async function detectPreflightCommand(targetDir) {
22482
- const pkgPath = path31.join(targetDir, "package.json");
22914
+ const pkgPath = path32.join(targetDir, "package.json");
22483
22915
  if (existsSync14(pkgPath)) {
22484
22916
  try {
22485
22917
  const raw = await fs34.readFile(pkgPath, "utf8");
@@ -22494,12 +22926,12 @@ async function detectPreflightCommand(targetDir) {
22494
22926
  } catch {
22495
22927
  }
22496
22928
  }
22497
- const tsconfigPath = path31.join(targetDir, "tsconfig.json");
22929
+ const tsconfigPath = path32.join(targetDir, "tsconfig.json");
22498
22930
  if (existsSync14(tsconfigPath)) {
22499
22931
  return { command: "npx", args: ["--no-install", "tsc", "--noEmit"] };
22500
22932
  }
22501
- const biomePath = path31.join(targetDir, "biome.json");
22502
- const biomecPath = path31.join(targetDir, "biome.jsonc");
22933
+ const biomePath = path32.join(targetDir, "biome.json");
22934
+ const biomecPath = path32.join(targetDir, "biome.jsonc");
22503
22935
  if (existsSync14(biomePath) || existsSync14(biomecPath)) {
22504
22936
  return { command: "npx", args: ["--no-install", "biome", "check"] };
22505
22937
  }
@@ -22536,7 +22968,7 @@ async function runPreflightCheck(targetDir) {
22536
22968
  };
22537
22969
  }
22538
22970
  async function preflight(opts = {}) {
22539
- const targetDir = opts.cwd ? path31.resolve(opts.cwd) : process.cwd();
22971
+ const targetDir = opts.cwd ? path32.resolve(opts.cwd) : process.cwd();
22540
22972
  const result = await runPreflightCheck(targetDir);
22541
22973
  if (result.ok) {
22542
22974
  if (opts.json) {
@@ -22608,7 +23040,7 @@ import {
22608
23040
  readlinkSync,
22609
23041
  realpathSync as realpathSync2
22610
23042
  } from "fs";
22611
- import path32 from "path";
23043
+ import path33 from "path";
22612
23044
  function git(args, cwd) {
22613
23045
  return execFileSync("git", args, {
22614
23046
  cwd,
@@ -22661,8 +23093,8 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
22661
23093
  const top = git(["rev-parse", "--show-toplevel"], projectRoot).toString("utf8").trim();
22662
23094
  if (!top) return void 0;
22663
23095
  const realTop = realpathSync2(top);
22664
- const realIamu = path32.join(realpathSync2(projectRoot), iamuDirName);
22665
- const iamuRel = path32.relative(realTop, realIamu).split(path32.sep).join("/");
23096
+ const realIamu = path33.join(realpathSync2(projectRoot), iamuDirName);
23097
+ const iamuRel = path33.relative(realTop, realIamu).split(path33.sep).join("/");
22666
23098
  const pathspec = iamuRel.startsWith("..") ? [":(top)"] : [":(top)", `:(top,exclude,literal)${iamuRel}`];
22667
23099
  const status2 = git(
22668
23100
  [
@@ -22692,7 +23124,7 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
22692
23124
  );
22693
23125
  for (const file of sorted.slice(0, MAX_UNTRACKED_HASHED)) {
22694
23126
  const rel = file.replace(/\/$/, "");
22695
- hash.update(`${file}\0${hashFile(path32.join(realTop, rel))}
23127
+ hash.update(`${file}\0${hashFile(path33.join(realTop, rel))}
22696
23128
  `);
22697
23129
  }
22698
23130
  if (sorted.length > MAX_UNTRACKED_HASHED) {
@@ -23298,12 +23730,12 @@ __export(validate_exports, {
23298
23730
  });
23299
23731
  import { randomUUID as randomUUID10 } from "crypto";
23300
23732
  import { promises as fs35 } from "fs";
23301
- import path33 from "path";
23733
+ import path34 from "path";
23302
23734
  import chalk27 from "chalk";
23303
23735
  function inferTypeFromPath(filePath, projectRoot) {
23304
- const rel = path33.relative(path33.join(projectRoot, "iamu"), filePath);
23305
- if (rel.startsWith("..") || path33.isAbsolute(rel)) return null;
23306
- const segments = rel.split(path33.sep);
23736
+ const rel = path34.relative(path34.join(projectRoot, "iamu"), filePath);
23737
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) return null;
23738
+ const segments = rel.split(path34.sep);
23307
23739
  if (segments.length < 2) return null;
23308
23740
  return TYPE_BY_ALIAS.get(segments[0]) ?? null;
23309
23741
  }
@@ -23333,7 +23765,7 @@ function validateFile(filePath, raw, projectRoot) {
23333
23765
  );
23334
23766
  return result;
23335
23767
  }
23336
- if (path33.basename(filePath) === "context.yml") {
23768
+ if (path34.basename(filePath) === "context.yml") {
23337
23769
  applyZodValidation(contextFrontmatterSchema, frontmatter, result);
23338
23770
  const warnings = [];
23339
23771
  const hasNotes = Boolean(
@@ -23402,7 +23834,7 @@ async function buildRepairLookups(projectRoot) {
23402
23834
  }
23403
23835
  }
23404
23836
  const runScenario = /* @__PURE__ */ new Map();
23405
- const runsDir2 = path33.join(projectRoot, "iamu", ".runs");
23837
+ const runsDir2 = path34.join(projectRoot, "iamu", ".runs");
23406
23838
  let runIds = [];
23407
23839
  try {
23408
23840
  runIds = await fs35.readdir(runsDir2);
@@ -23412,7 +23844,7 @@ async function buildRepairLookups(projectRoot) {
23412
23844
  try {
23413
23845
  const manifest = JSON.parse(
23414
23846
  await fs35.readFile(
23415
- path33.join(runsDir2, runId, "walkthrough.json"),
23847
+ path34.join(runsDir2, runId, "walkthrough.json"),
23416
23848
  "utf8"
23417
23849
  )
23418
23850
  );
@@ -23443,7 +23875,7 @@ async function fixFindings(projectRoot, results, dryRun) {
23443
23875
  parsed.frontmatter,
23444
23876
  {
23445
23877
  ...lookups,
23446
- fileSlug: path33.basename(result.path, ".md"),
23878
+ fileSlug: path34.basename(result.path, ".md"),
23447
23879
  newId: randomUUID10,
23448
23880
  body: parsed.body
23449
23881
  }
@@ -23482,7 +23914,7 @@ async function validate(filePath, opts) {
23482
23914
  if (process.exitCode === 1) return;
23483
23915
  const results = [];
23484
23916
  if (filePath) {
23485
- const abs = path33.isAbsolute(filePath) ? filePath : path33.resolve(process.cwd(), filePath);
23917
+ const abs = path34.isAbsolute(filePath) ? filePath : path34.resolve(process.cwd(), filePath);
23486
23918
  try {
23487
23919
  const raw = await fs35.readFile(abs, "utf8");
23488
23920
  results.push(validateFile(abs, raw, projectRoot));
@@ -23498,7 +23930,7 @@ async function validate(filePath, opts) {
23498
23930
  return;
23499
23931
  }
23500
23932
  } else if (opts.run) {
23501
- const findingsDir = path33.join(projectRoot, "iamu", ENTITY_DIRS.finding);
23933
+ const findingsDir = path34.join(projectRoot, "iamu", ENTITY_DIRS.finding);
23502
23934
  let names2 = [];
23503
23935
  try {
23504
23936
  names2 = await fs35.readdir(findingsDir);
@@ -23506,7 +23938,7 @@ async function validate(filePath, opts) {
23506
23938
  }
23507
23939
  for (const name of names2) {
23508
23940
  if (!name.endsWith(".md")) continue;
23509
- const abs = path33.join(findingsDir, name);
23941
+ const abs = path34.join(findingsDir, name);
23510
23942
  try {
23511
23943
  const raw = await fs35.readFile(abs, "utf8");
23512
23944
  const { frontmatter } = parseFrontmatter(raw);
@@ -23515,7 +23947,7 @@ async function validate(filePath, opts) {
23515
23947
  }
23516
23948
  } catch {
23517
23949
  results.push({
23518
- path: path33.relative(projectRoot, abs),
23950
+ path: path34.relative(projectRoot, abs),
23519
23951
  valid: false,
23520
23952
  errors: ["Cannot read file or parse frontmatter"]
23521
23953
  });
@@ -23548,7 +23980,7 @@ async function validate(filePath, opts) {
23548
23980
  } else {
23549
23981
  const typesToCheck = typeFilter ? ENTITY_TYPES.filter((t) => typeFilter.has(t)) : ENTITY_TYPES;
23550
23982
  for (const type2 of typesToCheck) {
23551
- const iamuTypeDir = path33.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
23983
+ const iamuTypeDir = path34.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
23552
23984
  let names2;
23553
23985
  try {
23554
23986
  names2 = await fs35.readdir(iamuTypeDir);
@@ -23557,20 +23989,20 @@ async function validate(filePath, opts) {
23557
23989
  }
23558
23990
  for (const name of names2) {
23559
23991
  if (!name.endsWith(".md")) continue;
23560
- const abs = path33.join(iamuTypeDir, name);
23992
+ const abs = path34.join(iamuTypeDir, name);
23561
23993
  try {
23562
23994
  const raw = await fs35.readFile(abs, "utf8");
23563
23995
  results.push(validateFile(abs, raw, projectRoot));
23564
23996
  } catch {
23565
23997
  results.push({
23566
- path: path33.relative(projectRoot, abs),
23998
+ path: path34.relative(projectRoot, abs),
23567
23999
  valid: false,
23568
24000
  errors: ["Cannot read file"]
23569
24001
  });
23570
24002
  }
23571
24003
  }
23572
24004
  }
23573
- const ctxPath = path33.join(projectRoot, "iamu", "context.yml");
24005
+ const ctxPath = path34.join(projectRoot, "iamu", "context.yml");
23574
24006
  try {
23575
24007
  const raw = await fs35.readFile(ctxPath, "utf8");
23576
24008
  results.push(validateFile(ctxPath, raw, projectRoot));
@@ -23595,19 +24027,19 @@ async function validate(filePath, opts) {
23595
24027
  valid,
23596
24028
  errors: results.filter((r) => !r.valid).flatMap(
23597
24029
  (r) => r.errors.map((e) => ({
23598
- path: path33.relative(projectRoot, r.path),
24030
+ path: path34.relative(projectRoot, r.path),
23599
24031
  message: e
23600
24032
  }))
23601
24033
  ),
23602
24034
  warnings: results.filter((r) => r.warnings && r.warnings.length > 0).flatMap(
23603
24035
  (r) => (r.warnings ?? []).map((w) => ({
23604
- path: path33.relative(projectRoot, r.path),
24036
+ path: path34.relative(projectRoot, r.path),
23605
24037
  message: w
23606
24038
  }))
23607
24039
  ),
23608
24040
  ...opts.fix ? {
23609
24041
  repaired: repaired.map((r) => ({
23610
- path: path33.relative(projectRoot, r.path),
24042
+ path: path34.relative(projectRoot, r.path),
23611
24043
  changes: r.repairs,
23612
24044
  valid: r.valid
23613
24045
  }))
@@ -23629,7 +24061,7 @@ async function validate(filePath, opts) {
23629
24061
  }
23630
24062
  let hasErrors = false;
23631
24063
  for (const r of results) {
23632
- const relPath = path33.relative(projectRoot, r.path);
24064
+ const relPath = path34.relative(projectRoot, r.path);
23633
24065
  if (r.valid) {
23634
24066
  if (r.warnings && r.warnings.length > 0) {
23635
24067
  console.log(` ${symbols.warning} ` + chalk27.yellow(relPath));
@@ -24030,7 +24462,7 @@ __export(set_entity_exports, {
24030
24462
  setEntity: () => setEntity
24031
24463
  });
24032
24464
  import { promises as fs36 } from "fs";
24033
- import path34 from "path";
24465
+ import path35 from "path";
24034
24466
  import chalk30 from "chalk";
24035
24467
  import { ZodError as ZodError2 } from "zod";
24036
24468
  function given(opts, key) {
@@ -24171,7 +24603,7 @@ async function setEntity(type2, slug, opts) {
24171
24603
  }
24172
24604
  raw = Buffer.concat(chunks).toString("utf8");
24173
24605
  } else {
24174
- raw = await fs36.readFile(path34.resolve(projectRoot, opts.file), "utf8");
24606
+ raw = await fs36.readFile(path35.resolve(projectRoot, opts.file), "utf8");
24175
24607
  }
24176
24608
  } catch (err) {
24177
24609
  commandError(
@@ -24599,7 +25031,7 @@ __export(edit_entity_exports, {
24599
25031
  });
24600
25032
  import { spawn as spawn2 } from "child_process";
24601
25033
  import { promises as fs37 } from "fs";
24602
- import path35 from "path";
25034
+ import path36 from "path";
24603
25035
  import chalk31 from "chalk";
24604
25036
  async function editEntity(type2, slug, opts = {}) {
24605
25037
  const ctx = await resolveProject(opts);
@@ -24613,19 +25045,19 @@ async function editEntity(type2, slug, opts = {}) {
24613
25045
  return;
24614
25046
  }
24615
25047
  const dir = ENTITY_DIRS[type2];
24616
- const filePath = path35.join(projectRoot, "iamu", dir, `${slug}.md`);
25048
+ const filePath = path36.join(projectRoot, "iamu", dir, `${slug}.md`);
24617
25049
  try {
24618
25050
  await fs37.access(filePath);
24619
25051
  } catch {
24620
25052
  commandError(
24621
- `${type2} "${slug}" not found at ${path35.relative(projectRoot, filePath)}.`,
25053
+ `${type2} "${slug}" not found at ${path36.relative(projectRoot, filePath)}.`,
24622
25054
  { ...opts, code: "entity_not_found", fix: `iamu list ${type2}s` }
24623
25055
  );
24624
25056
  return;
24625
25057
  }
24626
25058
  if (!process.stdin.isTTY || process.env.CI) {
24627
25059
  commandError(
24628
- `Cannot open editor in a non-interactive environment. Edit the file directly: ${path35.relative(projectRoot, filePath)}`,
25060
+ `Cannot open editor in a non-interactive environment. Edit the file directly: ${path36.relative(projectRoot, filePath)}`,
24629
25061
  {
24630
25062
  ...opts,
24631
25063
  code: "non_interactive_environment",
@@ -24700,7 +25132,7 @@ async function editEntity(type2, slug, opts = {}) {
24700
25132
  console.log();
24701
25133
  console.log(
24702
25134
  chalk31.dim(
24703
- ` Fix the file and run: iamu validate ${path35.relative(projectRoot, filePath)}`
25135
+ ` Fix the file and run: iamu validate ${path36.relative(projectRoot, filePath)}`
24704
25136
  )
24705
25137
  );
24706
25138
  console.log();
@@ -24721,7 +25153,7 @@ async function editEntity(type2, slug, opts = {}) {
24721
25153
  {
24722
25154
  ...opts,
24723
25155
  code: "validation_error",
24724
- fix: `iamu validate ${path35.relative(projectRoot, filePath)}`
25156
+ fix: `iamu validate ${path36.relative(projectRoot, filePath)}`
24725
25157
  }
24726
25158
  );
24727
25159
  }
@@ -24858,7 +25290,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
24858
25290
  const byKey = /* @__PURE__ */ new Map();
24859
25291
  const byPlaceView = /* @__PURE__ */ new Map();
24860
25292
  const signatureOf = (s) => exploreSignature(s.headings, s.title);
24861
- function addSurface(state, place, view, path43, depth) {
25293
+ function addSurface(state, place, view, path44, depth) {
24862
25294
  const key = exploreSurfaceKey(place, view);
24863
25295
  const surface = {
24864
25296
  key,
@@ -24870,7 +25302,7 @@ async function crawlFrom(page, opts, origin, takeBlocked) {
24870
25302
  headings: state.headings,
24871
25303
  fields: state.fields,
24872
25304
  depth,
24873
- path: path43,
25305
+ path: path44,
24874
25306
  actions: []
24875
25307
  };
24876
25308
  surfaces.push(surface);
@@ -24888,9 +25320,9 @@ ${surface.signature}`, key);
24888
25320
  });
24889
25321
  await settle(page, settleMs);
24890
25322
  }
24891
- async function replay(path43) {
24892
- if (!("goto" in (path43[0] ?? {}))) await goto(opts.startUrl);
24893
- for (const step of path43) {
25323
+ async function replay(path44) {
25324
+ if (!("goto" in (path44[0] ?? {}))) await goto(opts.startUrl);
25325
+ for (const step of path44) {
24894
25326
  if ("goto" in step) {
24895
25327
  await goto(step.goto);
24896
25328
  } else {
@@ -25014,8 +25446,8 @@ ${sig}`, key);
25014
25446
  if (stats.surfaces >= budget.max_surfaces) {
25015
25447
  stats.truncated = true;
25016
25448
  } else {
25017
- const path43 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
25018
- addSurface(after, place, view, path43, surface.depth + 1);
25449
+ const path44 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
25450
+ addSurface(after, place, view, path44, surface.depth + 1);
25019
25451
  }
25020
25452
  }
25021
25453
  }
@@ -25215,7 +25647,7 @@ __export(explore_exports, {
25215
25647
  exploreList: () => exploreList,
25216
25648
  exploreRun: () => exploreRun
25217
25649
  });
25218
- import path36 from "path";
25650
+ import path37 from "path";
25219
25651
  import chalk32 from "chalk";
25220
25652
  function fail2(message, json2) {
25221
25653
  if (json2) console.error(JSON.stringify({ error: message }));
@@ -25349,7 +25781,7 @@ async function exploreRun(url, opts) {
25349
25781
  surfaces: result.surfaces
25350
25782
  };
25351
25783
  const file = await writeExploreMap(ctx.projectRoot, map2);
25352
- const rel = path36.relative(process.cwd(), file);
25784
+ const rel = path37.relative(process.cwd(), file);
25353
25785
  if (opts.json) {
25354
25786
  console.log(JSON.stringify({ id: map2.id, path: rel, stats: map2.stats }));
25355
25787
  return;
@@ -25455,15 +25887,15 @@ async function exploreCoverageCommand(mapId, opts) {
25455
25887
  " \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"
25456
25888
  )
25457
25889
  );
25458
- for (const cap of capReport.capabilities) {
25459
- const title = cap.slug.padEnd(24);
25460
- const surfaces = `${cap.reached_surfaces}/${cap.total_surfaces}`.padStart(
25890
+ for (const cap2 of capReport.capabilities) {
25891
+ const title = cap2.slug.padEnd(24);
25892
+ const surfaces = `${cap2.reached_surfaces}/${cap2.total_surfaces}`.padStart(
25461
25893
  8
25462
25894
  );
25463
- const covStr = `${cap.coverage_pct}%`.padStart(8);
25464
- const scenarios = `${cap.primary_scenarios} / ${cap.secondary_scenarios}`;
25465
- const untestedFlag = cap.primary_scenarios === 0 && cap.unreached_surfaces > 0 ? chalk32.yellow(" (untested)") : "";
25466
- const covColored = cap.coverage_pct === 100 ? chalk32.green(covStr) : cap.coverage_pct === 0 ? chalk32.red(covStr) : chalk32.yellow(covStr);
25895
+ const covStr = `${cap2.coverage_pct}%`.padStart(8);
25896
+ const scenarios = `${cap2.primary_scenarios} / ${cap2.secondary_scenarios}`;
25897
+ const untestedFlag = cap2.primary_scenarios === 0 && cap2.unreached_surfaces > 0 ? chalk32.yellow(" (untested)") : "";
25898
+ const covColored = cap2.coverage_pct === 100 ? chalk32.green(covStr) : cap2.coverage_pct === 0 ? chalk32.red(covStr) : chalk32.yellow(covStr);
25467
25899
  console.log(
25468
25900
  ` ${title} ${surfaces} ${covColored} ${scenarios}${untestedFlag}`
25469
25901
  );
@@ -25501,10 +25933,10 @@ async function exploreCoverageCommand(mapId, opts) {
25501
25933
  const hasGaps = capReport.capabilities.some((c) => c.unreached_surfaces > 0) || capReport.unassigned_surfaces.some((s) => s.status !== "reached");
25502
25934
  if (hasGaps) {
25503
25935
  console.log(chalk32.red("\n UNREACHED SURFACES (Gaps)"));
25504
- for (const cap of capReport.capabilities) {
25505
- const gaps = cap.surfaces.filter((s) => s.status !== "reached");
25936
+ for (const cap2 of capReport.capabilities) {
25937
+ const gaps = cap2.surfaces.filter((s) => s.status !== "reached");
25506
25938
  if (gaps.length === 0) continue;
25507
- console.log(` ${chalk32.bold(cap.slug)}:`);
25939
+ console.log(` ${chalk32.bold(cap2.slug)}:`);
25508
25940
  for (const s of gaps) {
25509
25941
  console.log(
25510
25942
  ` ${chalk32.red("\u25CB")} ${s.key.padEnd(32)} ${s.label}${chalk32.dim(` (depth ${s.depth})`)}`
@@ -25525,10 +25957,10 @@ async function exploreCoverageCommand(mapId, opts) {
25525
25957
  }
25526
25958
  if (opts.all) {
25527
25959
  console.log(chalk32.green("\n REACHED SURFACES"));
25528
- for (const cap of capReport.capabilities) {
25529
- const reached = cap.surfaces.filter((s) => s.status === "reached");
25960
+ for (const cap2 of capReport.capabilities) {
25961
+ const reached = cap2.surfaces.filter((s) => s.status === "reached");
25530
25962
  if (reached.length === 0) continue;
25531
- console.log(` ${chalk32.bold(cap.slug)}:`);
25963
+ console.log(` ${chalk32.bold(cap2.slug)}:`);
25532
25964
  for (const s of reached) {
25533
25965
  console.log(
25534
25966
  ` ${chalk32.green("\u2713")} ${s.key.padEnd(32)} ${s.label}${chalk32.dim(` (depth ${s.depth})`)}`
@@ -25850,7 +26282,7 @@ __export(start_run_exports, {
25850
26282
  startRun: () => startRun,
25851
26283
  testabilityTarget: () => testabilityTarget2
25852
26284
  });
25853
- import path37 from "path";
26285
+ import path38 from "path";
25854
26286
  import chalk33 from "chalk";
25855
26287
  async function testabilityTarget2(projectRoot) {
25856
26288
  try {
@@ -26094,7 +26526,7 @@ async function startRun(scenarioSlugArg, opts) {
26094
26526
  ...viewport ? { viewport } : {}
26095
26527
  };
26096
26528
  await atomicWriteFile(
26097
- path37.join(run.absPath, "walkthrough.json"),
26529
+ path38.join(run.absPath, "walkthrough.json"),
26098
26530
  JSON.stringify(manifest, null, 2) + "\n"
26099
26531
  );
26100
26532
  const liveRes = await registerLiveRun({
@@ -26110,7 +26542,7 @@ async function startRun(scenarioSlugArg, opts) {
26110
26542
  JSON.stringify(
26111
26543
  {
26112
26544
  run_id: run.runId,
26113
- path: path37.relative(projectRoot, run.absPath),
26545
+ path: path38.relative(projectRoot, run.absPath),
26114
26546
  scenario_slug: scenarioSlug,
26115
26547
  target_url: start2.url,
26116
26548
  target_reachable: targetReachable,
@@ -26164,7 +26596,7 @@ async function startRun(scenarioSlugArg, opts) {
26164
26596
  );
26165
26597
  }
26166
26598
  console.log(
26167
- ` ${chalk33.dim("path:")} ${path37.relative(projectRoot, run.absPath)}`
26599
+ ` ${chalk33.dim("path:")} ${path38.relative(projectRoot, run.absPath)}`
26168
26600
  );
26169
26601
  console.log();
26170
26602
  console.log(` ${chalk33.bold("Next steps for blind execution:")}`);
@@ -26394,7 +26826,7 @@ function composeWalkerPrompt(p) {
26394
26826
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
26395
26827
  `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`,
26396
26828
  "`--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.",
26397
- `Before you finish, record synthesis with: ${iamu} summary ${p.runId} --worked "..." --worked "..." --question "..."`,
26829
+ `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.`,
26398
26830
  `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.`,
26399
26831
  "Execute everything via CLI commands in the shell. Never invoke host Edit or Write tools on files in the workspace.",
26400
26832
  "",
@@ -27044,7 +27476,7 @@ __export(new_finding_exports, {
27044
27476
  });
27045
27477
  import { randomUUID as randomUUID12 } from "crypto";
27046
27478
  import { access, readFile as readFile7 } from "fs/promises";
27047
- import path38 from "path";
27479
+ import path39 from "path";
27048
27480
  import chalk36 from "chalk";
27049
27481
  async function newFinding(opts) {
27050
27482
  const ctx = await resolveProject(opts);
@@ -27121,7 +27553,7 @@ async function newFinding(opts) {
27121
27553
  }
27122
27554
  try {
27123
27555
  await access(
27124
- path38.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
27556
+ path39.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
27125
27557
  );
27126
27558
  } catch {
27127
27559
  commandError(
@@ -27140,7 +27572,7 @@ async function newFinding(opts) {
27140
27572
  if (!scenarioSlug) {
27141
27573
  try {
27142
27574
  const manifestRaw = await readFile7(
27143
- path38.join(runDirPath, "walkthrough.json"),
27575
+ path39.join(runDirPath, "walkthrough.json"),
27144
27576
  "utf8"
27145
27577
  );
27146
27578
  const manifest = JSON.parse(manifestRaw);
@@ -27287,7 +27719,7 @@ __export(summary_exports, {
27287
27719
  summaryCommand: () => summaryCommand
27288
27720
  });
27289
27721
  import { promises as fs38 } from "fs";
27290
- import path39 from "path";
27722
+ import path40 from "path";
27291
27723
  import chalk37 from "chalk";
27292
27724
  function blockedFrom(value) {
27293
27725
  const reason = value && typeof value === "object" ? value.reason : value;
@@ -27310,12 +27742,18 @@ async function summaryCommand(runIdArg, opts) {
27310
27742
  if (!runDirPath) return;
27311
27743
  let whatWorked = [];
27312
27744
  let openQuestions = [];
27745
+ let setupNotes = [];
27313
27746
  let model = opts.model?.trim() || void 0;
27314
- const summaryPath = path39.join(runDirPath, "summary.json");
27747
+ const summaryPath = path40.join(runDirPath, "summary.json");
27315
27748
  let blocked;
27316
27749
  try {
27317
27750
  const prev = JSON.parse(await fs38.readFile(summaryPath, "utf8"));
27318
27751
  blocked = blockedFrom(prev.blocked);
27752
+ if (Array.isArray(prev.setup_notes)) {
27753
+ setupNotes = prev.setup_notes.filter(
27754
+ (x) => typeof x === "string" && x.trim() !== ""
27755
+ );
27756
+ }
27319
27757
  } catch {
27320
27758
  }
27321
27759
  if (opts.file) {
@@ -27324,7 +27762,7 @@ async function summaryCommand(runIdArg, opts) {
27324
27762
  if (opts.file === "-") {
27325
27763
  raw = await readStdinText();
27326
27764
  } else {
27327
- raw = await fs38.readFile(path39.resolve(projectRoot, opts.file), "utf8");
27765
+ raw = await fs38.readFile(path40.resolve(projectRoot, opts.file), "utf8");
27328
27766
  }
27329
27767
  const parsed = JSON.parse(raw);
27330
27768
  if (Array.isArray(parsed.what_worked)) {
@@ -27337,6 +27775,11 @@ async function summaryCommand(runIdArg, opts) {
27337
27775
  (x) => typeof x === "string" && x.trim() !== ""
27338
27776
  );
27339
27777
  }
27778
+ if (Array.isArray(parsed.setup_notes)) {
27779
+ setupNotes = parsed.setup_notes.filter(
27780
+ (x) => typeof x === "string" && x.trim() !== ""
27781
+ );
27782
+ }
27340
27783
  if (typeof parsed.model === "string" && parsed.model.trim()) {
27341
27784
  model = parsed.model.trim();
27342
27785
  }
@@ -27362,6 +27805,7 @@ async function summaryCommand(runIdArg, opts) {
27362
27805
  if (cliQuestions.length > 0) {
27363
27806
  openQuestions = [...openQuestions, ...cliQuestions];
27364
27807
  }
27808
+ setupNotes = [.../* @__PURE__ */ new Set([...setupNotes, ...toList(opts.setupNote)])];
27365
27809
  if (opts.blocked === false) {
27366
27810
  blocked = void 0;
27367
27811
  } else {
@@ -27370,6 +27814,7 @@ async function summaryCommand(runIdArg, opts) {
27370
27814
  const data = {
27371
27815
  what_worked: whatWorked,
27372
27816
  open_questions: openQuestions,
27817
+ setup_notes: setupNotes,
27373
27818
  ...model ? { model } : {},
27374
27819
  ...blocked ? { blocked } : {}
27375
27820
  };
@@ -27380,7 +27825,7 @@ async function summaryCommand(runIdArg, opts) {
27380
27825
  JSON.stringify(
27381
27826
  {
27382
27827
  run_id: runId,
27383
- path: path39.relative(projectRoot, summaryPath),
27828
+ path: path40.relative(projectRoot, summaryPath),
27384
27829
  summary: data
27385
27830
  },
27386
27831
  null,
@@ -27406,6 +27851,11 @@ async function summaryCommand(runIdArg, opts) {
27406
27851
  ` ${chalk37.dim("open questions:")} ${data.open_questions.length} item(s)`
27407
27852
  );
27408
27853
  }
27854
+ if (data.setup_notes.length > 0) {
27855
+ console.log(
27856
+ ` ${chalk37.dim("setup notes:")} ${data.setup_notes.length} item(s)`
27857
+ );
27858
+ }
27409
27859
  console.log();
27410
27860
  }
27411
27861
  var init_summary = __esm({
@@ -27440,7 +27890,7 @@ __export(complete_exports, {
27440
27890
  validateCanonicalWalkthrough: () => validateCanonicalWalkthrough
27441
27891
  });
27442
27892
  import { promises as fs39 } from "fs";
27443
- import path40 from "path";
27893
+ import path41 from "path";
27444
27894
  import chalk38 from "chalk";
27445
27895
  async function complete(runIdArg, opts) {
27446
27896
  const ctx = await resolveProject(opts);
@@ -27477,7 +27927,7 @@ async function complete(runIdArg, opts) {
27477
27927
  let judgeReported;
27478
27928
  try {
27479
27929
  const verdictsRaw = await fs39.readFile(
27480
- path40.join(run.runDirPath, "verdicts.json"),
27930
+ path41.join(run.runDirPath, "verdicts.json"),
27481
27931
  "utf8"
27482
27932
  );
27483
27933
  const parsed = JSON.parse(verdictsRaw);
@@ -27553,7 +28003,7 @@ async function loadRun(runId, opts) {
27553
28003
  });
27554
28004
  return null;
27555
28005
  }
27556
- const manifestPath = path40.join(runDirPath, "walkthrough.json");
28006
+ const manifestPath = path41.join(runDirPath, "walkthrough.json");
27557
28007
  let manifest;
27558
28008
  try {
27559
28009
  manifest = JSON.parse(
@@ -27584,7 +28034,7 @@ async function reportAlreadyCompleted(run, opts) {
27584
28034
  if (opts?.json) {
27585
28035
  try {
27586
28036
  const raw = await fs39.readFile(
27587
- path40.join(run.runDirPath, "canonical.json"),
28037
+ path41.join(run.runDirPath, "canonical.json"),
27588
28038
  "utf8"
27589
28039
  );
27590
28040
  console.log(raw.trim());
@@ -27720,15 +28170,11 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
27720
28170
  }
27721
28171
  console.log();
27722
28172
  console.log(
27723
- ` ${chalk38.dim("canonical:")} ${chalk38.dim(path40.relative(projectRoot, assembled.canonicalPath))}`
27724
- );
27725
- console.log(
27726
- ` ${chalk38.dim("report:")} ${chalk38.dim(path40.relative(projectRoot, assembled.reportPath))}`
28173
+ ` ${chalk38.dim("canonical:")} ${chalk38.dim(path41.relative(projectRoot, assembled.canonicalPath))}`
27727
28174
  );
28175
+ console.log(` ${chalk38.dim("report:")} ${assembled.reportPath}`);
27728
28176
  if (assembled.htmlReportPath) {
27729
- console.log(
27730
- ` ${chalk38.dim("html:")} ${chalk38.dim(path40.relative(projectRoot, assembled.htmlReportPath))}`
27731
- );
28177
+ console.log(` ${chalk38.dim("html:")} ${assembled.htmlReportPath}`);
27732
28178
  }
27733
28179
  if (extras.viewportWarning) {
27734
28180
  console.log();
@@ -27797,7 +28243,7 @@ function screenshotFitsViewport(width, expected) {
27797
28243
  }
27798
28244
  async function screenshotSizeWarning(runDirPath, viewport) {
27799
28245
  if (!viewport) return null;
27800
- const dir = path40.join(runDirPath, "screenshots");
28246
+ const dir = path41.join(runDirPath, "screenshots");
27801
28247
  let first;
27802
28248
  try {
27803
28249
  first = (await fs39.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
@@ -27807,7 +28253,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
27807
28253
  if (!first) return null;
27808
28254
  let size = null;
27809
28255
  try {
27810
- const handle = await fs39.open(path40.join(dir, first), "r");
28256
+ const handle = await fs39.open(path41.join(dir, first), "r");
27811
28257
  try {
27812
28258
  const head = Buffer.alloc(24);
27813
28259
  const { bytesRead } = await handle.read(head, 0, 24, 0);
@@ -27824,7 +28270,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
27824
28270
  async function readWalkerSummary(runDirPath) {
27825
28271
  try {
27826
28272
  const data = JSON.parse(
27827
- await fs39.readFile(path40.join(runDirPath, "summary.json"), "utf8")
28273
+ await fs39.readFile(path41.join(runDirPath, "summary.json"), "utf8")
27828
28274
  );
27829
28275
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
27830
28276
  const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
@@ -27832,6 +28278,7 @@ async function readWalkerSummary(runDirPath) {
27832
28278
  return {
27833
28279
  what_worked: strings(data.what_worked),
27834
28280
  open_questions: strings(data.open_questions),
28281
+ setup_notes: strings(data.setup_notes),
27835
28282
  model,
27836
28283
  ...typeof reason === "string" && reason.trim() !== "" ? { blocked: { reason: reason.trim() } } : {}
27837
28284
  };
@@ -27933,7 +28380,7 @@ function validateCanonicalWalkthrough(canonical) {
27933
28380
  return canonicalWalkthroughSchema.parse(canonical);
27934
28381
  }
27935
28382
  async function persistCanonical(runDirPath, canonical) {
27936
- const canonicalPath = path40.join(runDirPath, "canonical.json");
28383
+ const canonicalPath = path41.join(runDirPath, "canonical.json");
27937
28384
  await atomicWriteFile(
27938
28385
  canonicalPath,
27939
28386
  `${JSON.stringify(canonical, null, 2)}
@@ -27942,7 +28389,7 @@ async function persistCanonical(runDirPath, canonical) {
27942
28389
  return canonicalPath;
27943
28390
  }
27944
28391
  async function persistReport(runDirPath, canonical) {
27945
- const reportPath = path40.join(runDirPath, "report.md");
28392
+ const reportPath = path41.join(runDirPath, "report.md");
27946
28393
  await atomicWriteFile(reportPath, renderCanonicalMarkdown(canonical));
27947
28394
  return reportPath;
27948
28395
  }
@@ -28121,13 +28568,13 @@ __export(match_exports, {
28121
28568
  matchRecord: () => matchRecord
28122
28569
  });
28123
28570
  import { promises as fs40 } from "fs";
28124
- import path41 from "path";
28571
+ import path42 from "path";
28125
28572
  import chalk39 from "chalk";
28126
28573
  async function completedManifest2(runDirPath, runId, opts) {
28127
28574
  let manifest;
28128
28575
  try {
28129
28576
  manifest = JSON.parse(
28130
- await fs40.readFile(path41.join(runDirPath, "walkthrough.json"), "utf8")
28577
+ await fs40.readFile(path42.join(runDirPath, "walkthrough.json"), "utf8")
28131
28578
  );
28132
28579
  } catch {
28133
28580
  commandError(`Run not found: ${runId}`, {
@@ -28164,7 +28611,7 @@ async function match(runId, opts) {
28164
28611
  );
28165
28612
  const needed = !manifest.blocked && input.current.length > 0 && (input.prior.length > 0 || (input.guidance ?? []).length > 0);
28166
28613
  const prompt = needed ? composeMatchPrompt(input) : null;
28167
- const promptFilePath = opts?.promptFile ?? path41.join(runDirPath, "match_prompt.md");
28614
+ const promptFilePath = opts?.promptFile ?? path42.join(runDirPath, "match_prompt.md");
28168
28615
  if (prompt) {
28169
28616
  await fs40.writeFile(promptFilePath, prompt, "utf8");
28170
28617
  } else {
@@ -28180,8 +28627,8 @@ async function match(runId, opts) {
28180
28627
  ...input,
28181
28628
  needed,
28182
28629
  ...manifest.blocked ? { blocked: manifest.blocked } : {},
28183
- prompt_file: prompt ? path41.relative(ctx.projectRoot, promptFilePath) : null,
28184
- prompt_file_abs: prompt ? path41.resolve(promptFilePath) : null,
28630
+ prompt_file: prompt ? path42.relative(ctx.projectRoot, promptFilePath) : null,
28631
+ prompt_file_abs: prompt ? path42.resolve(promptFilePath) : null,
28185
28632
  match_prompt: prompt
28186
28633
  },
28187
28634
  null,
@@ -28209,7 +28656,7 @@ async function matchRecord(runId, opts) {
28209
28656
  const manifest = await completedManifest2(runDirPath, runId, opts);
28210
28657
  if (!manifest) return;
28211
28658
  const input = await readAnswerInput(opts, "matches", {
28212
- defaultPath: path41.join(runDirPath, "matches.json"),
28659
+ defaultPath: path42.join(runDirPath, "matches.json"),
28213
28660
  code: "missing_matches_input",
28214
28661
  fix: `iamu match-record ${runId} --data '<json>'`
28215
28662
  });
@@ -28288,10 +28735,10 @@ __export(advise_exports, {
28288
28735
  readAdvisePrompt: () => readAdvisePrompt
28289
28736
  });
28290
28737
  import { existsSync as existsSync17, promises as fs41, readFileSync as readFileSync10 } from "fs";
28291
- import path42 from "path";
28738
+ import path43 from "path";
28292
28739
  import chalk40 from "chalk";
28293
28740
  function readAdvisePrompt(projectRoot) {
28294
- const installed = path42.join(projectRoot, "iamu", ".prompts", "advise.md");
28741
+ const installed = path43.join(projectRoot, "iamu", ".prompts", "advise.md");
28295
28742
  if (existsSync17(installed)) {
28296
28743
  try {
28297
28744
  return readFileSync10(installed, "utf8");
@@ -28336,7 +28783,7 @@ async function advise(runId, opts = {}) {
28336
28783
  findings2 = input.findings.length;
28337
28784
  needed = findings2 >= 2;
28338
28785
  if (!needed) reason = "fewer than two open findings in the project";
28339
- promptPath = opts.promptFile ?? path42.join(analysisDir(projectRoot), "advise_prompt.md");
28786
+ promptPath = opts.promptFile ?? path43.join(analysisDir(projectRoot), "advise_prompt.md");
28340
28787
  } else {
28341
28788
  if (!requireRunId(runId, opts, "iamu advise <run-id>")) return;
28342
28789
  const runDirPath = resolveRunDir(projectRoot, runId, opts);
@@ -28346,21 +28793,21 @@ async function advise(runId, opts = {}) {
28346
28793
  input = await buildAdviseInput(projectRoot, runId, manifest.scenario_slug);
28347
28794
  refsDir = runDirPath;
28348
28795
  findings2 = input.findings.length;
28349
- const advised = existsSync17(path42.join(runDirPath, ADVICE_FILE));
28796
+ const advised = existsSync17(path43.join(runDirPath, ADVICE_FILE));
28350
28797
  needed = !manifest.blocked && findings2 > 0 && !advised;
28351
28798
  if (manifest.blocked) reason = "the run was blocked";
28352
28799
  else if (advised) reason = "the run is already advised";
28353
28800
  else if (findings2 === 0) reason = "no open findings to advise";
28354
- promptPath = opts.promptFile ?? path42.join(runDirPath, "advise_prompt.md");
28801
+ promptPath = opts.promptFile ?? path43.join(runDirPath, "advise_prompt.md");
28355
28802
  }
28356
28803
  const { bundle, refs } = promptBundle(input);
28357
28804
  const prompt = needed ? composePrompt(projectRoot, bundle) : null;
28358
28805
  if (prompt) {
28359
- await fs41.mkdir(path42.dirname(promptPath), { recursive: true });
28806
+ await fs41.mkdir(path43.dirname(promptPath), { recursive: true });
28360
28807
  await fs41.writeFile(promptPath, prompt, "utf8");
28361
28808
  await fs41.mkdir(refsDir, { recursive: true });
28362
28809
  await fs41.writeFile(
28363
- path42.join(refsDir, ADVISE_REFS_FILE),
28810
+ path43.join(refsDir, ADVISE_REFS_FILE),
28364
28811
  `${JSON.stringify(refs, null, 2)}
28365
28812
  `,
28366
28813
  "utf8"
@@ -28374,8 +28821,8 @@ async function advise(runId, opts = {}) {
28374
28821
  ...reason ? { reason } : {},
28375
28822
  mode: opts.project ? "project" : "run",
28376
28823
  findings: findings2,
28377
- prompt_file: prompt ? path42.relative(projectRoot, promptPath) : null,
28378
- prompt_file_abs: prompt ? path42.resolve(promptPath) : null,
28824
+ prompt_file: prompt ? path43.relative(projectRoot, promptPath) : null,
28825
+ prompt_file_abs: prompt ? path43.resolve(promptPath) : null,
28379
28826
  prompt_chars: prompt?.length ?? 0,
28380
28827
  advise_prompt: prompt
28381
28828
  },
@@ -28439,7 +28886,7 @@ async function adviseRecord(runId, opts) {
28439
28886
  const { projectRoot } = ctx;
28440
28887
  if (opts.project) {
28441
28888
  const input2 = await readAnswerInput(opts, "advice", {
28442
- defaultPath: path42.join(analysisDir(projectRoot), "answer.json"),
28889
+ defaultPath: path43.join(analysisDir(projectRoot), "answer.json"),
28443
28890
  code: "missing_advice_input",
28444
28891
  fix: "iamu advise-record --project --data '<json>'"
28445
28892
  });
@@ -28469,7 +28916,7 @@ async function adviseRecord(runId, opts) {
28469
28916
  const manifest = await completedManifest2(runDirPath, runId, opts);
28470
28917
  if (!manifest) return;
28471
28918
  const input = await readAnswerInput(opts, "advice", {
28472
- defaultPath: path42.join(runDir(projectRoot, runId), "advise_answer.json"),
28919
+ defaultPath: path43.join(runDir(projectRoot, runId), "advise_answer.json"),
28473
28920
  code: "missing_advice_input",
28474
28921
  fix: `iamu advise-record ${runId} --data '<json>'`
28475
28922
  });
@@ -28503,9 +28950,9 @@ async function directionsList(opts) {
28503
28950
  const ctx = await resolveProject(opts);
28504
28951
  if (!ctx) return;
28505
28952
  const all = await listDirections(ctx.projectRoot);
28506
- const rank = (s) => ["accepted", "proposed", "done", "rejected"].indexOf(s);
28953
+ const rank2 = (s) => ["accepted", "proposed", "done", "rejected"].indexOf(s);
28507
28954
  const sorted = [...all].sort(
28508
- (a, b) => rank(a.frontmatter.status) - rank(b.frontmatter.status)
28955
+ (a, b) => rank2(a.frontmatter.status) - rank2(b.frontmatter.status)
28509
28956
  );
28510
28957
  if (opts.json) {
28511
28958
  console.log(
@@ -28515,7 +28962,7 @@ async function directionsList(opts) {
28515
28962
  problem: d.texts.problem,
28516
28963
  change: d.texts.change,
28517
28964
  leading_practice: d.texts.leading_practice,
28518
- path: path42.relative(ctx.projectRoot, d.path)
28965
+ path: path43.relative(ctx.projectRoot, d.path)
28519
28966
  })),
28520
28967
  null,
28521
28968
  2
@@ -28896,9 +29343,9 @@ function registerProjectCommands(program2) {
28896
29343
  "after",
28897
29344
  "\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
28898
29345
  ).action(
28899
- async (path43, opts) => {
29346
+ async (path44, opts) => {
28900
29347
  const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
28901
- await validate2(path43, opts);
29348
+ await validate2(path44, opts);
28902
29349
  }
28903
29350
  );
28904
29351
  program2.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
@@ -29272,13 +29719,16 @@ function registerWalkthroughCommands(program2) {
29272
29719
  }
29273
29720
  );
29274
29721
  program2.command("summary [run-id]").helpGroup("Walkthrough:").description(
29275
- "Record walker summary (what worked, open questions, or that the walk was blocked) atomically"
29722
+ "Record walker summary (what worked, open questions, setup notes, or that the walk was blocked) atomically"
29276
29723
  ).option("--run <run-id>", "Run ID (or pass as positional argument)").option(
29277
29724
  "--worked <note...>",
29278
29725
  "Things that worked well (can be specified multiple times)"
29279
29726
  ).option(
29280
29727
  "--question <note...>",
29281
- "Open questions that arose (can be specified multiple times)"
29728
+ "Questions for the product team the walk raised (can be specified multiple times)"
29729
+ ).option(
29730
+ "--setup-note <note...>",
29731
+ "Doubts about the test setup, not the product: missing data, accounts, dates (can be specified multiple times)"
29282
29732
  ).option("--model <name>", "Model identifier (e.g. flash, haiku)").option(
29283
29733
  "--blocked <reason>",
29284
29734
  "The walk could not start as set up (persona unreachable, world missing): what was missing"