@amityco/social-plus-vise 1.0.0 → 1.1.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +71 -24
  2. package/LICENSE +8 -6
  3. package/README.md +168 -358
  4. package/dist/capabilities.js +19 -62
  5. package/dist/intelligence/grounding.js +0 -23
  6. package/dist/intelligence/placement.js +0 -9
  7. package/dist/outcomes.js +44 -22
  8. package/dist/server.js +75 -38
  9. package/dist/tools/ast.js +3 -209
  10. package/dist/tools/blocks.js +6 -20
  11. package/dist/tools/compliance.js +168 -43
  12. package/dist/tools/creative.js +15 -41
  13. package/dist/tools/debug.js +0 -16
  14. package/dist/tools/design.js +18 -364
  15. package/dist/tools/docs.js +53 -24
  16. package/dist/tools/experienceCompiler.js +7 -10
  17. package/dist/tools/experienceSensors.js +1 -1
  18. package/dist/tools/harness.js +2 -27
  19. package/dist/tools/integration.js +6 -38
  20. package/dist/tools/learning.js +1 -1
  21. package/dist/tools/project.js +763 -546
  22. package/dist/tools/sdkFacts.js +2 -15
  23. package/dist/tools/sdkVersion.js +3 -36
  24. package/dist/tools/sensors.js +0 -6
  25. package/dist/tools/uxHarness.js +12 -9
  26. package/package.json +8 -97
  27. package/rules/chat.yaml +225 -0
  28. package/rules/event.yaml +45 -0
  29. package/rules/feed.yaml +24 -24
  30. package/rules/invitation.yaml +58 -0
  31. package/rules/live-data.yaml +104 -2
  32. package/rules/notification-tray.yaml +106 -0
  33. package/rules/poll.yaml +71 -0
  34. package/rules/sdk-lifecycle.yaml +112 -6
  35. package/rules/search.yaml +131 -0
  36. package/rules/story.yaml +221 -0
  37. package/rules/user-blocking.yaml +71 -0
  38. package/sdk-surface/flutter.json +1 -1
  39. package/sdk-surface/ios.json +1 -1
  40. package/sdk-surface/manifest.json +12 -12
  41. package/sdk-surface/models.flutter.json +96 -96
  42. package/sdk-surface/models.ios.json +1 -1
  43. package/sdk-surface/typescript.json +4 -4
  44. package/skills/social-plus-vise/SKILL.md +25 -5
  45. package/scripts/catalog-coverage-html.mjs +0 -325
  46. package/scripts/catalog-relationships-html.mjs +0 -686
  47. package/scripts/catalog-sheets.mjs +0 -286
  48. package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
  49. package/scripts/dart-model-extractor/pubspec.lock +0 -149
  50. package/scripts/dart-model-extractor/pubspec.yaml +0 -16
  51. package/scripts/extract-sdk-models.mjs +0 -749
  52. package/scripts/import-sdk-surface.mjs +0 -161
  53. package/scripts/pilot-feedback.mjs +0 -107
  54. package/scripts/workshop-board-html.mjs +0 -1018
  55. package/scripts/workshop-kit.mjs +0 -252
  56. package/skills/vise-harness-engineer/SKILL.md +0 -35
@@ -4,33 +4,15 @@ import path from "node:path";
4
4
  import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
5
5
  import { packageVersion } from "../version.js";
6
6
  import { tryParse } from "./ast.js";
7
- /**
8
- * Design-contract extractor.
9
- *
10
- * Ingests an HTML/CSS prototype and produces a *graded* design contract:
11
- * declared CSS custom properties are recorded as EXACT tokens; repeated literal
12
- * values are recorded as INFERRED tokens; component shapes from the DOM are
13
- * recorded as ADVISORY observations. Provenance is first-class so downstream
14
- * consumers never treat an inferred value as authoritative.
15
- *
16
- * Design principle (mirrors `tryParse` degrading to regex): a messy prototype
17
- * yields a *weaker* contract, never a *wrong* one. Less signal -> fewer tokens,
18
- * never fabricated ones.
19
- */
20
7
  export const DESIGN_CONTRACT_SCHEMA_VERSION = 1;
21
8
  export const DESIGN_CONTRACT_FILENAME = "design-contract.json";
22
9
  export const DESIGN_PREVIEW_FILENAME = "design-preview.html";
23
10
  export const DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID = "design_contract_confirmation";
24
- /** A literal value must appear at least this many times to become an inferred token. Single-use literals are one-offs (a scrim, one accent), not design tokens. */
25
11
  export const INFERRED_MIN_USES = 2;
26
- /** Bound the prototype walk so a large input directory cannot stall extraction. */
27
12
  const MAX_PROTOTYPE_FILES = 300;
28
- /** Bound the source scan for the advisory `design check`. */
29
13
  const MAX_SCAN_FILES = 2000;
30
- /** Cap the off-contract sample so the advisory report stays readable. */
31
14
  const OFF_CONTRACT_SAMPLE = 20;
32
15
  const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".css", ".scss", ".vue", ".xml"]);
33
- /** Extensions whose design tokens are extracted by parsing object literals (TS/JS token modules, tailwind config). */
34
16
  const MODULE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
35
17
  const MAX_FILE_BYTES = 2_000_000;
36
18
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", "vendor", ".turbo", ".cache"]);
@@ -55,9 +37,6 @@ export function designContractConfirmationFromAnswers(answers) {
55
37
  export function designPreviewPath(repoPath) {
56
38
  return path.join(path.resolve(repoPath), "sp-vise", DESIGN_PREVIEW_FILENAME);
57
39
  }
58
- // ---------------------------------------------------------------------------
59
- // Public entry points
60
- // ---------------------------------------------------------------------------
61
40
  export const designExtractTool = {
62
41
  name: "design_extract",
63
42
  description: "Extract a graded design contract (declared/inferred tokens + advisory components) for design-conformant social.plus UI. Source is an HTML/CSS prototype, or — with fromProject — the host project's own design system (CSS vars, token modules, tailwind config).",
@@ -116,15 +95,6 @@ export async function extractDesignContract(prototypePath) {
116
95
  const sources = await readPrototypeSources(resolved);
117
96
  return buildDesignContract(sources, { kind: "html-css-prototype", inputs: sources.inputs, file_count: sources.inputs.length });
118
97
  }
119
- /**
120
- * Derive a design contract from the *host project's own* design system, for when
121
- * the customer gives no external prototype. Scopes to the design-source files
122
- * Vise detects (theme/token modules, tailwind config, global CSS) — never the
123
- * whole repo — and extracts: CSS custom properties (incl. shadcn `:root` and
124
- * Tailwind v4 `@theme`), plus concrete tokens from TS/JS token modules and
125
- * inline tailwind configs. References (`var()`/`theme()`/`calc()`) are rejected,
126
- * so a var-mapped config contributes nothing rather than wrong tokens.
127
- */
128
98
  export async function extractDesignContractFromProject(repoPath) {
129
99
  const root = path.resolve(repoPath);
130
100
  const files = await findProjectDesignFiles(root);
@@ -187,14 +157,13 @@ export async function extractDesignContractFromProject(repoPath) {
187
157
  }
188
158
  }
189
159
  inputs.sort();
190
- // Hash each source file so design check can detect staleness without re-extracting.
191
160
  const input_digests = {};
192
161
  for (const rel of inputs) {
193
162
  try {
194
163
  const content = await readFile(path.join(root, rel), "utf8");
195
164
  input_digests[rel] = `sha256:${createHash("sha256").update(content).digest("hex")}`;
196
165
  }
197
- catch { /* file read already succeeded above; defensive only */ }
166
+ catch { }
198
167
  }
199
168
  return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
200
169
  }
@@ -228,17 +197,6 @@ export async function writeDesignPreview(repoPath, contract, referencePath) {
228
197
  await writeFile(target, html, "utf8");
229
198
  return target;
230
199
  }
231
- // ---------------------------------------------------------------------------
232
- // Visual contract review + conformance report (advisory, dependency-free)
233
- // ---------------------------------------------------------------------------
234
- //
235
- // Vise produces the visual artifact; a human (or a VLM) judges whether the
236
- // generated UI matches. This is NOT an automated pixel/render diff — that would
237
- // need a headless browser (non-deterministic, heavy dep) and does not belong in
238
- // Vise's deterministic, dependency-free core. The honest comparison data is:
239
- // (1) the contract's tokens rendered as visual swatches, (2) the actual HTML
240
- // reference embedded beside them when renderable, and (3) the `design check`
241
- // conformance numbers (coverage + on/off-contract) as the textual diff.
242
200
  export const designPreviewTool = {
243
201
  name: "design_preview",
244
202
  description: "Generate a self-contained HTML visual review of the design contract (token swatches + embedded HTML reference + conformance report) for human/VLM judgment. Advisory, non-blocking; not an automated pixel diff.",
@@ -281,7 +239,6 @@ export const designPreviewTool = {
281
239
  });
282
240
  },
283
241
  };
