@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.
- package/CHANGELOG.md +75 -2
- package/README.md +23 -10
- package/dist/capabilities.js +35 -4
- package/dist/entryState.js +71 -0
- package/dist/experience.js +70 -0
- package/dist/explore.js +51 -0
- package/dist/flow.js +382 -0
- package/dist/humanFormat.js +251 -0
- package/dist/intake.js +117 -0
- package/dist/intelligence/placement.js +2 -1
- package/dist/outcomes.js +141 -42
- package/dist/productExpectations.js +15 -0
- package/dist/requestReadiness.js +99 -0
- package/dist/server.js +675 -56
- package/dist/sidecar.js +5 -0
- package/dist/solutionPath.js +2 -2
- package/dist/tools/compliance.js +1014 -133
- package/dist/tools/creative.js +14 -12
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +344 -14
- package/dist/tools/experienceCompiler.js +1 -1
- package/dist/tools/experienceSensors.js +1 -1
- package/dist/tools/harness.js +13 -0
- package/dist/tools/integration.js +263 -90
- package/dist/tools/learning.js +1 -3
- package/dist/tools/project.js +987 -72
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/tools/smoke.js +134 -0
- package/dist/tools/uxHarness.js +54 -6
- package/dist/uikitCustomization.js +22 -6
- package/dist/version.js +7 -3
- package/package.json +1 -1
- package/packages/intelligence/catalog/catalog.schema.json +1 -1
- package/packages/intelligence/catalog/experience-objects.json +2 -2
- package/packages/intelligence/catalog/variants.json +24 -0
- package/rules/event.yaml +3 -0
- package/rules/feed.yaml +251 -0
- package/rules/invitation.yaml +4 -0
- package/rules/live-data.yaml +110 -0
- package/rules/notification-tray.yaml +4 -0
- package/rules/poll.yaml +5 -0
- package/rules/sdk-lifecycle.yaml +559 -0
- package/rules/search.yaml +5 -0
- package/rules/security.yaml +12 -0
- package/rules/story.yaml +5 -0
- package/rules/user-blocking.yaml +5 -0
- package/skills/social-plus-vise/SKILL.md +163 -15
package/dist/tools/design.js
CHANGED
|
@@ -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:
|
|
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"
|
|
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
|
-
${
|
|
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) =>
|
|
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)) ||
|
|
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:
|
|
1152
|
-
|
|
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:
|
|
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
|
|
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
|
package/dist/tools/harness.js
CHANGED
|
@@ -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",
|