@amityco/social-plus-vise 1.2.0 → 1.4.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 (47) hide show
  1. package/CHANGELOG.md +75 -2
  2. package/README.md +23 -10
  3. package/dist/capabilities.js +35 -4
  4. package/dist/entryState.js +71 -0
  5. package/dist/experience.js +70 -0
  6. package/dist/explore.js +51 -0
  7. package/dist/flow.js +382 -0
  8. package/dist/humanFormat.js +251 -0
  9. package/dist/intake.js +117 -0
  10. package/dist/intelligence/placement.js +2 -1
  11. package/dist/outcomes.js +141 -42
  12. package/dist/productExpectations.js +15 -0
  13. package/dist/requestReadiness.js +99 -0
  14. package/dist/server.js +675 -56
  15. package/dist/sidecar.js +5 -0
  16. package/dist/solutionPath.js +2 -2
  17. package/dist/tools/compliance.js +1014 -133
  18. package/dist/tools/creative.js +14 -12
  19. package/dist/tools/debug.js +83 -26
  20. package/dist/tools/design.js +344 -14
  21. package/dist/tools/experienceCompiler.js +1 -1
  22. package/dist/tools/experienceSensors.js +1 -1
  23. package/dist/tools/harness.js +13 -0
  24. package/dist/tools/integration.js +263 -90
  25. package/dist/tools/learning.js +1 -3
  26. package/dist/tools/project.js +987 -72
  27. package/dist/tools/sdkFacts.js +8 -1
  28. package/dist/tools/smoke.js +134 -0
  29. package/dist/tools/uxHarness.js +54 -6
  30. package/dist/uikitCustomization.js +22 -6
  31. package/dist/version.js +7 -3
  32. package/package.json +1 -1
  33. package/packages/intelligence/catalog/catalog.schema.json +1 -1
  34. package/packages/intelligence/catalog/experience-objects.json +2 -2
  35. package/packages/intelligence/catalog/variants.json +24 -0
  36. package/rules/event.yaml +3 -0
  37. package/rules/feed.yaml +251 -0
  38. package/rules/invitation.yaml +4 -0
  39. package/rules/live-data.yaml +110 -0
  40. package/rules/notification-tray.yaml +4 -0
  41. package/rules/poll.yaml +5 -0
  42. package/rules/sdk-lifecycle.yaml +559 -0
  43. package/rules/search.yaml +5 -0
  44. package/rules/security.yaml +12 -0
  45. package/rules/story.yaml +5 -0
  46. package/rules/user-blocking.yaml +5 -0
  47. package/skills/social-plus-vise/SKILL.md +163 -15
@@ -13,6 +13,21 @@ const MAX_PROTOTYPE_FILES = 300;
13
13
  const MAX_SCAN_FILES = 2000;
14
14
  const OFF_CONTRACT_SAMPLE = 20;
15
15
  const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".css", ".scss", ".vue", ".xml"]);
16
+ const UIKIT_JSON_DENY = new Set([
17
+ "package.json", "package-lock.json", "tsconfig.json", "jsconfig.json", "manifest.json",
18
+ "app.json", "babel.config.json", "components.json", "design-contract.json", "design-profile.json",
19
+ ]);
20
+ function isUikitThemeConfigCandidate(file) {
21
+ const base = path.basename(file).toLowerCase();
22
+ if (UIKIT_JSON_DENY.has(base) || base.startsWith("tsconfig.") || base.endsWith(".lock") || base.endsWith("-lock.json")) {
23
+ return false;
24
+ }
25
+ return base === "uikit.config.json" || base === "config.json" || /uikit/.test(base);
26
+ }
27
+ function looksLikeUikitThemeConfig(content) {
28
+ return /"preferred_theme"\s*:/.test(content) ||
29
+ (/"theme"\s*:/.test(content) && /"(?:primary_color|secondary_color|base_color|background_color)"\s*:/.test(content));
30
+ }
16
31
  const MODULE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
17
32
  const MAX_FILE_BYTES = 2_000_000;