284
- /** Read an HTML prototype into a self-contained string (inlining linked stylesheets) for embedding. Returns null if there's no renderable HTML. */
285
242
  export async function readReferenceHtml(referencePath) {
286
243
  const resolved = path.resolve(referencePath);
287
244
  let htmlFile = null;
@@ -312,14 +269,13 @@ export async function readReferenceHtml(referencePath) {
312
269
  catch {
313
270
  return null;
314
271
  }
315
- // Inline <link rel="stylesheet" href="..."> so the embed is self-contained.
316
272
  const dir = path.dirname(htmlFile);
317
273
  const linkPattern = /<link[^>]*rel\s*=\s*["']stylesheet["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi;
318
274
  const links = [...html.matchAll(linkPattern)];
319
275
  for (const link of links) {
320
276
  const href = link[1];
321
277
  if (/^https?:|^\/\//i.test(href)) {
322
- continue; // external — leave as-is (won't load in sandboxed iframe, that's fine)
278
+ continue;
323
279
  }
324
280
  try {
325
281
  const cssPath = path.join(dir, href);
@@ -327,7 +283,6 @@ export async function readReferenceHtml(referencePath) {
327
283
  html = html.replace(link[0], `<style>\n${css}\n</style>`);
328
284
  }
329
285
  catch {
330
- // leave the link; missing CSS just won't apply
331
286
  }
332
287
  }
333
288
  return html;
@@ -420,29 +375,10 @@ function esc(s) {
420
375
  function escAttr(s) {
421
376
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
422
377
  }
423
- /** Sanitize a contract value before use inside an inline CSS `style` attribute (values come from the user's own files, but keep the preview from breaking out of the attribute). */
424
378
  function safeCss(value) {
425
379
  return value.replace(/[<>"]/g, "").slice(0, 200);
426
380
  }
427
- // ---------------------------------------------------------------------------
428
- // Social-plus token scaffold (vise design init-tokens)
429
- // ---------------------------------------------------------------------------
430
- //
431
- // Creates a dedicated `src/styles/social-plus-tokens.css` in the customer's
432
- // project — the single editable source for social.plus feature styling.
433
- // The contract always points at this file; customers edit it freely without
434
- // needing an AI agent. Design check detects changes and prompts re-extract.
435
- //
436
- // Two cases:
437
- // Greenfield (no existing design system): scaffold Option-B neutral defaults.
438
- // Brownfield (existing tokens found): seed from their concrete values.
439
- // Already exists: leave it untouched (idempotent — never clobbers edits).
440
- /** Relative path within the customer's project where sp tokens live. */
441
381
  export const SP_TOKENS_PATH = "src/styles/social-plus-tokens.css";
442
- /** Option-B neutral default — a clean, adaptive light-mode system using system
443
- * fonts. All token names use `--sp-` prefix to avoid collision with the
444
- * customer's own design system. Will be replaced with the official social.plus
445
- * palette (Option A) once that palette is finalised. */
446
382
  export const NEUTRAL_SP_TOKENS_DEFAULT = `/* social-plus-tokens.css — social.plus feature design system.
447
383
  * This file controls the look of all social.plus features in your app.
448
384
  * Edit freely. Run: vise design extract --from-project . to refresh the contract.
@@ -577,7 +513,6 @@ export const designInitTokensTool = {
577
513
  export async function initSpTokens(repoPath, force = false) {
578
514
  const root = path.resolve(repoPath);
579
515
  const target = path.join(root, SP_TOKENS_PATH);
580
- // Idempotent: don't overwrite unless forced.
581
516
  try {
582
517
  await stat(target);
583
518
  if (!force) {
@@ -588,14 +523,12 @@ export async function initSpTokens(repoPath, force = false) {
588
523
  };
589
524
  }
590
525
  }
591
- catch { /* file doesn't exist — proceed to scaffold */ }
592
- // Try brownfield seeding: extract concrete token values from the existing project.
526
+ catch { }
593
527
  const existingContract = await extractDesignContractFromProject(root);
594
528
  const hasBrownfieldTokens = existingContract.tokens.length > 0;
595
529
  let css;
596
530
  let seededFrom;
597
531
  if (hasBrownfieldTokens) {
598
- // Seed from their existing concrete values, namespaced as --sp-*.
599
532
  const lines = [
600
533
  `/* social-plus-tokens.css — social.plus feature design system.`,
601
534
  ` * Seeded from your existing design tokens on ${new Date().toISOString().slice(0, 10)}.`,
@@ -619,7 +552,6 @@ export async function initSpTokens(repoPath, force = false) {
619
552
  seededFrom = existingContract.source.inputs;
620
553
  }
621
554
  else {
622
- // Greenfield: neutral Option-B defaults.
623
555
  css = NEUTRAL_SP_TOKENS_DEFAULT;
624
556
  }
625
557
  await mkdir(path.dirname(target), { recursive: true });
@@ -633,16 +565,6 @@ export async function initSpTokens(repoPath, force = false) {
633
565
  : `Scaffolded ${SP_TOKENS_PATH} with neutral defaults. Fill in your colors, then run vise design extract --from-project .`,
634
566
  };
635
567
  }
636
- // ---------------------------------------------------------------------------
637
- // Design-system reference document (human/VLM-readable, advisory)
638
- // ---------------------------------------------------------------------------
639
- //
640
- // Pairs with design-contract.json (machine-readable): this is the "v1.0" visual
641
- // spec a human or VLM can read, share, or diff. It reads the source CSS so that
642
- // var(--x) resolves live in the browser; for non-CSS projects (Android, Flutter,
643
- // iOS, TS module) it falls back to rendering directly from the contract tokens.
644
- //
645
- // NOT a gate — advisory documentation only.
646
568
  export const designReferenceTool = {
647
569
  name: "design_reference",
648
570
  description: "Generate a self-contained HTML design-system reference (token swatches, type samples, component demos, growth-layer summary) from the Vise design contract. Human/VLM-readable; advisory, not an enforcement gate.",
@@ -685,11 +607,6 @@ export const designReferenceTool = {
685
607
  });
686
608
  },
687
609
  };
688
- /**
689
- * Generate a self-contained HTML design-system reference from the contract.
690
- * Reads source CSS files for full var() resolution; falls back to contract tokens
691
- * for non-CSS projects (Android XML, Flutter Dart, iOS, TS module sources).
692
- */
693
610
  export async function generateDesignReference(repoPath, contract, title) {
694
611
  const root = path.resolve(repoPath);
695
612
  const cssTexts = await Promise.all((contract.source?.inputs ?? []).map(async (rel) => {
@@ -704,10 +621,6 @@ export async function generateDesignReference(repoPath, contract, title) {
704
621
  }
705
622
  }));
706
623
  const tokenCss = cssTexts.join("\n");
707
- // `ref` = how this token is referenced in a CSS style attribute.
708
- // CSS projects: `var(--name)` so the token resolves live via the inlined :root.
709
- // Non-CSS projects (Android/Flutter/iOS/TS module): use the concrete value directly
710
- // since there is no :root to resolve from.
711
624
  const hasCssInputs = (contract.source?.inputs ?? []).some((rel) => /\.(css|scss)$/i.test(rel));
712
625
  let allTokens;
713
626
  if (hasCssInputs && tokenCss.trim()) {
@@ -720,7 +633,6 @@ export async function generateDesignReference(repoPath, contract, title) {
720
633
  }));
721
634
  }
722
635
  else {
723
- // Non-CSS project: render from contract tokens directly, using concrete values.
724
636
  allTokens = contract.tokens
725
637
  .filter((t) => t.name !== null)
726
638
  .map((t) => ({ name: t.name, value: t.value, ref: safeCss(t.value), inContract: true, category: t.category }));
@@ -744,8 +656,6 @@ export async function generateDesignReference(repoPath, contract, title) {
744
656
  { id: "bp", label: "Breakpoints", kind: "chip", match: (n) => n.startsWith("--bp-") },
745
657
  { id: "z", label: "Z-index", kind: "chip", match: (n) => n.startsWith("--z-") },
746
658
  ];
747
- // Maps contract category → rendering kind; used as fallback for non-CSS tokens
748
- // whose names don't carry the --prefix conventions above.
749
659
  const CATEGORY_TO_KIND = {
750
660
  color: "color", space: "space", radius: "radius", shadow: "shadow",
751
661
  fontFamily: "family", fontSize: "fontsize", motion: "chip", opacity: "opacity",
@@ -760,8 +670,6 @@ export async function generateDesignReference(repoPath, contract, title) {
760
670
  items.forEach((t) => used.add(t.name));
761
671
  return { id: g.id, label: g.label, kind: g.kind, items };
762
672
  }).filter((g) => g.items.length > 0);
763
- // For non-CSS tokens not matched by name-prefix above, fall back to grouping by
764
- // the contract category field so native projects produce a properly-sectioned doc.
765
673
  const unmatchedByName = allTokens.filter((t) => !used.has(t.name));
766
674
  const catBuckets = new Map();
767
675
  const trueUngrouped = [];
@@ -804,14 +712,12 @@ export async function generateDesignReference(repoPath, contract, title) {
804
712
  function renderTokenRow(t) {
805
713
  return `<div class="ds-token-row"><code class="ds-token-name">${esc(t.name)}${provTag(t)}</code><code class="ds-token-pill">${esc(t.value)}</code></div>`;
806
714
  }
807
- // Super-group assignment for nav + section tags.
808
715
  const GROUP_SUPER = {
809
716
  brand: "COLOR", bg: "COLOR", text: "COLOR", line: "COLOR",
810
717
  font: "TYPOGRAPHY", fs: "TYPOGRAPHY", fw: "TYPOGRAPHY", lh: "TYPOGRAPHY",
811
718
  space: "LAYOUT", size: "LAYOUT", bp: "LAYOUT",
812
719
  radius: "SURFACE", border: "SURFACE", shadow: "SURFACE",
813
720
  opacity: "EFFECTS", motion: "EFFECTS", z: "EFFECTS",
814
- // category-based (native projects)
815
721
  color: "COLOR", fontFamily: "TYPOGRAPHY", fontSize: "TYPOGRAPHY",
816
722
  other: "OTHER",
817
723
  };
@@ -1180,8 +1086,6 @@ export async function runDesignCheck(repoPath) {
1180
1086
  note: ADVISORY_NOTE,
1181
1087
  };
1182
1088
  }
1183
- // Freshness check: compare source file content hashes to those recorded at extract time.
1184
- // Advisory only — never blocks, just surfaces a nudge to re-extract.
1185
1089
  const staleContract = await checkContractFreshness(repoRoot, contract);
1186
1090
  const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => SCAN_EXTS.has(path.extname(file).toLowerCase()));
1187
1091
  if (files.length === 0) {
@@ -1211,7 +1115,6 @@ export async function runDesignCheck(repoPath) {
1211
1115
  scanned += 1;
1212
1116
  const rel = path.relative(repoRoot, file);
1213
1117
  const isCss = file.toLowerCase().endsWith(".css") || file.toLowerCase().endsWith(".scss");
1214
- // Token coverage: a declared token is "referenced" if its var name OR its value appears in the code.
1215
1118
  for (const token of declaredTokens) {
1216
1119
  const key = tokenKey(token);
1217
1120
  if (referenced.has(key)) {
@@ -1221,7 +1124,6 @@ export async function runDesignCheck(repoPath) {
1221
1124
  referenced.add(key);
1222
1125
  }
1223
1126
  }
1224
- // Raw color literals: counted, then classified on/off contract.
1225
1127
  for (const value of scanColorLiterals(content)) {
1226
1128
  totalColors += 1;
1227
1129
  if (contractColorValues.has(value)) {
@@ -1231,7 +1133,6 @@ export async function runDesignCheck(repoPath) {
1231
1133
  colorSample.push({ value, file: rel, on_contract: false });
1232
1134
  }
1233
1135
  }
1234
- // Token hygiene: collect var(--x) references (any file) and --x: definitions (CSS files).
1235
1136
  for (const name of scanVarReferences(content)) {
1236
1137
  varRefs.push({ token: name, file: rel });
1237
1138
  }
@@ -1243,11 +1144,6 @@ export async function runDesignCheck(repoPath) {
1243
1144
  }
1244
1145
  const referencedTokens = declaredTokens.filter((token) => referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1245
1146
  const unreferencedTokens = declaredTokens.filter((token) => !referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1246
- // A var(--x) referenced but defined in no scanned CSS file AND not a known
1247
- // contract token resolves to nothing at runtime — a typo or hallucinated
1248
- // token. Contract token names are excluded: they are legitimate design tokens
1249
- // that may be supplied by an imported/external design-system stylesheet, so
1250
- // flagging them would risk a false positive (Vise's cardinal sin).
1251
1147
  const contractTokenNames = new Set(contract.tokens.map((token) => token.name).filter((name) => Boolean(name)));
1252
1148
  const undefinedRefs = dedupeByToken(varRefs.filter((ref) => !definedVars.has(ref.token) && !contractTokenNames.has(ref.token)));
1253
1149
  return {
@@ -1278,8 +1174,6 @@ export async function runDesignCheck(repoPath) {
1278
1174
  note: ADVISORY_NOTE,
1279
1175
  };
1280
1176
  }
1281
- /** Compare source.inputs file content against hashes recorded at extract time.
1282
- * Returns null if the contract is fresh or has no recorded digests. */
1283
1177
  async function checkContractFreshness(repoRoot, contract) {
1284
1178
  const recorded = contract.source?.input_digests;
1285
1179
  if (!recorded || Object.keys(recorded).length === 0)
@@ -1293,7 +1187,7 @@ async function checkContractFreshness(repoRoot, contract) {
1293
1187
  changed.push(rel);
1294
1188
  }
1295
1189
  catch {
1296
- changed.push(rel); // file deleted or unreadable — also stale
1190
+ changed.push(rel);
1297
1191
  }
1298
1192
  }
1299
1193
  if (changed.length === 0)
@@ -1325,7 +1219,6 @@ function scanVarReferences(content) {
1325
1219
  }
1326
1220
  function scanVarDefinitions(content) {
1327
1221
  const out = [];
1328
- // Match `--x:` declarations, but not `var(--x)` references (no colon after the name there).
1329
1222
  const pattern = /(--[\w-]+)\s*:/g;
1330
1223
  let match;
1331
1224
  while ((match = pattern.exec(content)) !== null) {
@@ -1344,26 +1237,21 @@ function contractSummary(contract) {
1344
1237
  function tokenKey(token) {
1345
1238
  return `${token.category}::${token.name ?? token.value}`;
1346
1239
  }
1347
- /** Extract comparable hex color values from a source file: web `#hex`, Flutter/Android `Color(0xAARRGGBB)`. */
1348
1240
  function scanColorLiterals(content) {
1349
1241
  const out = [];
1350
1242
  let match;
1351
- // Web/Android/XML hex (`#RRGGBB`, incl. `<color>#RRGGBB</color>`).
1352
1243
  const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
1353
1244
  while ((match = hexPattern.exec(content)) !== null) {
1354
1245
  out.push(normalizeHex(match[0]));
1355
1246
  }
1356
- // Flutter/Android ARGB `0xAARRGGBB` -> #rrggbb (drop alpha).
1357
1247
  const argbPattern = /0x([0-9a-fA-F]{8})\b/g;
1358
1248
  while ((match = argbPattern.exec(content)) !== null) {
1359
1249
  out.push(normalizeHex(`#${match[1].slice(2)}`));
1360
1250
  }
1361
- // iOS Swift `Color(hex: "RRGGBB")`.
1362
1251
  const swiftHex = /Color\(\s*hex:\s*"#?([0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?)"/g;
1363
1252
  while ((match = swiftHex.exec(content)) !== null) {
1364
1253
  out.push(normalizeHex(`#${match[1].slice(0, 6)}`));
1365
1254
  }
1366
- // iOS Swift `Color(red: r, green: g, blue: b)` (floats or n/255).
1367
1255
  const swiftRgb = /(?:UI)?Color\(\s*red:\s*([\d./]+)\s*,\s*green:\s*([\d./]+)\s*,\s*blue:\s*([\d./]+)/g;
1368
1256
  while ((match = swiftRgb.exec(content)) !== null) {
1369
1257
  const r = parseColorComponent(match[1]);
@@ -1443,26 +1331,14 @@ async function collectFiles(root, max = MAX_PROTOTYPE_FILES) {
1443
1331
  }
1444
1332
  return out;
1445
1333
  }
1446
- // ---------------------------------------------------------------------------
1447
- // Host-project design-file discovery
1448
- // ---------------------------------------------------------------------------
1449
1334
  const DESIGN_FILE_SKIP = new Set([
1450
1335
  ...SKIP_DIRS,
1451
1336
  "test", "tests", "__tests__", "__mocks__", "example", "examples", "sample", "samples",
1452
1337
  "pods", ".dart_tool", "coverage", "fastlane", "generated", "gen", "snapshots",
1453
- // Platform-runner dirs (Flutter/React Native) — design lives in lib/ or src/,
1454
- // not these. Skipping them keeps the walk from exhausting its budget before
1455
- // reaching the real design files in large native repos.
1456
1338
  "macos", "windows", "linux", "gradle",
1457
1339
  ]);
1458
1340
  const MAX_SCAN_DIRS = 60000;
1459
1341
  const MAX_DESIGN_FILES = 60;
1460
- /**
1461
- * Recursively find a project's design-source files by HIGH-SIGNAL name (not every
1462
- * stylesheet — that would inflate noise). Real apps put these at non-standard
1463
- * paths (e.g. `lib/v4/core/theme.dart`, `common/src/main/res/values/colors.xml`),
1464
- * so a fixed root-relative candidate list misses them.
1465
- */
1466
1342
  async function findProjectDesignFiles(root) {
1467
1343
  const out = [];
1468
1344
  const stack = [root];
@@ -1508,10 +1384,10 @@ async function isDesignFile(full) {
1508
1384
  return cssFileDeclaresDesignTokens(full);
1509
1385
  }
1510
1386
  if (ext === ".json") {
1511
- return /\.colorset\//.test(lower) && base === "contents.json"; // iOS asset-catalog color
1387
+ return /\.colorset\//.test(lower) && base === "contents.json";
1512
1388
  }
1513
1389
  if (ext === ".swift") {
1514
- return /(theme|color|palette|style|appearance|design)/.test(base); // iOS design-named Swift
1390
+ return /(theme|color|palette|style|appearance|design)/.test(base);
1515
1391
  }
1516
1392
  if (MODULE_EXTS.has(ext)) {
1517
1393
  if (/^tailwind\.config\.(js|ts|cjs|mjs)$/.test(base)) {
@@ -1537,18 +1413,13 @@ async function cssFileDeclaresDesignTokens(full) {
1537
1413
  return false;
1538
1414
  }
1539
1415
  }
1540
- // ---------------------------------------------------------------------------
1541
- // Contract construction
1542
- // ---------------------------------------------------------------------------
1543
1416
  export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = []) {
1544
- // CSS bodies come from .css files plus <style> blocks inside HTML.
1545
1417
  const cssText = [...sources.css, ...sources.html.flatMap(extractStyleBlocks)].join("\n");
1546
1418
  const strippedCss = stripCssComments(cssText);
1547
1419
  const declarations = parseDeclarations(strippedCss);
1548
1420
  const inlineDeclarations = sources.html.flatMap(extractInlineStyles);
1549
1421
  const allDeclarations = [...declarations, ...inlineDeclarations];
1550
1422
  const varReferenceCounts = countVarReferences(strippedCss);
1551
- // --- Declared (exact) tokens: every CSS custom property. ---
1552
1423
  const declaredTokens = [];
1553
1424
  const declaredValues = new Set();
1554
1425
  for (const decl of declarations) {
@@ -1572,13 +1443,9 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1572
1443
  uses: varReferenceCounts.get(decl.prop) ?? 0,
1573
1444
  });
1574
1445
  }
1575
- // Declared tokens from non-CSS sources (TS/JS token modules, tailwind config)
1576
- // are exact too. Record their values so inferred clustering won't duplicate them.
1577
1446
  for (const token of extraDeclaredTokens) {
1578
1447
  declaredValues.add(token.value);
1579
1448
  }
1580
- // Collapse duplicate declarations (CSS custom properties + module tokens),
1581
- // keying on category+name, keeping the highest use count.
1582
1449
  const declaredByName = new Map();
1583
1450
  for (const token of [...declaredTokens, ...extraDeclaredTokens]) {
1584
1451
  const key = `${token.category}::${token.name}`;
@@ -1587,11 +1454,10 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1587
1454
  declaredByName.set(key, token);
1588
1455
  }
1589
1456
  }
1590
- // --- Inferred tokens: literal values observed >= INFERRED_MIN_USES times. ---
1591
1457
  const observations = new Map();
1592
1458
  for (const decl of allDeclarations) {
1593
1459
  if (decl.prop.startsWith("--")) {
1594
- continue; // declared tokens are exact, handled above
1460
+ continue;
1595
1461
  }
1596
1462
  for (const observed of observeLiterals(decl.prop, decl.value)) {
1597
1463
  const key = `${observed.category}::${observed.value}`;
@@ -1607,10 +1473,10 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1607
1473
  const inferredTokens = [];
1608
1474
  for (const obs of observations.values()) {
1609
1475
  if (obs.uses < INFERRED_MIN_USES) {
1610
- continue; // one-off literal, not a token
1476
+ continue;
1611
1477
  }
1612
1478
  if (declaredValues.has(obs.value)) {
1613
- continue; // already represented as a declared token
1479
+ continue;
1614
1480
  }
1615
1481
  inferredTokens.push({ category: obs.category, name: null, value: obs.value, provenance: "inferred", uses: obs.uses });
1616
1482
  }
@@ -1650,7 +1516,6 @@ function gradeStrength(declared, inferred) {
1650
1516
  export function stripCssComments(css) {
1651
1517
  return css.replace(/\/\*[\s\S]*?\*\//g, " ");
1652
1518
  }
1653
- /** Parse declarations inside the innermost `{ ... }` rule bodies. */
1654
1519
  export function parseDeclarations(css) {
1655
1520
  const out = [];
1656
1521
  const bodyPattern = /\{([^{}]*)\}/g;
@@ -1722,9 +1587,6 @@ export function parseBreakpoints(css) {
1722
1587
  }
1723
1588
  return [...seen.values()].sort((a, b) => a.px - b.px || a.edge.localeCompare(b.edge));
1724
1589
  }
1725
- // ---------------------------------------------------------------------------
1726
- // Value classification
1727
- // ---------------------------------------------------------------------------
1728
1590
  const COLOR_FUNCTION = /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\(/i;
1729
1591
  const HEX_COLOR = /^#[0-9a-f]{3,8}$/i;
1730
1592
  const LENGTH = /^-?\d*\.?\d+(px|rem|em|vh|vw|%)$/i;
@@ -1748,8 +1610,6 @@ function categorizeDeclaredVar(name, value) {
1748
1610
  if (/radius|radii|corner|\bround/.test(n)) {
1749
1611
  return "radius";
1750
1612
  }
1751
- // borderWidth: name-and-value gated, MUST come before the color branch because
1752
- // the color regex matches /border/ — `--border-width-thin: 1px` is a length, not a color.
1753
1613
  if (/border-?width|stroke-?width|outline-?width/.test(n) && LENGTH.test(v)) {
1754
1614
  return "borderWidth";
1755
1615
  }
@@ -1759,46 +1619,30 @@ function categorizeDeclaredVar(name, value) {
1759
1619
  if (/(font-family|fontfamily|typeface|family)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v)) {
1760
1620
  return "fontFamily";
1761
1621
  }
1762
- // fontSize: broaden to include the common --fs-* naming convention (e.g. --fs-sm: 14px).
1763
- // 'leading' and 'line-height' are moved to lineHeight below.
1764
1622
  if (/(font-size|fontsize|text-size)/.test(n) || /\btext-(xs|sm|base|md|lg|xl|\dxl|\d)\b/.test(n) || /^--fs-\w/.test(n)) {
1765
- return "fontSize"; // incl. the Tailwind text-scale convention (--text-sm/base/lg) and --fs-* shorthand
1623
+ return "fontSize";
1766
1624
  }
1767
- // Motion is value-gated: only time/easing values become motion tokens. This
1768
- // stops substring matches like "increase"/"decrease" (which contain "ease")
1769
- // from turning a px length into a nonsensical motion token.
1770
1625
  if (TIME.test(v) || /cubic-bezier|steps\(/i.test(v)) {
1771
1626
  return "motion";
1772
1627
  }
1773
1628
  if (/(duration|easing|transition|motion|animation|\bease)/.test(n) && !LENGTH.test(v) && !isColor(v)) {
1774
1629
  return "motion";
1775
1630
  }
1776
- // Generic font/type hint, but only for an actual family value (not a length —
1777
- // e.g. "prototype" contains "type" but `--prototype-flag: 10px` is not a font).
1778
1631
  if (/(font|type)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v) && !TIME.test(v)) {
1779
1632
  return "fontFamily";
1780
1633
  }
1781
- // fontWeight: name-gated (bare integers collide with z-index). Value must be a
1782
- // 3-digit integer in the valid CSS font-weight range 100–950.
1783
1634
  if (/\bfw\b|font-?weight|\bweight\b/.test(n)) {
1784
1635
  const num = Number(v);
1785
1636
  if (/^\d{3}$/.test(v) && num >= 100 && num <= 950) {
1786
1637
  return "fontWeight";
1787
1638
  }
1788
1639
  }
1789
- // lineHeight: name-gated + unitless value (1.1, 1.4, 1.6 etc.). Captures both
1790
- // 'lh-*' shorthand and 'leading-*'/'line-height-*' naming conventions.
1791
1640
  if (/\blh\b|leading|line-?height|lineheight/.test(n) && /^-?[0-9]*\.?[0-9]+$/.test(v) && !LENGTH.test(v)) {
1792
1641
  return "lineHeight";
1793
1642
  }
1794
- // letterSpacing: name-gated + any CSS length. Must come before the generic
1795
- // LENGTH→space fallback so --ls-* tokens aren't misfiled as spacing.
1796
1643
  if (/\bls\b|letter-?spacing|letterspacing|\btracking\b/.test(n) && LENGTH.test(v)) {
1797
1644
  return "letterSpacing";
1798
1645
  }
1799
- // breakpoint: name-gated + length. Must precede the LENGTH fallback. The
1800
- // contract already captures @media breakpoints in contract.breakpoints[]; this
1801
- // catches explicit --bp-* / --breakpoint-* custom-property tokens.
1802
1646
  if (/\bbp\b|break-?point|viewport-?width|screen-?size/.test(n) && LENGTH.test(v)) {
1803
1647
  return "breakpoint";
1804
1648
  }
@@ -1811,29 +1655,22 @@ function categorizeDeclaredVar(name, value) {
1811
1655
  if (LENGTH.test(v)) {
1812
1656
  return "space";
1813
1657
  }
1814
- // Opacity: declared-only (a bare 0.4 in code could be line-height/scale/anything — never infer).
1815
1658
  if (/opacity/.test(n) && /^(0(\.\d+)?|\.\d+|1(\.0*)?)$/.test(v)) {
1816
1659
  return "opacity";
1817
1660
  }
1818
- // zIndex: name-gated strictly; integers collide with fontWeight so the name is
1819
- // the only reliable signal. Use z-index / z-idx or the common --z-* prefix.
1820
- // Note: bare `/z/` would catch --zoom, --size, etc. — do NOT relax this guard.
1821
1661
  if ((/z-?index|z-?idx/.test(n) || /^--z-\w/.test(n)) && /^-?\d+$/.test(v)) {
1822
1662
  return "zIndex";
1823
1663
  }
1824
1664
  return null;
1825
1665
  }
1826
- /** Emit zero or more (category, normalizedValue) observations from one literal declaration. */
1827
1666
  function observeLiterals(prop, value) {
1828
1667
  const v = value.trim();
1829
1668
  if (!v || v.startsWith("var(") || v === "inherit" || v === "initial" || v === "unset" || v === "none" || v === "auto") {
1830
1669
  return [];
1831
1670
  }
1832
- // Shadows: keep the whole declaration as one token; don't dissect inner colors/lengths.
1833
1671
  if (/shadow/.test(prop)) {
1834
1672
  return [{ category: "shadow", value: collapseSpaces(v) }];
1835
1673
  }
1836
- // Motion: durations/easing.
1837
1674
  if (/^(transition|animation)(-|$)/.test(prop) || TIME.test(v)) {
1838
1675
  if (TIME.test(v)) {
1839
1676
  const time = v.match(/\b\d*\.?\d+m?s\b/i);
@@ -1843,20 +1680,16 @@ function observeLiterals(prop, value) {
1843
1680
  }
1844
1681
  return [];
1845
1682
  }
1846
- // Font family.
1847
1683
  if (prop === "font-family") {
1848
1684
  return [{ category: "fontFamily", value: collapseSpaces(v) }];
1849
1685
  }
1850
- // Font size.
1851
1686
  if (prop === "font-size" && LENGTH.test(v)) {
1852
1687
  return [{ category: "fontSize", value: v.toLowerCase() }];
1853
1688
  }
1854
1689
  const out = [];
1855
- // Colors anywhere in the value.
1856
1690
  for (const color of extractColorLiterals(v)) {
1857
1691
  out.push({ category: "color", value: color });
1858
1692
  }
1859
- // Lengths — only meaningful as spacing/radius tokens on the relevant props.
1860
1693
  if (/radius/.test(prop)) {
1861
1694
  for (const len of extractLengths(v)) {
1862
1695
  out.push({ category: "radius", value: len });
@@ -1889,7 +1722,7 @@ function extractLengths(value) {
1889
1722
  while ((match = pattern.exec(value)) !== null) {
1890
1723
  const token = match[0].toLowerCase();
1891
1724
  if (token.startsWith("0") && (token === "0px" || token === "0rem" || token === "0em")) {
1892
- continue; // zero is not a spacing token
1725
+ continue;
1893
1726
  }
1894
1727
  out.push(token);
1895
1728
  }
@@ -1915,7 +1748,6 @@ function normalizeValue(category, value) {
1915
1748
  function normalizeHex(hex) {
1916
1749
  let h = hex.toLowerCase();
1917
1750
  if (h.length === 4) {
1918
- // #abc -> #aabbcc
1919
1751
  h = `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
1920
1752
  }
1921
1753
  else if (h.length === 5) {
@@ -1926,9 +1758,6 @@ function normalizeHex(hex) {
1926
1758
  function collapseSpaces(value) {
1927
1759
  return value.replace(/\s+/g, " ").trim();
1928
1760
  }
1929
- // ---------------------------------------------------------------------------
1930
- // Component inference (advisory only)
1931
- // ---------------------------------------------------------------------------
1932
1761
  const COMPONENT_NAME_HINTS = /(card|btn|button|avatar|badge|chip|header|footer|nav|navbar|modal|dialog|sheet|list|item|row|tile|tag|pill|toolbar|toast|banner|input|field|composer|message|bubble|post|feed|comment|reaction)/;
1933
1762
  const COMPONENT_MIN_USES = 3;
1934
1763
  function inferComponents(htmlSources, css) {
@@ -1992,9 +1821,6 @@ function friendlyComponentName(cls) {
1992
1821
  const hint = lower.match(COMPONENT_NAME_HINTS);
1993
1822
  return hint ? hint[0] : cls;
1994
1823
  }
1995
- // ---------------------------------------------------------------------------
1996
- // Sorting, summarizing, digest
1997
- // ---------------------------------------------------------------------------
1998
1824
  const CATEGORY_ORDER = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "borderWidth", "breakpoint", "motion", "opacity", "zIndex"];
1999
1825
  function sortTokens(tokens) {
2000
1826
  return [...tokens].sort((a, b) => {
@@ -2016,7 +1842,6 @@ function summarizeTokens(tokens) {
2016
1842
  }
2017
1843
  return summary;
2018
1844
  }
2019
- /** Digest over design facts only — excludes timestamps, version, and input paths so the same prototype always yields the same digest. */
2020
1845
  export function digestContract(contract) {
2021
1846
  const facts = {
2022
1847
  kind: contract.source.kind,
@@ -2026,17 +1851,8 @@ export function digestContract(contract) {
2026
1851
  };
2027
1852
  return `sha256:${createHash("sha256").update(stableStringify(facts)).digest("hex")}`;
2028
1853
  }
2029
- // ---------------------------------------------------------------------------
2030
- // Token-module / tailwind-config extraction (TS/JS object literals)
2031
- // ---------------------------------------------------------------------------
2032
- const REFERENCE_VALUE = /var\(|theme\(|calc\(|env\(/i; // references / computed — not concrete tokens
1854
+ const REFERENCE_VALUE = /var\(|theme\(|calc\(|env\(/i;
2033
1855
  const MODULE_LENGTH = /^-?\d*\.?\d+(px|rem|em|vh|vw|%)$/i;
2034
- /**
2035
- * Extract concrete declared tokens from the object literals in a TS/JS token
2036
- * module or tailwind config. Only emits a token when the value is a concrete
2037
- * literal we can confidently categorize — references and un-placeable values are
2038
- * skipped (weaker, never wrong).
2039
- */
2040
1856
  export function extractTokensFromModule(source) {
2041
1857
  const tree = tryParse("tsx", source) ?? tryParse("typescript", source);
2042
1858
  if (!tree) {
@@ -2046,10 +1862,6 @@ export function extractTokensFromModule(source) {
2046
1862
  const stack = [tree.rootNode];
2047
1863
  while (stack.length > 0) {
2048
1864
  const node = stack.pop();
2049
- // Kick off collection at standalone object literals (an object that is a
2050
- // pair's value is already captured by the parent's recursion). Seed the key
2051
- // path with the declared variable name so `export const spacing = {...}`
2052
- // carries the "spacing" category hint down to its leaf values.
2053
1865
  if (node.type === "object" && node.parent?.type !== "pair") {
2054
1866
  collectObjectStringPairs(node, seedKeyPath(node), pairs);
2055
1867
  }
@@ -2111,9 +1923,6 @@ function collectObjectStringPairs(node, keyPath, out) {
2111
1923
  collectObjectStringPairs(valueNode, [...keyPath, key], out);
2112
1924
  }
2113
1925
  else {
2114
- // Arrays (e.g. Tailwind fontSize: ['14px', { lineHeight }]) — take the
2115
- // first string-literal element (the size); numeric/unitless arrays yield
2116
- // nothing.
2117
1926
  const valueText = valueNode.type === "array" ? firstArrayStringLiteral(valueNode) : moduleStringLiteral(valueNode);
2118
1927
  if (valueText !== undefined) {
2119
1928
  out.push({ keyPath: [...keyPath, key].join("."), value: valueText });
@@ -2145,17 +1954,12 @@ function moduleStringLiteral(node) {
2145
1954
  if (node.type === "string" || node.type === "template_string") {
2146
1955
  const text = node.text;
2147
1956
  if (text.includes("${")) {
2148
- return undefined; // interpolated — not a concrete literal
1957
+ return undefined;
2149
1958
  }
2150
1959
  return stripQuotes(text);
2151
1960
  }
2152
1961
  return undefined;
2153
1962
  }
2154
- /**
2155
- * Android resources: `<color name="x">#hex</color>` (concrete) and
2156
- * `<dimen name="x">16dp</dimen>`. `<item>@color/x</item>` references are
2157
- * ignored (they aren't concrete values).
2158
- */
2159
1963
  export function extractTokensFromAndroidXml(content) {
2160
1964
  const tokens = [];
2161
1965
  const seen = new Set();
@@ -2185,25 +1989,19 @@ export function extractTokensFromAndroidXml(content) {
2185
1989
  else if (/(text|font)/.test(key)) {
2186
1990
  push("fontSize", name, value);
2187
1991
  }
2188
- // unplaceable dimens are skipped (never guessed)
2189
1992
  }
2190
1993
  return tokens;
2191
1994
  }
2192
1995
  function normalizeAndroidHex(hex) {
2193
1996
  const h = hex.toLowerCase();
2194
1997
  if (h.length === 9) {
2195
- return `#${h.slice(3)}`; // #AARRGGBB -> #rrggbb (drop alpha)
1998
+ return `#${h.slice(3)}`;
2196
1999
  }
2197
2000
  if (h.length === 5) {
2198
- return normalizeHex(`#${h.slice(2)}`); // #ARGB -> #RGB -> expand
2001
+ return normalizeHex(`#${h.slice(2)}`);
2199
2002
  }
2200
2003
  return normalizeHex(h);
2201
2004
  }
2202
- /**
2203
- * Flutter color tokens: both `kPrimary = Color(0xAARRGGBB)` (const declarations)
2204
- * and `primaryColor: const Color(0xAARRGGBB)` (named-parameter theme construction,
2205
- * the common real pattern). Named `Colors.*` are not extractable.
2206
- */
2207
2005
  export function extractTokensFromDart(content) {
2208
2006
  const tokens = [];
2209
2007
  const seen = new Set();
@@ -2211,7 +2009,7 @@ export function extractTokensFromDart(content) {
2211
2009
  let match;
2212
2010
  while ((match = pattern.exec(content)) !== null) {
2213
2011
  const name = match[1];
2214
- const value = `#${match[2].slice(2).toLowerCase()}`; // 0xAARRGGBB -> #rrggbb
2012
+ const value = `#${match[2].slice(2).toLowerCase()}`;
2215
2013
  const key = `color::${name}::${value}`;
2216
2014
  if (!seen.has(key)) {
2217
2015
  seen.add(key);
@@ -2220,11 +2018,6 @@ export function extractTokensFromDart(content) {
2220
2018
  }
2221
2019
  return tokens;
2222
2020
  }
2223
- /**
2224
- * iOS asset catalog: `*.colorset/Contents.json` (sRGB/display-p3 components as
2225
- * 0–255 integers, 0–1 floats, or 0xNN hex). The colorset directory name is the
2226
- * token name. Dark-appearance variants are skipped in favor of the base color.
2227
- */
2228
2021
  export function extractTokensFromColorset(contentsJson, name) {
2229
2022
  let parsed;
2230
2023
  try {
@@ -2268,7 +2061,6 @@ function parseColorComponent(raw) {
2268
2061
  if (!Number.isFinite(num)) {
2269
2062
  return null;
2270
2063
  }
2271
- // A decimal point means a 0–1 float component; a bare integer is 0–255.
2272
2064
  return v.includes(".") ? clampByte(Math.round(num * 255)) : clampByte(Math.round(num));
2273
2065
  }
2274
2066
  function clampByte(n) {
@@ -2277,7 +2069,6 @@ function clampByte(n) {
2277
2069
  function toHex2(n) {
2278
2070
  return n.toString(16).padStart(2, "0");
2279
2071
  }
2280
- /** Swift color constants: `Color(hex: "RRGGBB")` and `Color(red: r, green: g, blue: b)` (floats or n/255). */
2281
2072
  export function extractTokensFromSwift(content) {
2282
2073
  const tokens = [];
2283
2074
  const seen = new Set();
@@ -2305,28 +2096,21 @@ export function extractTokensFromSwift(content) {
2305
2096
  }
2306
2097
  return tokens;
2307
2098
  }
2308
- /** Conservative, FP-safe categorization for a token-module/config value. */
2309
2099
  function categorizeTokenModuleValue(keyPath, value) {
2310
2100
  const v = value.trim();
2311
2101
  if (!v || REFERENCE_VALUE.test(v)) {
2312
- return null; // reference / computed — not a concrete token
2102
+ return null;
2313
2103
  }
2314
2104
  const key = keyPath.toLowerCase();
2315
- // These categories are CSS-only (declared custom properties): the name-gating
2316
- // that makes them safe against false positives only works in CSS. Module/native
2317
- // extraction skips them rather than risk inventing wrong tokens.
2318
2105
  if (/screen|breakpoint|media|z-?index|opacity|weight|\blh\b|leading|line-?height|letter-?spacing/.test(key)) {
2319
2106
  return null;
2320
2107
  }
2321
- // Colors: unambiguous from the value alone.
2322
2108
  if (isColor(v)) {
2323
2109
  return { category: "color", value: normalizeValue("color", v) };
2324
2110
  }
2325
- // Shadows: key-hinted, value carries length(s).
2326
2111
  if (/shadow|elevation/.test(key) && /\d/.test(v)) {
2327
2112
  return { category: "shadow", value: collapseSpaces(v) };
2328
2113
  }
2329
- // Lengths: only categorize when the key places them — otherwise skip (never guess).
2330
2114
  if (MODULE_LENGTH.test(v)) {
2331
2115
  if (/radius|radii|corner|\bround/.test(key)) {
2332
2116
  return { category: "radius", value: v.toLowerCase() };
@@ -2337,13 +2121,10 @@ function categorizeTokenModuleValue(keyPath, value) {
2337
2121
  if (/(font-?size|text-?size|leading|line-?height)/.test(key) ||
2338
2122
  /\btext-(xs|sm|base|md|lg|xl|\dxl|\d)\b/.test(key) ||
2339
2123
  (/(typography|font|text)/.test(key) && /(^|\.)sizes?(\.|$)/.test(key))) {
2340
- return { category: "fontSize", value: v.toLowerCase() }; // incl. text-scale + typography.size.* conventions
2124
+ return { category: "fontSize", value: v.toLowerCase() };
2341
2125
  }
2342
2126
  return null;
2343
2127
  }
2344
- // Motion: cubic-bezier/steps is unambiguously an easing token (value-based,
2345
- // like colors) — catches keys like "smooth"/"standard" that lack an ease/-
2346
- // duration hint.
2347
2128
  if (/cubic-bezier|steps\(/i.test(v)) {
2348
2129
  return { category: "motion", value: collapseSpaces(v) };
2349
2130
  }
@@ -2356,7 +2137,6 @@ function categorizeTokenModuleValue(keyPath, value) {
2356
2137
  if (/(ease|easing|bezier)/.test(key) && /cubic-bezier|ease/i.test(v)) {
2357
2138
  return { category: "motion", value: collapseSpaces(v) };
2358
2139
  }
2359
- // Font family: key-hinted, value is a family list.
2360
2140
  if (/(font-?family|fontfamily|typeface|family)/.test(key) && /[a-z]/i.test(v) && !MODULE_LENGTH.test(v)) {
2361
2141
  return { category: "fontFamily", value: collapseSpaces(v) };
2362
2142
  }
@@ -2374,26 +2154,13 @@ function stableStringify(value) {
2374
2154
  }
2375
2155
  return JSON.stringify(value);
2376
2156
  }
2377
- /**
2378
- * Structural grounding helper: builds a BriefLine and throws a TypeError at
2379
- * construction time if groundedIn is empty. This makes the grounding invariant
2380
- * structural — an ungrounded line is impossible rather than a runtime surprise.
2381
- */
2382
2157
  function line(text, groundedIn, confidence = "high") {
2383
2158
  if (groundedIn.length === 0) {
2384
2159
  throw new TypeError(`BriefLine created with empty groundedIn: "${text}"`);
2385
2160
  }
2386
2161
  return { text, groundedIn, confidence };
2387
2162
  }
2388
- /**
2389
- * Per-role keyword rules in spec-defined order.
2390
- * Each entry: [pattern, role, confidence].
2391
- * Rules are applied with first-match-wins semantics.
2392
- * Compound rules (muted+text, muted+bg/surface) must precede their plain
2393
- * counterparts so "--color-text-muted" resolves to textSecondary, not textPrimary.
2394
- */
2395
2163
  const ROLE_RULES = [
2396
- // Compound rules — must precede their plain counterparts
2397
2164
  {
2398
2165
  test: (n) => /muted/.test(n) && /\btext\b|foreground|fg/.test(n),
2399
2166
  role: "textSecondary",
@@ -2406,11 +2173,6 @@ const ROLE_RULES = [
2406
2173
  confidence: "medium",
2407
2174
  reason: "name contains 'muted' and 'bg'/'surface'/'background'",
2408
2175
  },
2409
- // Noun-first compounds — in real design systems the NOUN keyword (text/surface/
2410
- // bg/border) sets the role family and primary/secondary act as modifiers within
2411
- // it: "--text-primary" is the primary BODY-TEXT color, not the action color.
2412
- // These must precede the plain primary/secondary rules below, or first-match-wins
2413
- // would misbind some of the most common token names in the wild.
2414
2176
  {
2415
2177
  test: (n) => /\btext\b|foreground|\bfg\b/.test(n) && /\bprimary\b/.test(n),
2416
2178
  role: "textPrimary",
@@ -2441,12 +2203,6 @@ const ROLE_RULES = [
2441
2203
  confidence: "medium",
2442
2204
  reason: "name contains a border keyword with a primary/secondary modifier — the noun keyword sets the role family",
2443
2205
  },
2444
- // Primary: plain "primary" → high; "brand"/"accent" → medium
2445
- // EXCLUSION: action roles bind interactive-family names only.
2446
- // A token whose name contains a text-family noun (text/foreground/fg) MUST
2447
- // NEVER bind primaryAction or secondaryAction — it represents a text colour,
2448
- // not an interactive accent (e.g. --text-bright-accent is a text accent, while
2449
- // --essential-bright-accent is the real interactive accent).
2450
2206
  {
2451
2207
  test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
2452
2208
  role: "primaryAction",
@@ -2508,7 +2264,6 @@ const ROLE_RULES = [
2508
2264
  reason: "name contains 'avatar'",
2509
2265
  },
2510
2266
  ];
2511
- /** Infer a (role, confidence, reason) from a token name, or null if no rule matches. */
2512
2267
  function inferRole(tokenName) {
2513
2268
  const n = tokenName.toLowerCase();
2514
2269
  for (const rule of ROLE_RULES) {
@@ -2518,11 +2273,6 @@ function inferRole(tokenName) {
2518
2273
  }
2519
2274
  return null;
2520
2275
  }
2521
- /**
2522
- * Among multiple candidates for the same role, prefer:
2523
- * 1. high confidence over medium
2524
- * 2. shorter name (e.g. "--color-primary" beats "--color-primary-hover")
2525
- */
2526
2276
  function bestCandidate(a, b) {
2527
2277
  if (a.confidence === "high" && b.confidence !== "high")
2528
2278
  return a;
@@ -2530,22 +2280,8 @@ function bestCandidate(a, b) {
2530
2280
  return b;
2531
2281
  return a.token.length <= b.token.length ? a : b;
2532
2282
  }
2533
- /**
2534
- * Build a DesignBuildBrief from a DesignContract.
2535
- *
2536
- * - Pure: no I/O, no side effects.
2537
- * - Never persisted or digested.
2538
- * - Every BriefLine has non-empty groundedIn citing actual contract tokens/roles.
2539
- * - Roles inferred from NAME only (never from value).
2540
- * - Inferred tokens (name: null) are never cited by name; they may be cited only
2541
- * as a count for do/avoid prose, but only if the contract has declared color
2542
- * tokens with recognizable names to ground the line instead.
2543
- */
2544
2283
  export function buildDesignBrief(contract) {
2545
2284
  const strength = contract.stats.strength;
2546
- // ── Role inference ──────────────────────────────────────────────────────────
2547
- // Walk only declared color tokens (provenance=declared, category=color, name != null).
2548
- // Inferred tokens always have name: null; value-only matching is forbidden.
2549
2285
  const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
2550
2286
  const roleMap = new Map();
2551
2287
  for (const token of colorTokens) {
@@ -2564,26 +2300,12 @@ export function buildDesignBrief(contract) {
2564
2300
  roleMap.set(inferred.role, existing ? bestCandidate(existing, candidate) : candidate);
2565
2301
  }
2566
2302
  const roles = [...roleMap.values()];
2567
- // Helper: is a role name in this brief?
2568
2303
  const roleNames = new Set(roles.map((r) => r.role));
2569
- // String-typed version for use in contexts where we compare arbitrary strings against role names.
2570
2304
  const roleNameStrings = new Set(roles.map((r) => r.role));
2571
- // ── Component hints ─────────────────────────────────────────────────────────
2572
- // Reference ONLY tokens that actually exist in the contract.
2573
- /**
2574
- * Representative token selector: among a category's declared tokens, prefer
2575
- * (in order) a name containing "base", then "default", then "md"/"medium",
2576
- * then the SHORTEST name, then first-in-order.
2577
- *
2578
- * Using first-in-contract-order (old behaviour) selected unrepresentative tokens
2579
- * like --encore-corner-radius-smaller over --encore-corner-radius-base, causing
2580
- * agents to anchor on non-base variants and miss the authoritative base token.
2581
- */
2582
2305
  function representativeToken(cat) {
2583
2306
  const candidates = contract.tokens.filter((t) => t.category === cat && t.name !== null && t.provenance === "declared");
2584
2307
  if (candidates.length === 0)
2585
2308
  return undefined;
2586
- // Scoring: lower is better. 0 = base, 1 = default, 2 = md/medium, 3 = shortest, 4 = first.
2587
2309
  const score = (name) => {
2588
2310
  const n = name.toLowerCase();
2589
2311
  if (/\bbase\b/.test(n))
@@ -2601,18 +2323,14 @@ export function buildDesignBrief(contract) {
2601
2323
  return t;
2602
2324
  if (ts > bs)
2603
2325
  return best;
2604
- // Same bucket: prefer shorter name, then first-in-order (best wins ties).
2605
2326
  return t.name.length < best.name.length ? t : best;
2606
2327
  });
2607
2328
  }
2608
2329
  const radiusToken = representativeToken("radius");
2609
2330
  const spaceToken = representativeToken("space");
2610
- // Border colour: sourced from the inferred border role (a color token named *border*/*outline*/*divider*).
2611
- // There is no "border" TokenCategory — border colours live in the "color" category.
2612
2331
  const borderRoleColor = roleMap.get("border");
2613
2332
  const shadowToken = representativeToken("shadow");
2614
2333
  const primaryRole = roleMap.get("primaryAction");
2615
- // card hint
2616
2334
  const cardGuidanceLines = [];
2617
2335
  if (radiusToken) {
2618
2336
  cardGuidanceLines.push(line(`Use ${radiusToken.name} (${radiusToken.value}) for card corner radius.`, [radiusToken.name]));
@@ -2629,7 +2347,6 @@ export function buildDesignBrief(contract) {
2629
2347
  const cardHint = cardGuidanceLines.length > 0
2630
2348
  ? { kind: "card", guidance: cardGuidanceLines, confidence: radiusToken && spaceToken ? "high" : "medium" }
2631
2349
  : { kind: "card", absent: true, note: "No card pattern confidently identified — reuse the host app's existing card styles." };
2632
- // button hint
2633
2350
  const buttonGuidanceLines = [];
2634
2351
  if (primaryRole) {
2635
2352
  buttonGuidanceLines.push(line(`Use ${primaryRole.token} (${primaryRole.value}) as the primary button background.`, [primaryRole.role]));
@@ -2643,7 +2360,6 @@ export function buildDesignBrief(contract) {
2643
2360
  const buttonHint = buttonGuidanceLines.length > 0
2644
2361
  ? { kind: "button", guidance: buttonGuidanceLines, confidence: primaryRole ? "high" : "medium" }
2645
2362
  : { kind: "button", absent: true, note: "No button pattern confidently identified — reuse the host app's existing button styles." };
2646
- // input hint
2647
2363
  const inputGuidanceLines = [];
2648
2364
  if (borderRoleColor) {
2649
2365
  inputGuidanceLines.push(line(`Use ${borderRoleColor.token} for input border colour.`, [borderRoleColor.role]));
@@ -2658,60 +2374,38 @@ export function buildDesignBrief(contract) {
2658
2374
  ? { kind: "input", guidance: inputGuidanceLines, confidence: borderRoleColor ? "high" : "medium" }
2659
2375
  : { kind: "input", absent: true, note: "No input pattern confidently identified — reuse the host app's existing input styles." };
2660
2376
  const componentHints = [cardHint, buttonHint, inputHint];
2661
- // ── Do/Avoid lines ──────────────────────────────────────────────────────────
2662
- // Every line MUST be grounded in tokens/roles that actually exist in this brief.
2663
2377
  const doLines = [];
2664
2378
  const avoidLines = [];
2665
- // Only emit do/avoid lines that are grounded in actually-present tokens.
2666
2379
  const declaredColorTokens = colorTokens.filter((t) => t.provenance === "declared");
2667
2380
  const declaredSpaceTokens = contract.tokens.filter((t) => t.category === "space" && t.name !== null && t.provenance === "declared");
2668
2381
  const declaredRadiusTokens = contract.tokens.filter((t) => t.category === "radius" && t.name !== null && t.provenance === "declared");
2669
- // Do: use declared color tokens
2670
2382
  if (declaredColorTokens.length > 0) {
2671
2383
  const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
2672
2384
  doLines.push(line(`Reference declared color tokens (e.g. ${tokenNames.join(", ")}) — never introduce new hex literals.`, tokenNames));
2673
2385
  }
2674
- // Do: use declared space tokens
2675
2386
  if (declaredSpaceTokens.length > 0) {
2676
2387
  const tokenNames = declaredSpaceTokens.slice(0, 3).map((t) => t.name);
2677
2388
  doLines.push(line(`Reference declared spacing tokens (e.g. ${tokenNames.join(", ")}) for margins, padding, and gaps.`, tokenNames));
2678
2389
  }
2679
- // Do: use declared radius tokens
2680
2390
  if (declaredRadiusTokens.length > 0) {
2681
2391
  const tokenNames = declaredRadiusTokens.slice(0, 2).map((t) => t.name);
2682
2392
  doLines.push(line(`Use declared radius tokens (e.g. ${tokenNames.join(", ")}) for corner rounding.`, tokenNames));
2683
2393
  }
2684
- // Do: use primary-role token for interactive elements
2685
2394
  if (primaryRole) {
2686
2395
  doLines.push(line(`Use the primary colour token (${primaryRole.token}) for primary interactive elements (buttons, CTAs).`, [primaryRole.role]));
2687
2396
  }
2688
- // Avoid: hex literals (grounded in declared color tokens)
2689
2397
  if (declaredColorTokens.length > 0) {
2690
2398
  const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
2691
2399
  avoidLines.push(line(`Do not introduce new hex or colour literals — use the ${declaredColorTokens.length} declared colour token(s) (e.g. ${tokenNames.join(", ")}).`, tokenNames));
2692
2400
  }
2693
- // Avoid: raw spacing literals (grounded in declared space tokens)
2694
2401
  if (declaredSpaceTokens.length > 0) {
2695
2402
  const tokenNames = declaredSpaceTokens.slice(0, 2).map((t) => t.name);
2696
2403
  avoidLines.push(line(`Do not hardcode raw spacing values — use declared spacing tokens (e.g. ${tokenNames.join(", ")}).`, tokenNames));
2697
2404
  }
2698
- // Avoid: overriding the primary colour token on interactive elements
2699
2405
  if (primaryRole) {
2700
2406
  avoidLines.push(line(`Do not override the primary colour token (${primaryRole.token}) with ad-hoc colours on interactive elements.`, [primaryRole.role]));
2701
2407
  }
2702
- // ── Breadth instruction (Fix 3 — anti-anchoring) ─────────────────────────────
2703
- //
2704
- // The roles and hints above are a starting lens — a minimal named anchor set.
2705
- // Without an explicit instruction, agents anchor on the named tokens and stop
2706
- // reading the full token file. This breadth line counters that by directing the
2707
- // agent to the FULL declared token set, grounded in one representative token
2708
- // per category so the grounding itself spans the system's breadth.
2709
- //
2710
- // Grounding rule: up to one representative declared token per category present,
2711
- // using the representativeToken() selector (Fix 2). Only emitted when ≥1
2712
- // declared token exists — weak/empty contracts do NOT get an invented breadth line.
2713
2408
  const ALL_TOKEN_CATEGORIES = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "borderWidth", "breakpoint", "motion", "opacity", "zIndex"];
2714
- // Collect one representative declared token per category that has any declared tokens.
2715
2409
  const breadthGroundingTokens = [];
2716
2410
  for (const cat of ALL_TOKEN_CATEGORIES) {
2717
2411
  const rep = representativeToken(cat);
@@ -2720,11 +2414,9 @@ export function buildDesignBrief(contract) {
2720
2414
  }
2721
2415
  const totalDeclaredTokens = contract.tokens.filter((t) => t.provenance === "declared" && t.name !== null).length;
2722
2416
  if (breadthGroundingTokens.length > 0) {
2723
- // Build the grounding set (names of the representative tokens).
2724
2417
  const breadthGrounding = breadthGroundingTokens.map((t) => t.name);
2725
2418
  doLines.push(line(`The roles and hints above are a starting lens, not the full design system — reference the FULL declared token set (${totalDeclaredTokens} declared tokens) and prefer an existing token over any new value.`, breadthGrounding));
2726
2419
  }
2727
- // ── Review notes ─────────────────────────────────────────────────────────────
2728
2420
  const reviewNotes = [];
2729
2421
  if (strength === "weak") {
2730
2422
  reviewNotes.push("Contract is weak — very few named tokens were found. Guidance above is minimal. Run `vise design extract --from-project` to derive a richer contract from the host project's design system, or provide a prototype.");
@@ -2732,8 +2424,6 @@ export function buildDesignBrief(contract) {
2732
2424
  if (roles.length === 0) {
2733
2425
  reviewNotes.push("No colour roles could be inferred from token names. Role-based guidance is unavailable. Ensure tokens use recognisable names (e.g. --color-primary, --color-surface) and run `vise design extract --from-project` again.");
2734
2426
  }
2735
- // Suggest missing roles using name examples, not camelCase role identifiers
2736
- // (camelCase role names must not appear in prose to keep the weak/neutral brief JSON clean).
2737
2427
  if (!roleNames.has("primaryAction") && contract.stats.declared_tokens > 0) {
2738
2428
  reviewNotes.push("No primary action colour found — consider naming a token --color-primary (or --color-brand / --color-accent) for primary interactive elements.");
2739
2429
  }
@@ -2743,19 +2433,12 @@ export function buildDesignBrief(contract) {
2743
2433
  if (!roleNames.has("border") && contract.stats.declared_tokens > 0) {
2744
2434
  reviewNotes.push("No border colour found — consider naming a token --color-border or --color-outline.");
2745
2435
  }
2746
- // Conservative-inference reviewNote (Fix 3):
2747
- // When roles bind fewer than half the declared COLOR tokens, the role inference
2748
- // was conservative on this vocabulary (e.g. Encore's essential-/decorative-
2749
- // prefixed names are correct restraint but result in thin role coverage).
2750
- // Alert the agent that it MUST read the full token file rather than relying on roles alone.
2751
2436
  const declaredColorCount = declaredColorTokens.length;
2752
2437
  const colorTokensBoundToRoles = roles.filter((r) => declaredColorTokens.some((t) => t.name === r.token)).length;
2753
2438
  if (declaredColorCount > 0 && colorTokensBoundToRoles < declaredColorCount / 2) {
2754
2439
  reviewNotes.push(`Role inference was conservative on this vocabulary — only ${colorTokensBoundToRoles} of ${declaredColorCount} declared colour token(s) are bound to roles. The full token file must be read to discover the complete colour system.`);
2755
2440
  }
2756
- // ── Summary ──────────────────────────────────────────────────────────────────
2757
2441
  const tokenCount = contract.tokens.filter((t) => t.name !== null).length;
2758
- // Count how many distinct declared token names are referenced by roles + hints.
2759
2442
  const briefReferencedTokenNames = new Set();
2760
2443
  for (const r of roles)
2761
2444
  briefReferencedTokenNames.add(r.token);
@@ -2763,7 +2446,6 @@ export function buildDesignBrief(contract) {
2763
2446
  if (!("absent" in h)) {
2764
2447
  for (const l of h.guidance) {
2765
2448
  for (const g of l.groundedIn) {
2766
- // Only add entries that are actual declared token names (not role names).
2767
2449
  if (!roleNameStrings.has(g))
2768
2450
  briefReferencedTokenNames.add(g);
2769
2451
  }
@@ -2786,26 +2468,14 @@ export function buildDesignBrief(contract) {
2786
2468
  reviewNotes,
2787
2469
  };
2788
2470
  }
2789
- /**
2790
- * Build outcome-specific design recipe items grounded in an existing brief.
2791
- *
2792
- * HARD INVARIANT: every item is grounded ONLY in roles/tokens already in the brief.
2793
- * Items for absent roles are silently omitted. Returns `undefined` when zero items
2794
- * can be grounded (e.g. empty brief).
2795
- *
2796
- * Pure — no I/O, no side effects. Generated at plan time; never persisted.
2797
- */
2798
2471
  export function buildOutcomeDesignRecipe(brief, outcome) {
2799
2472
  const roleMap = new Map(brief.roles.map((r) => [r.role, r]));
2800
- // Collect groundedIn entries from a given component hint (absent hints contribute nothing).
2801
2473
  const hintGrounding = (kind) => {
2802
2474
  const hint = brief.componentHints.find((h) => h.kind === kind);
2803
2475
  if (!hint || "absent" in hint)
2804
2476
  return [];
2805
2477
  return hint.guidance.flatMap((l) => l.groundedIn);
2806
2478
  };
2807
- // Collect radius-specific grounding from the card hint by looking for guidance
2808
- // lines that mention "corner radius" — avoids mis-citing a space token as radius.
2809
2479
  const cardRadiusGrounding = () => {
2810
2480
  const hint = brief.componentHints.find((h) => h.kind === "card");
2811
2481
  if (!hint || "absent" in hint)
@@ -2816,30 +2486,24 @@ export function buildOutcomeDesignRecipe(brief, outcome) {
2816
2486
  };
2817
2487
  const items = [];
2818
2488
  if (outcome === "add-feed") {
2819
- // Composer / action button — only when primaryAction exists.
2820
2489
  const primaryAction = roleMap.get("primaryAction");
2821
2490
  if (primaryAction) {
2822
2491
  items.push(line(`The post composer action button uses the primary action colour token (${primaryAction.token}).`, ["primaryAction"]));
2823
2492
  }
2824
- // Post cards — only when the card hint has grounding tokens.
2825
2493
  const cardGrounding = hintGrounding("card");
2826
2494
  if (cardGrounding.length > 0) {
2827
2495
  items.push(line("Post cards follow the card component hint: apply the card hint tokens for corner radius, padding, and border/shadow.", cardGrounding));
2828
2496
  }
2829
- // Post metadata and timestamps.
2830
2497
  const textSecondary = roleMap.get("textSecondary");
2831
2498
  if (textSecondary) {
2832
2499
  items.push(line(`Post metadata and timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
2833
2500
  }
2834
- // Report / delete affordances.
2835
2501
  const danger = roleMap.get("danger");
2836
2502
  if (danger) {
2837
2503
  items.push(line(`Report and delete affordances use the danger colour token (${danger.token}).`, ["danger"]));
2838
2504
  }
2839
2505
  }
2840
2506
  else {
2841
- // add-chat
2842
- // Message bubbles use surface.
2843
2507
  const surface = roleMap.get("surface");
2844
2508
  if (surface) {
2845
2509
  const radiusGrounding = cardRadiusGrounding();
@@ -2850,33 +2514,23 @@ export function buildOutcomeDesignRecipe(brief, outcome) {
2850
2514
  items.push(line(`Message bubbles use the surface colour token (${surface.token}).`, ["surface"]));
2851
2515
  }
2852
2516
  }
2853
- // Own-message vs other-message contrast — ONLY when BOTH primaryAction AND surface exist.
2854
2517
  const primaryAction = roleMap.get("primaryAction");
2855
2518
  if (primaryAction && surface) {
2856
2519
  items.push(line(`Own messages use the primary action colour (${primaryAction.token}) as background; other messages use the surface colour (${surface.token}).`, ["primaryAction", "surface"]));
2857
2520
  }
2858
- // Timestamps.
2859
2521
  const textSecondary = roleMap.get("textSecondary");
2860
2522
  if (textSecondary) {
2861
2523
  items.push(line(`Message timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
2862
2524
  }
2863
- // Composer follows the input hint tokens.
2864
2525
  const inputGrounding = hintGrounding("input");
2865
2526
  if (inputGrounding.length > 0) {
2866
2527
  items.push(line("The message composer follows the input component hint: apply the input hint tokens for border colour, corner radius, and padding.", inputGrounding));
2867
2528
  }
2868
- // Moderation actions.
2869
2529
  const danger = roleMap.get("danger");
2870
2530
  if (danger) {
2871
2531
  items.push(line(`Moderation actions (report, block, mute) use the danger colour token (${danger.token}).`, ["danger"]));
2872
2532
  }
2873
2533
  }
2874
- // Breadth audit item (Fix 3 — anti-anchoring):
2875
- // Append a final item instructing the agent to audit against the FULL token set.
2876
- // Grounded the same way as the brief's breadth do-line: reuse the groundedIn of
2877
- // the "starting lens" do-line if it exists in the brief (spans the breadth of
2878
- // the system's categories). Only emitted when the brief has declared tokens
2879
- // (the breadth do-line only exists when there are declared tokens).
2880
2534
  const breadthDoLine = brief.do.find((l) => l.text.includes("starting lens"));
2881
2535
  if (breadthDoLine) {
2882
2536
  items.push(line("Before finishing, audit the UI against the full declared token set and replace any near-miss values with the matching token.", breadthDoLine.groundedIn));