@iamu-dev/iamu 0.8.0-beta.8 → 0.8.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -17766,7 +17766,7 @@ function buildScenarioFrontmatter(row, personaSlug, outlineSlug, localFrontmatte
17766
17766
  };
17767
17767
  return { frontmatter, body: resolveBody(row, row.brief ?? ""), slug };
17768
17768
  }
17769
- function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17769
+ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId, localFrontmatter) {
17770
17770
  const descriptionParts = [];
17771
17771
  if (row.description)
17772
17772
  descriptionParts.push(row.description);
@@ -17790,7 +17790,7 @@ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17790
17790
  ...dismissReason(row),
17791
17791
  ...present(row, "suppressedByGuidance", "suppressed_by_guidance"),
17792
17792
  fix_status: row.fixStatus ?? "open",
17793
- lens: row.lens ?? "persona",
17793
+ lens: "lens" in row ? row.lens ?? "persona" : localFrontmatter?.lens ?? "persona",
17794
17794
  // The designer's fields, when the server has them.
17795
17795
  ...present(row, "source", "source"),
17796
17796
  ...present(row, "basis", "basis"),
@@ -17826,6 +17826,20 @@ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17826
17826
  citations: row.citations ?? [],
17827
17827
  ...timestamps(row)
17828
17828
  };
17829
+ if (localFrontmatter) {
17830
+ for (const [field2, key] of LOCAL_WHEN_ABSENT) {
17831
+ if (field2 in row || !(key in localFrontmatter))
17832
+ continue;
17833
+ if (localFrontmatter[key] === void 0)
17834
+ continue;
17835
+ frontmatter[key] = localFrontmatter[key];
17836
+ }
17837
+ for (const key of LOCAL_ONLY) {
17838
+ if (localFrontmatter[key] !== void 0 && !(key in frontmatter)) {
17839
+ frontmatter[key] = localFrontmatter[key];
17840
+ }
17841
+ }
17842
+ }
17829
17843
  return { frontmatter, body: resolveBody(row, descriptionParts.join("\n")) };
17830
17844
  }
17831
17845
  function present(row, field2, key) {
@@ -17875,12 +17889,33 @@ function buildGuidanceFrontmatter(row, slug, capabilityIdToSlug, outlineIdToSlug
17875
17889
  body: resolveBody(row, composeGuidanceBody(row.rule, row.reason))
17876
17890
  };
17877
17891
  }
17892
+ var LOCAL_WHEN_ABSENT, LOCAL_ONLY;
17878
17893
  var init_frontmatter2 = __esm({
17879
17894
  "../../packages/local-store/dist/pull/frontmatter.js"() {
17880
17895
  "use strict";
17881
17896
  init_dist();
17882
17897
  init_paths();
17883
17898
  init_types2();
17899
+ LOCAL_WHEN_ABSENT = [
17900
+ ["source", "source"],
17901
+ ["basis", "basis"],
17902
+ ["category", "category"],
17903
+ ["screen", "screen"],
17904
+ ["steps", "steps"],
17905
+ ["evidence", "evidence"],
17906
+ ["what", "what"],
17907
+ ["why", "why"],
17908
+ ["breaksRule", "breaks"],
17909
+ ["breaksFile", "breaks_file"],
17910
+ ["designCauseOf", "design_cause_of"],
17911
+ ["seenIn", "seen_in"],
17912
+ ["generatedBy", "generated_by"],
17913
+ ["dispatch", "dispatch"],
17914
+ ["uiFact", "ui_fact"],
17915
+ ["consequence", "consequence"],
17916
+ ["walkthroughStepNumber", "walkthrough_step_id"]
17917
+ ];
17918
+ LOCAL_ONLY = ["canonical_id", "design_cause_of"];
17884
17919
  }
17885
17920
  });
17886
17921
 
@@ -18413,7 +18448,7 @@ async function pullFindings(projectRoot, cloud, projectId, personaIdToSlug, poli
18413
18448
  const existingSlug = localIndex.byId.get(row.id);
18414
18449
  const base = row.title ? slugify(row.title) : `finding-${row.id.slice(0, 8)}`;
18415
18450
  const slug = existingSlug ?? deriveUniqueSlug(base || "finding", usedSlugs, row.id);
18416
- const { frontmatter, body } = buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId);
18451
+ const { frontmatter, body } = buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId, localIndex.frontmatterById.get(row.id));
18417
18452
  await applyEntityPullOutcome(projectRoot, "finding", row, slug, frontmatter, body, localIndex, policy, result.pulls.finding, batch);
18418
18453
  }
18419
18454
  await finalizePullBatch(projectRoot, batch);
@@ -19712,6 +19747,13 @@ async function migrateFindings(projectRoot, apply, plan) {
19712
19747
  } catch {
19713
19748
  return;
19714
19749
  }