18
33
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", "vendor", ".turbo", ".cache"]);
@@ -99,6 +114,7 @@ export async function extractDesignContractFromProject(repoPath) {
99
114
  const root = path.resolve(repoPath);
100
115
  const files = await findProjectDesignFiles(root);
101
116
  const css = [];
117
+ const htmlSources = [];
102
118
  const moduleTokens = [];
103
119
  const inputs = [];
104
120
  for (const abs of files) {
@@ -119,6 +135,10 @@ export async function extractDesignContractFromProject(repoPath) {
119
135
  css.push(content);
120
136
  inputs.push(rel);
121
137
  }
138
+ else if (ext === ".html" || ext === ".htm" || ext === ".vue" || ext === ".svelte") {
139
+ htmlSources.push(content);
140
+ inputs.push(rel);
141
+ }
122
142
  else if (MODULE_EXTS.has(ext)) {
123
143
  const tokens = extractTokensFromModule(content);
124
144
  if (tokens.length > 0) {
@@ -155,6 +175,13 @@ export async function extractDesignContractFromProject(repoPath) {
155
175
  inputs.push(rel);
156
176
  }
157
177
  }
178
+ else if (ext === ".kt") {
179
+ const tokens = extractTokensFromKotlin(content);
180
+ if (tokens.length > 0) {
181
+ moduleTokens.push(...tokens);
182
+ inputs.push(rel);
183
+ }
184
+ }
158
185
  }
159
186
  inputs.sort();
160
187
  const input_digests = {};
@@ -165,7 +192,7 @@ export async function extractDesignContractFromProject(repoPath) {
165
192
  }
166
193
  catch { }
167
194
  }
168
- return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
195
+ return buildDesignContract({ css, html: htmlSources, inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
169
196
  }
170
197
  export async function writeDesignContract(repoPath, contract) {
171
198
  const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
@@ -188,6 +215,145 @@ export async function readDesignContract(repoPath) {
188
215
  return null;
189
216
  }
190
217
  }
218
+ export const DESIGN_PROFILE_FILENAME = "design-profile.json";
219
+ export const BRAND_PROFILE_PROTOCOL = [
220
+ "Author sp-vise/design-profile.json to ground social.plus UI in the host's BRAND, not just its tokens.",
221
+ "A capable agent produces it by examining the host app's REAL screens (screenshots / live UI), capturing what a token scrape misses:",
222
+ ' - schemaVersion: "0.1.0"',
223
+ " - essence: one sentence of the brand identity",
224
+ " - personality: 4-7 adjectives ; voice: the tone of UI microcopy this brand uses",
225
+ ' - palette: [{ role: "bg|surface|text|textDim|accent|accentText|line", hex, note }] — colors WITH ROLES (which color does what), incl. how the accent is rationed',
226
+ " - typography: { headingFamily, bodyFamily, headingStyle, scaleNote, letterSpacing }",
227
+ ' - density: "dense|comfortable|airy" + rhythm note ; elevation: "flat|soft-shadow|strong"',
228
+ " - motion: implied motion energy ; imagery: gradient-heavy | photographic | minimal-illustrative | iconographic",
229
+ " - do: 4-6 concrete on-brand directives ; avoid: 3-5 off-brand things",
230
+ "Then build each surface honoring this profile (palette ROLES, type treatment, density, shape, voice).",
231
+ "Advisory — it grounds generation; it never gates `vise check`.",
232
+ ].join("\n");
233
+ const REQUIRED_PROFILE_STRINGS = ["essence", "voice", "density", "elevation", "motion", "imagery"];
234
+ export function validateDesignProfile(raw) {
235
+ if (!raw || typeof raw !== "object") {
236
+ return null;
237
+ }
238
+ const p = raw;
239
+ if (typeof p.schemaVersion !== "string") {
240
+ return null;
241
+ }
242
+ for (const f of REQUIRED_PROFILE_STRINGS) {
243
+ if (typeof p[f] !== "string" || p[f].trim().length === 0) {
244
+ return null;
245
+ }
246
+ }
247
+ const isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
248
+ if (!isStrArr(p.personality) || !isStrArr(p.do) || !isStrArr(p.avoid)) {
249
+ return null;
250
+ }
251
+ if (!Array.isArray(p.palette) ||
252
+ !p.palette.every((e) => e &&
253
+ typeof e === "object" &&
254
+ typeof e.role === "string" &&
255
+ typeof e.hex === "string")) {
256
+ return null;
257
+ }
258
+ if (!p.typography || typeof p.typography !== "object") {
259
+ return null;
260
+ }
261
+ return raw;
262
+ }
263
+ export async function readDesignProfile(repoPath) {
264
+ const target = path.join(path.resolve(repoPath), "sp-vise", DESIGN_PROFILE_FILENAME);
265
+ try {
266
+ return validateDesignProfile(JSON.parse(await readFile(target, "utf8")));
267
+ }
268
+ catch {
269
+ return null;
270
+ }
271
+ }
272
+ export async function writeDesignProfile(repoPath, profile) {
273
+ const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
274
+ await mkdir(sidecarDir, { recursive: true });
275
+ const target = path.join(sidecarDir, DESIGN_PROFILE_FILENAME);
276
+ await writeFile(target, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
277
+ return target;
278
+ }
279
+ function hexToRgb(value) {
280
+ const h = normalizeHex(value.trim().startsWith("#") ? value.trim() : `#${value.trim()}`);
281
+ const m = /^#([0-9a-f]{6})$/.exec(h);
282
+ if (!m) {
283
+ return null;
284
+ }
285
+ const n = parseInt(m[1], 16);
286
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
287
+ }
288
+ export function contrastRatio(a, b) {
289
+ const ca = hexToRgb(a);
290
+ const cb = hexToRgb(b);
291
+ if (!ca || !cb) {
292
+ return null;
293
+ }
294
+ const lum = ([r, g, bl]) => {
295
+ const channel = (c) => {
296
+ const s = c / 255;
297
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
298
+ };
299
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(bl);
300
+ };
301
+ const la = lum(ca);
302
+ const lb = lum(cb);
303
+ const hi = Math.max(la, lb);
304
+ const lo = Math.min(la, lb);
305
+ return Math.round(((hi + 0.05) / (lo + 0.05)) * 100) / 100;
306
+ }
307
+ const DESIGN_REVIEW_STATES = [
308
+ "empty — first post / no messages yet: an inviting empty state, not a blank screen",
309
+ "loading — skeletons matching the real layout, not a bare spinner",
310
+ "error — a human message + a retry affordance, not a dead end",
311
+ ];
312
+ export function paletteContrastChecks(palette) {
313
+ const byRole = (role) => palette.find((p) => p.role.toLowerCase().trim() === role)?.hex;
314
+ const bg = byRole("bg") ?? byRole("surface") ?? byRole("background");
315
+ const text = byRole("text");
316
+ const textDim = byRole("textdim");
317
+ const accent = byRole("accent");
318
+ const accentText = byRole("accenttext");
319
+ const checks = [];
320
+ const add = (pair, fg, on, threshold, note) => {
321
+ if (!fg || !on) {
322
+ return;
323
+ }
324
+ const ratio = contrastRatio(fg, on);
325
+ if (ratio == null) {
326
+ return;
327
+ }
328
+ checks.push({ pair, ratio, threshold, passes: ratio >= threshold, ...(note ? { note } : {}) });
329
+ };
330
+ add("body text on background", text, bg, 4.5);
331
+ add("dim/secondary text on background", textDim, bg, 4.5);
332
+ add("text on accent (e.g. a button label)", accentText, accent, 4.5, "the white-on-bright trap: a label on an accent fill must clear 4.5:1");
333
+ add("accent against background", accent, bg, 3.0, "accent used for icons/borders needs 3:1; if you put TEXT in the raw accent it needs 4.5:1 — otherwise reserve the accent for fills");
334
+ return checks;
335
+ }
336
+ export function buildDesignReviewLoop(profile) {
337
+ const accessibility = paletteContrastChecks(profile.palette);
338
+ const paletteDiscipline = profile.palette
339
+ .filter((p) => p.note != null && p.note.trim().length > 0)
340
+ .map((p) => `${p.role} (${p.hex}): ${p.note.trim()}`);
341
+ return {
342
+ status: "custom-ui",
343
+ note: "Advisory generation-time self-check for the FIRST custom-UI build of this surface — render it, score it against the checks below, and iterate. vise never edits your code and this never gates `vise check`; your own design choices (and edits to sp-vise/design-profile.json) are authority. It is not an ongoing audit of work you've already tuned.",
344
+ protocol: [
345
+ "Build the surface honouring brandGrounding — palette ROLES, type treatment, density, voice — not just the hex values.",
346
+ "Render what you built (headless browser / your preview tooling) and actually look at it.",
347
+ "Score the render against the checks below: brandAvoids, paletteDiscipline, the computed accessibility ratios, statesRequired, and 'does it read as intentionally designed, not framework-default?'.",
348
+ "Do NOT eyeball contrast. For every text/icon colour you use that is NOT one of the palette pairs above (e.g. a one-off 'subtle' grey), compute it with `vise design contrast <foreground> <background>` and don't ship a pair below 4.5:1 (3:1 for large/bold text) — a faint colour that looks fine on a bright screen routinely fails.",
349
+ "Fix each failure and re-render. Iterate up to 3 rounds; stop when the checks are clean. Don't ship a round with a failing accessibility pair.",
350
+ ],
351
+ brandAvoids: profile.avoid,
352
+ paletteDiscipline,
353
+ accessibility,
354
+ statesRequired: DESIGN_REVIEW_STATES,
355
+ };
356
+ }
191
357
  export async function writeDesignPreview(repoPath, contract, referencePath) {
192
358
  const check = await runDesignCheck(repoPath).catch(() => null);
193
359
  const reference = referencePath ? await readReferenceHtml(referencePath) : null;
@@ -232,8 +398,19 @@ export const designPreviewTool = {
232
398
  written,
233
399
  digest: contract.digest,
234
400
  strength: contract.stats.strength,
401
+ ...(contract.source.self_referential ? { self_referential: true } : {}),
235
402
  referenceEmbedded: Boolean(reference),
236
- conformance: check && check.status === "advisory" ? { coverage: `${check.tokenCoverage?.referenced}/${check.tokenCoverage?.declared_total}`, off_contract: check.colorLiterals?.off_contract } : "no UI code scanned",
403
+ conformance: check && check.status === "advisory"
404
+ ? {
405
+ coverage: `${check.tokenCoverage?.referenced}/${check.tokenCoverage?.declared_total}`,
406
+ off_contract: check.colorLiterals?.off_contract,
407
+ off_contract_ratio: check.colorLiterals?.off_contract_ratio,
408
+ ...(check.colorLiterals?.palette_review ? { palette_review: check.colorLiterals.palette_review } : {}),
409
+ }
410
+ : "no UI code scanned",
411
+ ...(contract.source.self_referential
412
+ ? { self_referential_note: "Contract derived from this project's own files (--from-project): conformance is circular and is NOT a design-quality signal. For a real design target, extract from an external prototype/theme." }
413
+ : {}),
237
414
  note: "Open the HTML to visually compare the contract (and embedded reference, if HTML) against your app. A human or VLM judges the visual match — this is not an automated pixel diff.",
238
415
  where: written ? `Open ${written} in a browser.` : "Not written (write=false).",
239
416
  });
@@ -764,11 +941,15 @@ export async function generateDesignReference(repoPath, contract, title) {
764
941
  .join("\n ");
765
942
  const digestShort = esc(contract.digest.slice(0, 23));
766
943
  const logoLetter = esc(title.slice(0, 1).toUpperCase());
944
+ const rootDecls = [...tokenCss.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)]
945
+ .map((m) => ` ${m[1]}: ${safeCss(m[2].trim())};`)
946
+ .join("\n");
947
+ const rootCss = rootDecls ? `:root{\n${rootDecls}\n}` : "";
767
948
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
768
949
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
769
950
  <title>${esc(title)} — Design System</title>
770
951
  <style>
771
- ${tokenCss}
952
+ ${rootCss}
772
953
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
773
954
  html{scroll-behavior:smooth}
774
955
  body{
@@ -1076,6 +1257,50 @@ export const designCheckTool = {
1076
1257
  return textResult(await runDesignCheck(optionalStringField(args, "repoPath") ?? "."));
1077
1258
  },
1078
1259
  };
1260
+ export function designContrastReport(foreground, background) {
1261
+ const fg = typeof foreground === "string" ? foreground : "";
1262
+ const bg = typeof background === "string" ? background : "";
1263
+ const ratio = contrastRatio(fg, bg);
1264
+ if (ratio == null) {
1265
+ return {
1266
+ status: "invalid",
1267
+ message: `Could not parse a colour. Expected hex like #5a6477 or #abc. Got foreground="${fg}", background="${bg}".`,
1268
+ };
1269
+ }
1270
+ const verdict = ratio >= 7.0
1271
+ ? "Excellent — clears WCAG AAA for body text."
1272
+ : ratio >= 4.5
1273
+ ? "Passes WCAG AA for body text."
1274
+ : ratio >= 3.0
1275
+ ? "Passes AA only for LARGE/bold text (≥18.66px bold or ≥24px) and UI/graphics — FAILS for normal body text."
1276
+ : "FAILS WCAG AA — unreadable as text. Choose a colour with more contrast against the background.";
1277
+ return {
1278
+ status: "ok",
1279
+ foreground: fg,
1280
+ background: bg,
1281
+ ratio,
1282
+ passes: { aaNormal: ratio >= 4.5, aaLarge: ratio >= 3.0, aaaNormal: ratio >= 7.0 },
1283
+ verdict,
1284
+ note: "WCAG 2.x contrast ratio (1–21). Advisory — never gates `vise check`.",
1285
+ };
1286
+ }
1287
+ export const designContrastTool = {
1288
+ name: "design_contrast",
1289
+ description: "Compute the WCAG 2.x contrast ratio between two colours (advisory). Use it to check any text/icon colour you use that is NOT one of your brand palette's role pairs — never eyeball contrast.",
1290
+ inputSchema: {
1291
+ type: "object",
1292
+ properties: {
1293
+ foreground: { type: "string", description: "Foreground (text/icon) colour, e.g. #5a6477" },
1294
+ background: { type: "string", description: "Background colour it sits on, e.g. #0e1726" },
1295
+ },
1296
+ required: ["foreground", "background"],
1297
+ additionalProperties: false,
1298
+ },
1299
+ async call(input) {
1300
+ const args = objectInput(input);
1301
+ return textResult(designContrastReport(optionalStringField(args, "foreground"), optionalStringField(args, "background")));
1302
+ },
1303
+ };
1079
1304
  export async function runDesignCheck(repoPath) {
1080
1305
  const repoRoot = path.resolve(repoPath);
1081
1306
  const contract = await readDesignContract(repoRoot);
@@ -1087,13 +1312,20 @@ export async function runDesignCheck(repoPath) {
1087
1312
  };
1088
1313
  }
1089
1314
  const staleContract = await checkContractFreshness(repoRoot, contract);
1090
- const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => SCAN_EXTS.has(path.extname(file).toLowerCase()));
1315
+ const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => {
1316
+ const ext = path.extname(file).toLowerCase();
1317
+ if (SCAN_EXTS.has(ext))
1318
+ return true;
1319
+ return ext === ".json" && isUikitThemeConfigCandidate(file);
1320
+ });
1091
1321
  if (files.length === 0) {
1092
1322
  return { status: "no-sources", message: "No UI source files found to check against the contract.", contract: contractSummary(contract), note: ADVISORY_NOTE };
1093
1323
  }
1094
1324
  const declaredTokens = contract.tokens.filter((token) => token.provenance === "declared");
1095
1325
  const contractColorValues = new Set(contract.tokens.filter((token) => token.category === "color").map((token) => token.value));
1096
1326
  const referenced = new Set();
1327
+ const referencedInApp = new Set();
1328
+ const seedTokensRel = SP_TOKENS_PATH.split("/").join(path.sep);
1097
1329
  const colorSample = [];
1098
1330
  const definedVars = new Set();
1099
1331
  const varRefs = [];
@@ -1112,16 +1344,24 @@ export async function runDesignCheck(repoPath) {
1112
1344
  catch {
1113
1345
  continue;
1114
1346
  }
1347
+ if (path.extname(file).toLowerCase() === ".json" && !looksLikeUikitThemeConfig(content)) {
1348
+ continue;
1349
+ }
1115
1350
  scanned += 1;
1116
1351
  const rel = path.relative(repoRoot, file);
1117
1352
  const isCss = file.toLowerCase().endsWith(".css") || file.toLowerCase().endsWith(".scss");
1353
+ const isSeedFile = rel === SP_TOKENS_PATH || rel === seedTokensRel;
1354
+ const contentLower = content.toLowerCase();
1118
1355
  for (const token of declaredTokens) {
1119
1356
  const key = tokenKey(token);
1120
- if (referenced.has(key)) {
1357
+ if (referenced.has(key) && referencedInApp.has(key)) {
1121
1358
  continue;
1122
1359
  }
1123
- if ((token.name && content.includes(token.name)) || content.includes(token.value)) {
1360
+ if ((token.name && content.includes(token.name)) || contentLower.includes(token.value.toLowerCase())) {
1124
1361
  referenced.add(key);
1362
+ if (!isSeedFile) {
1363
+ referencedInApp.add(key);
1364
+ }
1125
1365
  }
1126
1366
  }
1127
1367
  for (const value of scanColorLiterals(content)) {
@@ -1144,12 +1384,38 @@ export async function runDesignCheck(repoPath) {
1144
1384
  }
1145
1385
  const referencedTokens = declaredTokens.filter((token) => referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1146
1386
  const unreferencedTokens = declaredTokens.filter((token) => !referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
1387
+ const seededOnlyTokens = declaredTokens
1388
+ .filter((token) => referenced.has(tokenKey(token)) && !referencedInApp.has(tokenKey(token)))
1389
+ .map((token) => token.name ?? token.value);
1147
1390
  const contractTokenNames = new Set(contract.tokens.map((token) => token.name).filter((name) => Boolean(name)));
1148
1391
  const undefinedRefs = dedupeByToken(varRefs.filter((ref) => !definedVars.has(ref.token) && !contractTokenNames.has(ref.token)));
1392
+ const offContract = totalColors - onContract;
1393
+ const offContractRatio = totalColors > 0 ? offContract / totalColors : 0;
1394
+ const paletteReview = totalColors >= 10 && offContractRatio >= 0.5
1395
+ ? `${offContract} of ${totalColors} color literals (${Math.round(offContractRatio * 100)}%) are off-contract — the palette is mostly ad-hoc hardcoded colors, not token-driven. Worth a human's eyes on visual coherence.`
1396
+ : undefined;
1397
+ const selfRefNote = contract.source.self_referential
1398
+ ? `Contract is SELF-REFERENTIAL (derived from this project's own files via --from-project): token-coverage measures internal reuse, not conformance to an external/authoritative design — not a design-quality signal. For that, extract from a real prototype/theme. `
1399
+ : "";
1400
+ const profile = await readDesignProfile(repoRoot);
1401
+ const palettePairs = profile ? paletteContrastChecks(profile.palette) : [];
1402
+ const accessibility = palettePairs.length > 0
1403
+ ? {
1404
+ palettePairs,
1405
+ failing: palettePairs.filter((c) => !c.passes).length,
1406
+ note: "Computed WCAG contrast for your brand palette's role pairs (advisory, never gates). A failing pair means that combination is unreadable — fix the palette or don't use it for text. Don't eyeball one-off colours: run `vise design contrast <fg> <bg>`.",
1407
+ }
1408
+ : undefined;
1149
1409
  return {
1150
1410
  status: "advisory",
1151
- message: `Checked ${scanned} source file(s) against design contract ${contract.digest}. ` +
1152
- `${referencedTokens.length}/${declaredTokens.length} declared tokens referenced; ${totalColors - onContract} of ${totalColors} color literal(s) are off-contract (review hints)` +
1411
+ message: selfRefNote +
1412
+ `Checked ${scanned} source file(s) against design contract ${contract.digest}. ` +
1413
+ (paletteReview ? `${paletteReview} ` : "") +
1414
+ `${referencedTokens.length}/${declaredTokens.length} declared tokens referenced` +
1415
+ (seededOnlyTokens.length > 0
1416
+ ? ` (${referencedTokens.length - seededOnlyTokens.length} used in your code, ${seededOnlyTokens.length} only in the Vise-seeded ${SP_TOKENS_PATH} — apply them to count)`
1417
+ : "") +
1418
+ `; ${offContract} of ${totalColors} color literal(s) are off-contract (review hints)` +
1153
1419
  (undefinedRefs.length > 0 ? `; ${undefinedRefs.length} undefined token reference(s) (likely broken styles)` : "") +
1154
1420
  ".",
1155
1421
  contract: contractSummary(contract),
@@ -1158,18 +1424,23 @@ export async function runDesignCheck(repoPath) {
1158
1424
  referenced: referencedTokens.length,
1159
1425
  referenced_tokens: referencedTokens,
1160
1426
  unreferenced_tokens: unreferencedTokens,
1427
+ referenced_in_app: referencedTokens.length - seededOnlyTokens.length,
1428
+ seeded_only_tokens: seededOnlyTokens,
1161
1429
  },
1162
1430
  colorLiterals: {
1163
1431
  scanned_files: scanned,
1164
1432
  total: totalColors,
1165
1433
  on_contract: onContract,
1166
- off_contract: totalColors - onContract,
1434
+ off_contract: offContract,
1435
+ off_contract_ratio: Math.round(offContractRatio * 100) / 100,
1436
+ ...(paletteReview ? { palette_review: paletteReview } : {}),
1167
1437
  sample: colorSample,
1168
1438
  },
1169
1439
  undefinedTokenRefs: {
1170
1440
  count: undefinedRefs.length,
1171
1441
  sample: undefinedRefs.slice(0, OFF_CONTRACT_SAMPLE),
1172
1442
  },
1443
+ ...(accessibility ? { accessibility } : {}),
1173
1444
  ...(staleContract ? { staleContract } : {}),
1174
1445
  note: ADVISORY_NOTE,
1175
1446
  };
@@ -1232,6 +1503,7 @@ function contractSummary(contract) {
1232
1503
  strength: contract.stats.strength,
1233
1504
  declared_tokens: contract.stats.declared_tokens,
1234
1505
  inferred_tokens: contract.stats.inferred_tokens,
1506
+ ...(contract.source.self_referential ? { self_referential: true } : {}),
1235
1507
  };
1236
1508
  }
1237
1509
  function tokenKey(token) {
@@ -1389,6 +1661,12 @@ async function isDesignFile(full) {
1389
1661
  if (ext === ".swift") {
1390
1662
  return /(theme|color|palette|style|appearance|design)/.test(base);
1391
1663
  }
1664
+ if (ext === ".kt") {
1665
+ return /(theme|color|token|palette)/.test(base);
1666
+ }
1667
+ if (ext === ".html" || ext === ".htm" || ext === ".vue" || ext === ".svelte") {
1668
+ return htmlFileDeclaresDesignTokens(full);
1669
+ }
1392
1670
  if (MODULE_EXTS.has(ext)) {
1393
1671
  if (/^tailwind\.config\.(js|ts|cjs|mjs)$/.test(base)) {
1394
1672
  return true;
@@ -1413,6 +1691,20 @@ async function cssFileDeclaresDesignTokens(full) {
1413
1691
  return false;
1414
1692
  }
1415
1693
  }
1694
+ async function htmlFileDeclaresDesignTokens(full) {
1695
+ try {
1696
+ const info = await stat(full);
1697
+ if (info.size > MAX_FILE_BYTES) {
1698
+ return false;
1699
+ }
1700
+ const content = await readFile(full, "utf8");
1701
+ const styleCss = extractStyleBlocks(content).join("\n");
1702
+ return /(^|[{\s;])--[a-zA-Z0-9_-]+\s*:\s*[^;{}]+[;}]/.test(stripCssComments(styleCss));
1703
+ }
1704
+ catch {
1705
+ return false;
1706
+ }
1707
+ }
1416
1708
  export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = []) {
1417
1709
  const cssText = [...sources.css, ...sources.html.flatMap(extractStyleBlocks)].join("\n");
1418
1710
  const strippedCss = stripCssComments(cssText);
@@ -1485,11 +1777,12 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1485
1777
  const components = inferComponents(sources.html, strippedCss);
1486
1778
  const declaredCount = tokens.filter((token) => token.provenance === "declared").length;
1487
1779
  const inferredCount = tokens.filter((token) => token.provenance === "inferred").length;
1780
+ const selfReferential = sourceMeta.kind === "host-project";
1488
1781
  const contract = {
1489
1782
  schema_version: DESIGN_CONTRACT_SCHEMA_VERSION,
1490
1783
  vise_version: packageVersion,
1491
1784
  foundry_version: packageVersion,
1492
- source: sourceMeta,
1785
+ source: selfReferential ? { ...sourceMeta, self_referential: true } : sourceMeta,
1493
1786
  digest: "",
1494
1787
  tokens,
1495
1788
  breakpoints,
@@ -1498,15 +1791,15 @@ export function buildDesignContract(sources, sourceMeta, extraDeclaredTokens = [
1498
1791
  declared_tokens: declaredCount,
1499
1792
  inferred_tokens: inferredCount,
1500
1793
  advisory_components: components.length,
1501
- strength: gradeStrength(declaredCount, inferredCount),
1794
+ strength: gradeStrength(declaredCount, inferredCount, selfReferential),
1502
1795
  },
1503
1796
  };
1504
1797
  contract.digest = digestContract(contract);
1505
1798
  return contract;
1506
1799
  }
1507
- function gradeStrength(declared, inferred) {
1800
+ function gradeStrength(declared, inferred, selfReferential = false) {
1508
1801
  if (declared >= 6) {
1509
- return "strong";
1802
+ return selfReferential ? "partial" : "strong";
1510
1803
  }
1511
1804
  if (declared >= 1 || inferred >= 4) {
1512
1805
  return "partial";
@@ -2018,6 +2311,22 @@ export function extractTokensFromDart(content) {
2018
2311
  }
2019
2312
  return tokens;
2020
2313
  }
2314
+ export function extractTokensFromKotlin(content) {
2315
+ const tokens = [];
2316
+ const seen = new Set();
2317
+ const pattern = /(\w+)\s*[:=]\s*(?:const\s+)?Color\(\s*0x([0-9a-fA-F]{8})\s*\)/g;
2318
+ let match;
2319
+ while ((match = pattern.exec(content)) !== null) {
2320
+ const name = match[1];
2321
+ const value = `#${match[2].slice(2).toLowerCase()}`;
2322
+ const key = `color::${name}::${value}`;
2323
+ if (!seen.has(key)) {
2324
+ seen.add(key);
2325
+ tokens.push({ category: "color", name, value, provenance: "declared", uses: 0 });
2326
+ }
2327
+ }
2328
+ return tokens;
2329
+ }
2021
2330
  export function extractTokensFromColorset(contentsJson, name) {
2022
2331
  let parsed;
2023
2332
  try {
@@ -2280,7 +2589,7 @@ function bestCandidate(a, b) {
2280
2589
  return b;
2281
2590
  return a.token.length <= b.token.length ? a : b;
2282
2591
  }
2283
- export function buildDesignBrief(contract) {
2592
+ export function buildDesignBrief(contract, profile) {
2284
2593
  const strength = contract.stats.strength;
2285
2594
  const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
2286
2595
  const roleMap = new Map();
@@ -2458,6 +2767,26 @@ export function buildDesignBrief(contract) {
2458
2767
  : tokenCount > 0
2459
2768
  ? `Brief grounded in ${tokenCount} named token(s); no colour roles could be inferred from token names. Contract strength: ${strength}. roles/hints cover ${briefCoveredCount} of ${totalDeclaredTokens} declared tokens — consult the full set.`
2460
2769
  : `Contract has no named tokens — guidance is unavailable. Contract strength: ${strength}. Run \`vise design extract --from-project\` to derive tokens from the host project.`;
2770
+ const brandGrounding = profile
2771
+ ? {
2772
+ status: "active",
2773
+ essence: profile.essence,
2774
+ personality: profile.personality,
2775
+ voice: profile.voice,
2776
+ palette: profile.palette,
2777
+ typography: profile.typography,
2778
+ density: profile.density,
2779
+ elevation: profile.elevation,
2780
+ motion: profile.motion,
2781
+ imagery: profile.imagery,
2782
+ do: profile.do,
2783
+ avoid: profile.avoid,
2784
+ source: profile.capturedFrom ?? "sp-vise/design-profile.json",
2785
+ }
2786
+ : {
2787
+ status: "absent",
2788
+ howTo: "These are tokens only. For richer brand grounding (personality, type system, density, voice, do/avoid), author sp-vise/design-profile.json from the host's real screens, then honor it per surface. See the 'Brand profile' subsection of the social.plus design skill for the schema.",
2789
+ };
2461
2790
  return {
2462
2791
  summary,
2463
2792
  strength,
@@ -2466,6 +2795,7 @@ export function buildDesignBrief(contract) {
2466
2795
  do: doLines,
2467
2796
  avoid: avoidLines,
2468
2797
  reviewNotes,
2798
+ brandGrounding,
2469
2799
  };
2470
2800
  }
2471
2801
  export function buildOutcomeDesignRecipe(brief, outcome) {
@@ -54,7 +54,7 @@ export async function compileExperience(options) {
54
54
  const repoRoot = path.resolve(options.repoPath);
55
55
  const selection = await readCreativeSelection(repoRoot);
56
56
  if (!selection) {
57
- throw new Error("Experience Compiler requires an accepted creative selection. Run `vise creative . --request \"...\"`, then `vise creative accept . --variant <id>` first.");
57
+ throw new Error("Experience Compiler requires an accepted creative selection. Run `vise creative . --request \"...\"`, then `vise creative accept . --variant <id> --rationale \"<why>\" --confidence high` first.");
58
58
  }
59
59
  const request = options.request ?? selection.source.request;
60
60
  const inspection = await inspectProject(repoRoot, options.surfacePath);
@@ -266,7 +266,7 @@ function businessSensors(uxHarness) {
266
266
  title: "Selected variant maps goals to patterns, objects, and UX",
267
267
  status: hasMapping ? "passed" : selected ? "needs-review" : "needs-setup",
268
268
  source: "ux-harness",
269
- command: "vise creative accept . --variant <id>",
269
+ command: "vise creative accept . --variant <id> --rationale \"<why>\" --confidence high",
270
270
  reason: hasMapping
271
271
  ? `Selected variant "${selected?.title}" carries business goals, solution patterns, experience objects, and UX patterns.`
272
272
  : selected
@@ -305,6 +305,19 @@ async function packageJsonSensors(root) {
305
305
  ...(skip ? { skip } : {}),
306
306
  };
307
307
  });
308
+ if (hasPackageDependency(parsed, "@amityco/ts-sdk-react-native")) {
309
+ sensors.push({
310
+ name: "React Native SDK import smoke",
311
+ command: [
312
+ "node",
313
+ "-e",
314
+ "require.resolve('@amityco/ts-sdk-react-native'); console.log('Vise smoke: @amityco/ts-sdk-react-native resolves')",
315
+ ],
316
+ timing: "after-change",
317
+ purpose: "Verify the social.plus React Native SDK resolves from the host project runtime environment.",
318
+ source: "package.json",
319
+ });
320
+ }
308
321
  if (hasPackageDependency(parsed, "@amityco/ts-sdk")) {
309
322
  sensors.push({
310
323
  name: "TypeScript SDK import smoke",