19750
+ const records = /* @__PURE__ */ new Map();
19751
+ const issuesOf = async (runId) => {
19752
+ if (!records.has(runId)) {
19753
+ records.set(runId, (await readDesignerRecord(projectRoot, runId))?.issues ?? null);
19754
+ }
19755
+ return records.get(runId) ?? null;
19756
+ };
19715
19757
  for (const name of names2) {
19716
19758
  const file = path35.join(dir, name);
19717
19759
  const slug = name.slice(0, -3);
@@ -19744,18 +19786,72 @@ async function migrateFindings(projectRoot, apply, plan) {
19744
19786
  });
19745
19787
  continue;
19746
19788
  }
19747
- if (fm.lens === "designer" && fm.source == null) {
19748
- text = setFrontmatterScalar(text, "source", "designer");
19749
- fm.source = "designer";
19750
- changes.push("source: designer (from lens)");
19751
- }
19752
- const designer2 = findingSource({
19753
- source: typeof fm.source === "string" ? fm.source : null,
19754
- lens: typeof fm.lens === "string" ? fm.lens : null
19755
- }) === "designer";
19756
- if (designer2 && !fm.design_cause_of && !fm.canonical_id && typeof fm.id === "string" && fm.id) {
19757
- text = setFrontmatterScalar(text, "canonical_id", yamlScalar(fm.id));
19758
- changes.push("canonical_id: its own id");
19789
+ const set2 = (key, value) => {
19790
+ text = setFrontmatterScalar(text, key, yamlValue(value));
19791
+ fm[key] = value;
19792
+ };
19793
+ const blank = (v) => v == null || typeof v === "string" && v.trim() === "" || Array.isArray(v) && v.length === 0;
19794
+ const designer2 = fm.lens === "designer" || fm.source === "designer";
19795
+ if (designer2) {
19796
+ const runId = designerRunOf(fm);
19797
+ const issues = runId ? await issuesOf(runId) : null;
19798
+ const issue = issues ? issues.find((i) => i.finding_id === fm.id) ?? issues.find((i) => typeof fm.title === "string" && fm.title && i.title === fm.title) : void 0;
19799
+ if (issue) {
19800
+ const restored = [];
19801
+ const fields = [
19802
+ ["basis", issue.basis],
19803
+ ["category", issue.kind],
19804
+ ["screen", issue.screen],
19805
+ ["what", issue.what],
19806
+ ["why", issue.why],
19807
+ ["steps", issue.steps],
19808
+ ["evidence", issue.evidence],
19809
+ ["title", issue.title],
19810
+ ["breaks", issue.breaks]
19811
+ ];
19812
+ for (const [key, value] of fields) {
19813
+ if (!blank(fm[key]) || blank(value))
19814
+ continue;
19815
+ set2(key, value);
19816
+ restored.push(key);
19817
+ }
19818
+ if (issue.design_cause_of) {
19819
+ if (fm.design_cause_of !== issue.design_cause_of) {
19820
+ set2("design_cause_of", issue.design_cause_of);
19821
+ restored.push("design_cause_of");
19822
+ }
19823
+ if (fm.canonical_id) {
19824
+ set2("canonical_id", null);
19825
+ restored.push("canonical_id: none (a design cause)");
19826
+ }
19827
+ } else if (typeof issue.canonical_id === "string" && issue.canonical_id && issue.canonical_id !== fm.canonical_id && (!fm.canonical_id || fm.canonical_id === fm.id)) {
19828
+ set2("canonical_id", issue.canonical_id);
19829
+ restored.push(fm.id === issue.canonical_id ? "canonical_id: its own id" : "canonical_id: the one the record shares");
19830
+ }
19831
+ if (restored.length > 0) {
19832
+ changes.push(`from the run's designer.json: ${restored.join(", ")}`);
19833
+ }
19834
+ }
19835
+ const missing = findingSourceErrors({
19836
+ ...fm,
19837
+ source: "designer"
19838
+ }).map((e) => e.split(" ")[0]).filter((f) => ["what", "why", "basis", "screen"].includes(f));
19839
+ if (missing.length > 0) {
19840
+ plan.notices.push({
19841
+ target: `iamu/findings/${name}`,
19842
+ what: `a designer finding missing ${missing.join(", ")}${runId ? `, and iamu/.runs/${runId}/designer.json has no record of it` : ", and no run to restore it from"}`,
19843
+ fix: "fill them in from the run's designer.json, or delete the file and pull it again once the cloud sends the designer's fields"
19844
+ });
19845
+ continue;
19846
+ }
19847
+ if (fm.source == null) {
19848
+ set2("source", "designer");
19849
+ changes.push("source: designer (from lens)");
19850
+ }
19851
+ if (!fm.design_cause_of && !fm.canonical_id && typeof fm.id === "string" && fm.id) {
19852
+ set2("canonical_id", fm.id);
19853
+ changes.push("canonical_id: its own id");
19854
+ }
19759
19855
  }
19760
19856
  if (changes.length === 0)
19761
19857
  continue;
@@ -19776,6 +19872,19 @@ async function migrateFindings(projectRoot, apply, plan) {
19776
19872
  });
19777
19873
  }
19778
19874
  }
19875
+ function designerRunOf(fm) {
19876
+ if (typeof fm.run === "string" && fm.run)
19877
+ return fm.run;
19878
+ const m = /^(?:designer|ux):(.+)$/.exec(typeof fm.seen_in === "string" ? fm.seen_in : "");
19879
+ return m ? m[1] : null;
19880
+ }
19881
+ function yamlValue(value) {
19882
+ if (value === null)
19883
+ return "null";
19884
+ if (typeof value === "string")
19885
+ return yamlScalar(value);
19886
+ return JSON.stringify(value);
19887
+ }
19779
19888
  function yamlScalar(value) {
19780
19889
  try {
19781
19890
  if (jsYaml.load(value, { schema: jsYaml.JSON_SCHEMA }) === value)
@@ -22113,7 +22222,7 @@ function rolesOf(personas, outlines, names2) {
22113
22222
  return out2;
22114
22223
  }
22115
22224
  function situationFacts(texts, roles) {
22116
- const roleWords = new Set(roles.flatMap((r) => r.split(/[^\p{L}]+/u)).filter(Boolean));
22225
+ const roleWords = new Set(roles.flatMap((r) => [...r.split(/[^\p{L}'’-]+/u), ...r.split(/[^\p{L}]+/u)]).map((w) => w.replace(/['’]s?$/, "")).filter(Boolean));
22117
22226
  const out2 = /* @__PURE__ */ new Set();
22118
22227
  for (const text of texts) {
22119
22228
  const dates = [...text.matchAll(new RegExp(DATE2.source, "g"))].map((m) => m[0]);
@@ -22151,7 +22260,8 @@ async function readDraftContext(projectRoot, opts = {}) {
22151
22260
  const outlines = (await listEntities(projectRoot, "outline")).entities.map((e) => e.frontmatter);
22152
22261
  const names2 = personas.map((p) => p.name);
22153
22262
  const roles = rolesOf(personas, outlines, names2);
22154
- const doNotQuote = situationFacts(outlines.flatMap((o) => [o.title, o.description, o.narrative, o.tension].filter((t) => Boolean(t))), roles.map((r) => r.role));
22263
+ const contextText = await fs47.readFile(path42.join(projectRoot, "iamu", "context.yml"), "utf8").catch(() => "");
22264
+ const doNotQuote = situationFacts(outlines.flatMap((o) => [o.title, o.description, o.narrative, o.tension].filter((t) => Boolean(t))), [...roles.map((r) => r.role), contextText]);
22155
22265
  const situations = outlines.map((o) => o.title.trim()).filter((t) => t && !namesPersona(t, names2) && !doNotQuote.some((f) => quotes(t, f)));
22156
22266
  const urlPrefixes = files.flatMap((f) => dir && (f.applies_to ?? []).includes(dir) ? (f.applies_to ?? []).filter(isUrlPrefix2) : []);
22157
22267
  return {
@@ -22514,6 +22624,8 @@ function checkFrontmatter(text, front, ctx, errors) {
22514
22624
  });
22515
22625
  return;
22516
22626
  }
22627
+ if (ctx.tokenFiles.length === 0)
22628
+ return;
22517
22629
  const defined = definedValues(ctx);
22518
22630
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
22519
22631
  for (const [key, value] of leaves(parsed)) {
@@ -22523,7 +22635,7 @@ function checkFrontmatter(text, front, ctx, errors) {
22523
22635
  const at = lines.findIndex((l, i) => i >= front.first - 1 && i < front.last && l.includes(value));
22524
22636
  errors.push({
22525
22637
  code: "value_not_a_token",
22526
- message: `${key}: ${value} is not a value the token sources define${ctx.tokenFiles.length === 0 ? " (no token source is recorded or found)" : ""}`,
22638
+ message: `${key}: ${value} is not a value the token sources define`,
22527
22639
  line: at === -1 ? null : at + 1
22528
22640
  });
22529
22641
  }
@@ -25435,8 +25547,8 @@ import { dirname as dirname3, join as join5, relative, resolve as resolve2 } fro
25435
25547
  import { fileURLToPath as fileURLToPath2 } from "url";
25436
25548
  import chalk6 from "chalk";
25437
25549
  function getVersion() {
25438
- if ("0.8.0-beta.8") {
25439
- return "0.8.0-beta.8";
25550
+ if ("0.8.0-beta.9") {
25551
+ return "0.8.0-beta.9";
25440
25552
  }
25441
25553
  try {
25442
25554
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -1 +1 @@
1
- eb30eaadd356dc88c43923cfcedb8242b496521ce2a8618863d9a180267cdc11
1
+ 6189ae317dafbd552e3bbc0664b1a3ff5a5845d05f9a606c2114466469d5ab65
package/dist/index.js CHANGED
@@ -17765,7 +17765,7 @@ function buildScenarioFrontmatter(row, personaSlug, outlineSlug, localFrontmatte
17765
17765
  };
17766
17766
  return { frontmatter, body: resolveBody(row, row.brief ?? ""), slug };
17767
17767
  }
17768
- function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17768
+ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId, localFrontmatter) {
17769
17769
  const descriptionParts = [];
17770
17770
  if (row.description)
17771
17771
  descriptionParts.push(row.description);
@@ -17789,7 +17789,7 @@ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17789
17789
  ...dismissReason(row),
17790
17790
  ...present(row, "suppressedByGuidance", "suppressed_by_guidance"),
17791
17791
  fix_status: row.fixStatus ?? "open",
17792
- lens: row.lens ?? "persona",
17792
+ lens: "lens" in row ? row.lens ?? "persona" : localFrontmatter?.lens ?? "persona",
17793
17793
  // The designer's fields, when the server has them.
17794
17794
  ...present(row, "source", "source"),
17795
17795
  ...present(row, "basis", "basis"),
@@ -17825,6 +17825,20 @@ function buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId) {
17825
17825
  citations: row.citations ?? [],
17826
17826
  ...timestamps(row)
17827
17827
  };
17828
+ if (localFrontmatter) {
17829
+ for (const [field2, key] of LOCAL_WHEN_ABSENT) {
17830
+ if (field2 in row || !(key in localFrontmatter))
17831
+ continue;
17832
+ if (localFrontmatter[key] === void 0)
17833
+ continue;
17834
+ frontmatter[key] = localFrontmatter[key];
17835
+ }
17836
+ for (const key of LOCAL_ONLY) {
17837
+ if (localFrontmatter[key] !== void 0 && !(key in frontmatter)) {
17838
+ frontmatter[key] = localFrontmatter[key];
17839
+ }
17840
+ }
17841
+ }
17828
17842
  return { frontmatter, body: resolveBody(row, descriptionParts.join("\n")) };
17829
17843
  }
17830
17844
  function present(row, field2, key) {
@@ -17874,12 +17888,33 @@ function buildGuidanceFrontmatter(row, slug, capabilityIdToSlug, outlineIdToSlug
17874
17888
  body: resolveBody(row, composeGuidanceBody(row.rule, row.reason))
17875
17889
  };
17876
17890
  }
17891
+ var LOCAL_WHEN_ABSENT, LOCAL_ONLY;
17877
17892
  var init_frontmatter2 = __esm({
17878
17893
  "../../packages/local-store/dist/pull/frontmatter.js"() {
17879
17894
  "use strict";
17880
17895
  init_dist();
17881
17896
  init_paths();
17882
17897
  init_types2();
17898
+ LOCAL_WHEN_ABSENT = [
17899
+ ["source", "source"],
17900
+ ["basis", "basis"],
17901
+ ["category", "category"],
17902
+ ["screen", "screen"],
17903
+ ["steps", "steps"],
17904
+ ["evidence", "evidence"],
17905
+ ["what", "what"],
17906
+ ["why", "why"],
17907
+ ["breaksRule", "breaks"],
17908
+ ["breaksFile", "breaks_file"],
17909
+ ["designCauseOf", "design_cause_of"],
17910
+ ["seenIn", "seen_in"],
17911
+ ["generatedBy", "generated_by"],
17912
+ ["dispatch", "dispatch"],
17913
+ ["uiFact", "ui_fact"],
17914
+ ["consequence", "consequence"],
17915
+ ["walkthroughStepNumber", "walkthrough_step_id"]
17916
+ ];
17917
+ LOCAL_ONLY = ["canonical_id", "design_cause_of"];
17883
17918
  }
17884
17919
  });
17885
17920
 
@@ -18412,7 +18447,7 @@ async function pullFindings(projectRoot, cloud, projectId, personaIdToSlug, poli
18412
18447
  const existingSlug = localIndex.byId.get(row.id);
18413
18448
  const base = row.title ? slugify(row.title) : `finding-${row.id.slice(0, 8)}`;
18414
18449
  const slug = existingSlug ?? deriveUniqueSlug(base || "finding", usedSlugs, row.id);
18415
- const { frontmatter, body } = buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId);
18450
+ const { frontmatter, body } = buildFindingFrontmatter(row, slug, personaSlug, scenarioSlug, runId, localIndex.frontmatterById.get(row.id));
18416
18451
  await applyEntityPullOutcome(projectRoot, "finding", row, slug, frontmatter, body, localIndex, policy, result.pulls.finding, batch);
18417
18452
  }
18418
18453
  await finalizePullBatch(projectRoot, batch);
@@ -19711,6 +19746,13 @@ async function migrateFindings(projectRoot, apply, plan) {
19711
19746
  } catch {
19712
19747
  return;
19713
19748
  }
19749
+ const records = /* @__PURE__ */ new Map();
19750
+ const issuesOf = async (runId) => {
19751
+ if (!records.has(runId)) {
19752
+ records.set(runId, (await readDesignerRecord(projectRoot, runId))?.issues ?? null);
19753
+ }
19754
+ return records.get(runId) ?? null;
19755
+ };
19714
19756
  for (const name of names2) {
19715
19757
  const file = path35.join(dir, name);
19716
19758
  const slug = name.slice(0, -3);
@@ -19743,18 +19785,72 @@ async function migrateFindings(projectRoot, apply, plan) {
19743
19785
  });
19744
19786
  continue;
19745
19787
  }
19746
- if (fm.lens === "designer" && fm.source == null) {
19747
- text = setFrontmatterScalar(text, "source", "designer");
19748
- fm.source = "designer";
19749
- changes.push("source: designer (from lens)");
19750
- }
19751
- const designer2 = findingSource({
19752
- source: typeof fm.source === "string" ? fm.source : null,
19753
- lens: typeof fm.lens === "string" ? fm.lens : null
19754
- }) === "designer";
19755
- if (designer2 && !fm.design_cause_of && !fm.canonical_id && typeof fm.id === "string" && fm.id) {
19756
- text = setFrontmatterScalar(text, "canonical_id", yamlScalar(fm.id));
19757
- changes.push("canonical_id: its own id");
19788
+ const set2 = (key, value) => {
19789
+ text = setFrontmatterScalar(text, key, yamlValue(value));
19790
+ fm[key] = value;
19791
+ };
19792
+ const blank = (v) => v == null || typeof v === "string" && v.trim() === "" || Array.isArray(v) && v.length === 0;
19793
+ const designer2 = fm.lens === "designer" || fm.source === "designer";
19794
+ if (designer2) {
19795
+ const runId = designerRunOf(fm);
19796
+ const issues = runId ? await issuesOf(runId) : null;
19797
+ const issue = issues ? issues.find((i) => i.finding_id === fm.id) ?? issues.find((i) => typeof fm.title === "string" && fm.title && i.title === fm.title) : void 0;
19798
+ if (issue) {
19799
+ const restored = [];
19800
+ const fields = [
19801
+ ["basis", issue.basis],
19802
+ ["category", issue.kind],
19803
+ ["screen", issue.screen],
19804
+ ["what", issue.what],
19805
+ ["why", issue.why],
19806
+ ["steps", issue.steps],
19807
+ ["evidence", issue.evidence],
19808
+ ["title", issue.title],
19809
+ ["breaks", issue.breaks]
19810
+ ];
19811
+ for (const [key, value] of fields) {
19812
+ if (!blank(fm[key]) || blank(value))
19813
+ continue;
19814
+ set2(key, value);
19815
+ restored.push(key);
19816
+ }
19817
+ if (issue.design_cause_of) {
19818
+ if (fm.design_cause_of !== issue.design_cause_of) {
19819
+ set2("design_cause_of", issue.design_cause_of);
19820
+ restored.push("design_cause_of");
19821
+ }
19822
+ if (fm.canonical_id) {
19823
+ set2("canonical_id", null);
19824
+ restored.push("canonical_id: none (a design cause)");
19825
+ }
19826
+ } else if (typeof issue.canonical_id === "string" && issue.canonical_id && issue.canonical_id !== fm.canonical_id && (!fm.canonical_id || fm.canonical_id === fm.id)) {
19827
+ set2("canonical_id", issue.canonical_id);
19828
+ restored.push(fm.id === issue.canonical_id ? "canonical_id: its own id" : "canonical_id: the one the record shares");
19829
+ }
19830
+ if (restored.length > 0) {
19831
+ changes.push(`from the run's designer.json: ${restored.join(", ")}`);
19832
+ }
19833
+ }
19834
+ const missing = findingSourceErrors({
19835
+ ...fm,
19836
+ source: "designer"
19837
+ }).map((e) => e.split(" ")[0]).filter((f) => ["what", "why", "basis", "screen"].includes(f));
19838
+ if (missing.length > 0) {
19839
+ plan.notices.push({
19840
+ target: `iamu/findings/${name}`,
19841
+ what: `a designer finding missing ${missing.join(", ")}${runId ? `, and iamu/.runs/${runId}/designer.json has no record of it` : ", and no run to restore it from"}`,
19842
+ fix: "fill them in from the run's designer.json, or delete the file and pull it again once the cloud sends the designer's fields"
19843
+ });
19844
+ continue;
19845
+ }
19846
+ if (fm.source == null) {
19847
+ set2("source", "designer");
19848
+ changes.push("source: designer (from lens)");
19849
+ }
19850
+ if (!fm.design_cause_of && !fm.canonical_id && typeof fm.id === "string" && fm.id) {
19851
+ set2("canonical_id", fm.id);
19852
+ changes.push("canonical_id: its own id");
19853
+ }
19758
19854
  }
19759
19855
  if (changes.length === 0)
19760
19856
  continue;
@@ -19775,6 +19871,19 @@ async function migrateFindings(projectRoot, apply, plan) {
19775
19871
  });
19776
19872
  }
19777
19873
  }
19874
+ function designerRunOf(fm) {
19875
+ if (typeof fm.run === "string" && fm.run)
19876
+ return fm.run;
19877
+ const m = /^(?:designer|ux):(.+)$/.exec(typeof fm.seen_in === "string" ? fm.seen_in : "");
19878
+ return m ? m[1] : null;
19879
+ }
19880
+ function yamlValue(value) {
19881
+ if (value === null)
19882
+ return "null";
19883
+ if (typeof value === "string")
19884
+ return yamlScalar(value);
19885
+ return JSON.stringify(value);
19886
+ }
19778
19887
  function yamlScalar(value) {
19779
19888
  try {
19780
19889
  if (jsYaml.load(value, { schema: jsYaml.JSON_SCHEMA }) === value)
@@ -22112,7 +22221,7 @@ function rolesOf(personas, outlines, names2) {
22112
22221
  return out2;
22113
22222
  }
22114
22223
  function situationFacts(texts, roles) {
22115
- const roleWords = new Set(roles.flatMap((r) => r.split(/[^\p{L}]+/u)).filter(Boolean));
22224
+ const roleWords = new Set(roles.flatMap((r) => [...r.split(/[^\p{L}'’-]+/u), ...r.split(/[^\p{L}]+/u)]).map((w) => w.replace(/['’]s?$/, "")).filter(Boolean));
22116
22225
  const out2 = /* @__PURE__ */ new Set();
22117
22226
  for (const text of texts) {
22118
22227
  const dates = [...text.matchAll(new RegExp(DATE2.source, "g"))].map((m) => m[0]);
@@ -22150,7 +22259,8 @@ async function readDraftContext(projectRoot, opts = {}) {
22150
22259
  const outlines = (await listEntities(projectRoot, "outline")).entities.map((e) => e.frontmatter);
22151
22260
  const names2 = personas.map((p) => p.name);
22152
22261
  const roles = rolesOf(personas, outlines, names2);
22153
- const doNotQuote = situationFacts(outlines.flatMap((o) => [o.title, o.description, o.narrative, o.tension].filter((t) => Boolean(t))), roles.map((r) => r.role));
22262
+ const contextText = await fs47.readFile(path42.join(projectRoot, "iamu", "context.yml"), "utf8").catch(() => "");
22263
+ const doNotQuote = situationFacts(outlines.flatMap((o) => [o.title, o.description, o.narrative, o.tension].filter((t) => Boolean(t))), [...roles.map((r) => r.role), contextText]);
22154
22264
  const situations = outlines.map((o) => o.title.trim()).filter((t) => t && !namesPersona(t, names2) && !doNotQuote.some((f) => quotes(t, f)));
22155
22265
  const urlPrefixes = files.flatMap((f) => dir && (f.applies_to ?? []).includes(dir) ? (f.applies_to ?? []).filter(isUrlPrefix2) : []);
22156
22266
  return {
@@ -22513,6 +22623,8 @@ function checkFrontmatter(text, front, ctx, errors) {
22513
22623
  });
22514
22624
  return;
22515
22625
  }
22626
+ if (ctx.tokenFiles.length === 0)
22627
+ return;
22516
22628
  const defined = definedValues(ctx);
22517
22629
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
22518
22630
  for (const [key, value] of leaves(parsed)) {
@@ -22522,7 +22634,7 @@ function checkFrontmatter(text, front, ctx, errors) {
22522
22634
  const at = lines.findIndex((l, i) => i >= front.first - 1 && i < front.last && l.includes(value));
22523
22635
  errors.push({
22524
22636
  code: "value_not_a_token",
22525
- message: `${key}: ${value} is not a value the token sources define${ctx.tokenFiles.length === 0 ? " (no token source is recorded or found)" : ""}`,
22637
+ message: `${key}: ${value} is not a value the token sources define`,
22526
22638
  line: at === -1 ? null : at + 1
22527
22639
  });
22528
22640
  }
@@ -25434,8 +25546,8 @@ import { dirname as dirname3, join as join5, relative, resolve as resolve2 } fro
25434
25546
  import { fileURLToPath as fileURLToPath2 } from "url";
25435
25547
  import chalk6 from "chalk";
25436
25548
  function getVersion() {
25437
- if ("0.8.0-beta.8") {
25438
- return "0.8.0-beta.8";
25549
+ if ("0.8.0-beta.9") {
25550
+ return "0.8.0-beta.9";
25439
25551
  }
25440
25552
  try {
25441
25553
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -93,7 +93,7 @@ Fix by looking, not by hoping:
93
93
  Only when no design doc is recorded: `iamu design template` refuses otherwise, and a recorded doc changes only through proposals (**C**). The template is filled in from the code, never by a model: the token values in the frontmatter (light theme), the token groups by name, the contrast clashes the tokens have, each component with the props it takes, and the roles people come in (from the personas and situations, never their names). Everything else starts as a `<placeholder>`. Auto-extract what the code and `iamu/context.yml` already answer, ask the user only for the qualitative intent and clash decisions that code cannot answer, and the result becomes the team's doc.
94
94
 
95
95
  1. **Template & Auto-Extraction (0 questions to the user).** Run `iamu design template --out iamu/.analysis/design-draft.md --json`. It is for the app the recorded tokens and components apply to, else the root; add `--dir <app>` when the user names another app, and pass the same `--dir` to the later commands. If it answers `out_exists`, a draft is in progress: ask whether to continue it (step 2 with that file) or start over (delete the file and run it again). Tell the user in two lines what came from the code (`sources`, the components, `roles`, `clashes`) and where the doc will go (`target`).
96
- Before asking the user anything, read the actual token files (`sources.tokens`), UI components (`sources.components`), page code, and `iamu/context.yml` (plus live rendered screens or recent runs if available), and pre-fill the code-evidenced placeholders in `iamu/.analysis/design-draft.md`:
96
+ Before asking the user anything, read the actual token files (`sources.tokens`), UI components (`sources.components`), page code, and `iamu/context.yml`—or, **when there is no local code (a remote site / URL-only project)**, fetch the live target URL's stylesheets and rendered HTML from `iamu/testability.md` (plus any parent/reference brand URL the user names) to extract CSS custom properties, colour/radius tokens (written into the YAML frontmatter), typography, recurring component patterns, and contrast ratios—and pre-fill the code/site-evidenced placeholders in `iamu/.analysis/design-draft.md`:
97
97
  - **Overview — Who it is for:** Synthesize who uses the product and what they come to do from `iamu/context.yml` and the roles and situations the Overview lists, then replace that list with the synthesized role-based words (roles only, never a persona's name or a single-scenario situation number).
98
98
  - **Look — Token roles:** Fill in `<what each is for>` for each extracted token group from how stylesheets and components actually apply those tokens (always name tokens in backticks, never raw hex or px values in prose).
99
99
  - **Interaction and States:** Extract how `Forms` (error reporting), `Loading`, `Failure`, and `Empty` states are actually implemented in `sources.components` and screens. Describe only the states the product has; missing state patterns go under `Not in the library yet` / `Open Questions`.
@@ -284,15 +284,16 @@ Iamu reads design docs wherever they are and never edits them on its own. Run `i
284
284
  - **Found docs** (a `DESIGN.md` in any format, design-system, brand, voice or interaction docs, design sections of `CLAUDE.md` or `AGENTS.md` shown as `CLAUDE.md#<heading>`, a `PRODUCT.md`, token files, a component library): list them once, one line each (kind, path). Show a candidate with a `note` (anything in a sample, export, fixture or spike folder) with its note, unticked. Ask once whether to record them; the user may drop some or add others. On yes, `iamu design sources <paths…>` (in a monorepo, `--applies-to` with the URL prefix or the app's folder each doc covers).
285
285
  - **A legacy `iamu/design.md`** (`iamu design status --json` shows `legacy.doc` and no `legacy.migrated_at`): fold it into the same single question. Run `iamu design migrate --json` first, and offer three answers next to the files: **move it** to a plain `DESIGN.md` the team owns (`iamu design migrate --move-to DESIGN.md --apply`), **keep** `iamu/design.md` as it is (`iamu design migrate --apply`, which records the choice so it is not offered again), or **not now** (run nothing; it is offered again next time). Only an explicit "keep" runs `--apply` without a move.
286
286
  - **Nothing found:** say so in one line: "No design docs found. The designer reviews each run by general craft until you record some (`iamu-design`) — strongly recommended so runs check your own tokens, components, and rules."
287
- - **No written design doc** (detection found only tokens or components, or nothing, and no design doc is recorded): after the question above, or in its place when there was nothing to record, explain in one sentence why a design doc makes Iamu much stronger (*"With a `DESIGN.md`, Iamu's designer checks your own tokens, components, states, and rules on every run instead of only general craft"*), and ask one question the user may skip:
287
+ - **No written design doc** (detection found only tokens or components, or nothing, and no design doc is recorded): after the question above, or in its place when there was nothing to record, explain in one sentence why a design doc makes Iamu much stronger (*"With a `DESIGN.md`, Iamu's designer checks your own tokens, components, states, and rules on every run instead of only general craft"*), and ask one standalone question the user may skip.
288
+ - **CRITICAL (Local Code AND Remote Sites):** Mode E works both when local source code exists AND when testing a remote site with no local code (on a remote site, Mode E extracts tokens, typography, component patterns, states, and contrast ratios directly from the live target URL's stylesheets/DOM or a reference brand URL). **NEVER omit Option 1 (`Yes — start DESIGN.md now`), NEVER mark `Not now` as `(Recommended)`, and NEVER batch this question into the same `ask_question` call as Step 3a**—wait for the user's answer to Step 4b first so Mode E can run if they choose Yes:
288
289
  - If interactive user-prompt tools exist (`ask_question`, `ask_followup_question`), invoke it:
289
- - **Question:** "There is no `DESIGN.md` yet. Iamu gets much sharper with one (every run checks your own tokens, components, states, and rules instead of generic craft). Start one from your code now?"
290
+ - **Question:** "There is no `DESIGN.md` yet. Iamu gets much sharper with one (every run checks your own tokens, components, states, and rules instead of generic craft). Start one now?"
290
291
  - **Options:**
291
- 1. "(Recommended) Yes — start `DESIGN.md` from my code now (auto-extracts tokens, components, states & formats, then asks 2–3 quick intent questions)"
292
+ 1. "(Recommended) Yes — start `DESIGN.md` now (auto-extracts tokens, components, states & formats from your code or live site, then asks 2–3 quick intent questions)"
292
293
  2. "Not now — finish setup first (keep reminding me to run `iamu-design`)"
293
294
  3. "No — review by general craft only and stop offering `DESIGN.md`"
294
295
  - Otherwise (text-only/CLI):
295
- "There is no design doc yet, and Iamu gets much sharper with one (checking your own tokens, components, and rules on every run). Shall I start one from your code? I auto-extract your tokens, components, states, formats and contrast clashes from code, then ask 2–3 quick questions on feel and clash rules. **Yes** (recommended) / **not now** (keep recommending `iamu-design`) / **no**."
296
+ "There is no design doc yet, and Iamu gets much sharper with one (checking your own tokens, components, and rules on every run). Shall I start one now? I auto-extract your tokens, components, states, formats and contrast clashes from your code or live site, then ask 2–3 quick questions on feel and clash rules. **Yes** (recommended) / **not now** (keep recommending `iamu-design`) / **no**."
296
297
  - **Yes:** follow the `iamu-design` skill, mode E, then carry on with setup.
297
298
  - **Not now:** run nothing; it stays recommended at the end of setup, in `iamu status`, and after runs until a design doc is recorded.
298
299
  - **No:** run `iamu design template --decline`, so runs stop offering it.
@@ -442,7 +443,7 @@ Next step:
442
443
  iamu-run <persona-slug>--<outline-slug>
443
444
 
444
445
  Other commands when you're ready:
445
- • iamu-design Create or record DESIGN.md from your code (strongly recommended if not set yet: makes every run check your own tokens, components, and rules instead of general craft)
446
+ • iamu-design Create or record DESIGN.md from your code or live site (strongly recommended if not set yet: makes every run check your own tokens, components, and rules instead of general craft)
446
447
  • iamu-new Draft a scenario for a different product flow
447
448
  • iamu-propose Brainstorm high-risk workflows and blind spots
448
449
  • iamu doctor Diagnose test environment and target health
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamu-dev/iamu",
3
- "version": "0.8.0-beta.8",
3
+ "version": "0.8.0-beta.9",
4
4
  "type": "module",
5
5
  "description": "Test your product as realistic personas — scenario-driven development CLI",
6
6
  "license": "MIT",
@@ -58,9 +58,9 @@
58
58
  "tsup": "^8.5.1",
59
59
  "typescript": "^5.8.0",
60
60
  "vitest": "^4.1.11",
61
- "@iamu-dev/local-store": "0.1.0",
62
61
  "@iamu/tsconfig": "0.1.0",
63
- "@iamu-dev/protocol": "0.1.0"
62
+ "@iamu-dev/protocol": "0.1.0",
63
+ "@iamu-dev/local-store": "0.1.0"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsup && tsc -b --force tsconfig.build.json --emitDeclarationOnly && node scripts/copy-assets.mjs",