@amityco/social-plus-vise 0.12.2 → 0.12.4

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/README.md CHANGED
@@ -250,6 +250,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
250
250
  | `vise design extract --from-project [path] [--no-write]` | No external prototype? Derive the contract from the host project's **own** design system — CSS custom properties (incl. shadcn `:root` and Tailwind v4 `@theme`), TS/JS token modules, inline tailwind configs, **Android** `colors.xml`/`dimens.xml`, **Flutter** `Color(0x…)`, and **iOS** `.xcassets/*.colorset` + Swift `Color(hex:)`/`Color(red:g:b:)`. Reference values (`var()`/`theme()`/`calc()`) are skipped, so a var-mapped config contributes nothing rather than wrong tokens |
251
251
  | `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
252
252
  | `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
253
+ | `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
253
254
 
254
255
  The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
255
256
 
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
10
- import { designCheckTool, designExtractTool, designPreviewTool } from "./tools/design.js";
10
+ import { designCheckTool, designExtractTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
11
11
  import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
12
12
  import { planHarnessTool } from "./tools/harness.js";
13
13
  import { planIntegrationTool } from "./tools/integration.js";
@@ -37,6 +37,7 @@ const tools = new Map([
37
37
  designExtractTool,
38
38
  designCheckTool,
39
39
  designPreviewTool,
40
+ designReferenceTool,
40
41
  ].map((tool) => [tool.name, tool]));
41
42
  const bundledSkillName = "social-plus-vise";
42
43
  const cliResult = await handleCli(process.argv.slice(2));
@@ -295,7 +296,16 @@ async function handleCli(args) {
295
296
  });
296
297
  return "exit";
297
298
  }
298
- console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", or "preview".`);
299
+ if (sub === "reference") {
300
+ assertOnlyKnownFlags(subArgs, ["title", "no-write"], "design reference");
301
+ await printToolResult(designReferenceTool, {
302
+ repoPath: positionalRepoPath(subArgs),
303
+ title: flagValue(subArgs, "title"),
304
+ write: !hasFlag(subArgs, "no-write"),
305
+ });
306
+ return "exit";
307
+ }
308
+ console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", or "reference".`);
299
309
  process.exitCode = 1;
300
310
  return "exit";
301
311
  }
@@ -504,23 +514,28 @@ Usage:
504
514
  vise design extract --from-project [repoPath] [--no-write]
505
515
  vise design check [repoPath]
506
516
  vise design preview [repoPath] [--reference <prototypePath>]
507
-
508
- extract Build a graded design contract and write it to sp-vise/design-contract.json.
509
- Declared CSS custom properties become exact tokens; repeated literal values
510
- become inferred (advisory) tokens; single-use literals are treated as one-offs.
511
- With --from-project (no external prototype), derive the contract from the host
512
- project's OWN design system: CSS custom properties (incl. shadcn :root and
513
- Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
514
- colors.xml/dimens.xml, Flutter Color(0x..), and iOS .xcassets/.colorset +
515
- Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
516
- var-mapped config contributes nothing rather than wrong tokens.
517
- check Advisory, non-blocking report on how closely the project's UI code
518
- matches the contract (token coverage + on/off-contract color literals).
519
- Never fails a build; it is NOT a \`vise check\` gate.
520
- preview Write a self-contained sp-vise/design-preview.html: the contract's tokens
521
- as visual swatches + the conformance report + the HTML reference embedded
522
- (with --reference) for side-by-side review. Vise renders; a human/VLM
523
- judges the visual match. Dependency-free NOT an automated pixel diff.`;
517
+ vise design reference [repoPath] [--title "Streamly"] [--no-write]
518
+
519
+ extract Build a graded design contract and write it to sp-vise/design-contract.json.
520
+ Declared CSS custom properties become exact tokens; repeated literal values
521
+ become inferred (advisory) tokens; single-use literals are treated as one-offs.
522
+ With --from-project (no external prototype), derive the contract from the host
523
+ project's OWN design system: CSS custom properties (incl. shadcn :root and
524
+ Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
525
+ colors.xml/dimens.xml, Flutter Color(0x..), and iOS .xcassets/.colorset +
526
+ Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
527
+ var-mapped config contributes nothing rather than wrong tokens.
528
+ check Advisory, non-blocking report on how closely the project's UI code
529
+ matches the contract (token coverage + on/off-contract color literals).
530
+ Never fails a build; it is NOT a \`vise check\` gate.
531
+ preview Write a self-contained sp-vise/design-preview.html: the contract's tokens
532
+ as visual swatches + the conformance report + the HTML reference embedded
533
+ (with --reference) for side-by-side review. Vise renders; a human/VLM
534
+ judges the visual match. Dependency-free — NOT an automated pixel diff.
535
+ reference Write a self-contained sp-vise/design-reference.html: human/VLM-readable
536
+ design-system spec (token swatches, type samples, component demos, growth-layer
537
+ summary). Pairs with the machine-readable design-contract.json. Use --title to
538
+ set the design system name. Advisory — not an enforcement gate.`;
524
539
  }
525
540
  return `${packageName}
526
541
 
@@ -358,14 +358,17 @@ export async function checkCompliance(repoPath) {
358
358
  // If the current source now produces a finding, the old sync record must
359
359
  // not mask code drift; the next `vise sync` will remove it.
360
360
  if (attestation.status === "deterministic-pass") {
361
+ const failStatus = rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail";
361
362
  results.push({
362
363
  ruleId: rule.id,
363
364
  title: rule.title,
364
365
  severity: rule.severity,
365
- status: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
366
- reason: rule.enforcement.attestation.allowed
367
- ? "Current deterministic check failed; previously synced deterministic-pass evidence is stale."
368
- : "Current deterministic check failed; this rule does not allow attestation.",
366
+ status: failStatus,
367
+ reason: rule.advisory
368
+ ? "Advisory: informational only does not affect compliance status."
369
+ : rule.enforcement.attestation.allowed
370
+ ? "Current deterministic check failed; previously synced deterministic-pass evidence is stale."
371
+ : "Current deterministic check failed; this rule does not allow attestation.",
369
372
  finding,
370
373
  recommendation: finding?.recommendation,
371
374
  rationale: rule.rationale,
@@ -384,8 +387,10 @@ export async function checkCompliance(repoPath) {
384
387
  ruleId: rule.id,
385
388
  title: rule.title,
386
389
  severity: rule.severity,
387
- status: rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
388
- reason: "Recorded attestation source fingerprints changed. Re-check the evidence and record a fresh attestation.",
390
+ status: rule.advisory ? "advisory" : rule.enforcement.attestation.allowed ? "attestation-needed" : "deterministic-fail",
391
+ reason: rule.advisory
392
+ ? "Advisory: informational only — does not affect compliance status."
393
+ : "Recorded attestation source fingerprints changed. Re-check the evidence and record a fresh attestation.",
389
394
  finding,
390
395
  recommendation: finding?.recommendation,
391
396
  rationale: rule.rationale,
@@ -413,9 +418,14 @@ export async function checkCompliance(repoPath) {
413
418
  continue;
414
419
  }
415
420
  }
416
- const baseStatus = (rule.enforcement.attestation.allowed || isInferential) ? "attestation-needed" : "deterministic-fail";
421
+ const baseStatus = rule.advisory
422
+ ? "advisory"
423
+ : (rule.enforcement.attestation.allowed || isInferential) ? "attestation-needed" : "deterministic-fail";
417
424
  let fallbackReason = "This rule does not allow attestation.";
418
- if (isInferential) {
425
+ if (rule.advisory) {
426
+ fallbackReason = "Advisory: informational only — does not affect compliance status.";
427
+ }
428
+ else if (isInferential) {
419
429
  fallbackReason = "Inferential check required. Please provide a host-agent attestation.";
420
430
  }
421
431
  else if (rule.enforcement.attestation.allowed) {
@@ -436,6 +446,7 @@ export async function checkCompliance(repoPath) {
436
446
  const summary = summarize(results);
437
447
  const hasBlocked = results.some((result) => result.status === "blocked");
438
448
  const hasDeterministicFailure = results.some((result) => result.status === "deterministic-fail");
449
+ // "advisory" status is intentionally excluded — advisory rules surface but never block.
439
450
  const needsAttestation = results.some((result) => result.status === "attestation-needed" || result.status === "stale");
440
451
  // Precedence: blocked (exit 3) > deterministic-failures (2) > needs-attestation (1) > green (0).
441
452
  // Contract drift (exit 4) is handled earlier and short-circuits the loop.
@@ -297,7 +297,14 @@ export function renderDesignPreview(contract, check, referenceHtml) {
297
297
  const shadows = byCat("shadow").map((t) => tokenRow(t, `<span class="box" style="box-shadow:${safeCss(t.value)}"></span>`)).join("");
298
298
  const fonts = byCat("fontFamily").map((t) => tokenRow(t, `<span class="type" style="font-family:${safeCss(t.value)}">Ag</span>`)).join("");
299
299
  const sizes = byCat("fontSize").map((t) => tokenRow(t, `<span class="type" style="font-size:${safeCss(t.value)}">Ag</span>`)).join("");
300
+ const weights = byCat("fontWeight").map((t) => tokenRow(t, `<span class="type" style="font-weight:${safeCss(t.value)};font-size:20px">Ag</span>`)).join("");
301
+ const lineHeights = byCat("lineHeight").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
302
+ const letterSpacings = byCat("letterSpacing").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
303
+ const borderWidths = byCat("borderWidth").map((t) => tokenRow(t, `<span class="box" style="border:${safeCss(t.value)} solid #888"></span>`)).join("");
304
+ const breakpoints = byCat("breakpoint").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
300
305
  const motions = byCat("motion").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
306
+ const opacities = byCat("opacity").map((t) => tokenRow(t, `<span class="sw" style="background:#888;opacity:${safeCss(t.value)}"></span>`)).join("");
307
+ const zIndices = byCat("zIndex").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
301
308
  const group = (title, body) => (body ? `<h2>${esc(title)}</h2><div class="grid">${body}</div>` : "");
302
309
  const reference = referenceHtml
303
310
  ? `<iframe class="ref" sandbox="allow-same-origin" srcdoc="${escAttr(referenceHtml)}" title="reference prototype"></iframe>`
@@ -351,9 +358,16 @@ ${group("Colors", colors)}
351
358
  ${group("Spacing", spaces)}
352
359
  ${group("Radius", radii)}
353
360
  ${group("Shadow", shadows)}
361
+ ${group("Border width", borderWidths)}
354
362
  ${group("Type — families", fonts)}
355
363
  ${group("Type — sizes", sizes)}
364
+ ${group("Font weight", weights)}
365
+ ${group("Line height", lineHeights)}
366
+ ${group("Letter spacing", letterSpacings)}
367
+ ${group("Breakpoints", breakpoints)}
356
368
  ${group("Motion", motions)}
369
+ ${group("Opacity", opacities)}
370
+ ${group("Z-index", zIndices)}
357
371
  </body></html>`;
358
372
  }
359
373
  function esc(s) {
@@ -366,6 +380,529 @@ function escAttr(s) {
366
380
  function safeCss(value) {
367
381
  return value.replace(/[<>"]/g, "").slice(0, 200);
368
382
  }
383
+ // ---------------------------------------------------------------------------
384
+ // Design-system reference document (human/VLM-readable, advisory)
385
+ // ---------------------------------------------------------------------------
386
+ //
387
+ // Pairs with design-contract.json (machine-readable): this is the "v1.0" visual
388
+ // spec a human or VLM can read, share, or diff. It reads the source CSS so that
389
+ // var(--x) resolves live in the browser; for non-CSS projects (Android, Flutter,
390
+ // iOS, TS module) it falls back to rendering directly from the contract tokens.
391
+ //
392
+ // NOT a gate — advisory documentation only.
393
+ export const designReferenceTool = {
394
+ name: "design_reference",
395
+ 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.",
396
+ inputSchema: {
397
+ type: "object",
398
+ properties: {
399
+ repoPath: { type: "string", description: "Project root containing sp-vise/design-contract.json. Defaults to current directory." },
400
+ title: { type: "string", description: "Design system name shown in the document heading (e.g. 'Streamly'). Defaults to 'Design System'." },
401
+ write: { type: "boolean", description: "Write sp-vise/design-reference.html (default true)." },
402
+ },
403
+ additionalProperties: false,
404
+ },
405
+ async call(input) {
406
+ const args = objectInput(input);
407
+ const repoPath = optionalStringField(args, "repoPath") ?? ".";
408
+ const title = optionalStringField(args, "title") ?? "Design System";
409
+ const write = args.write !== false;
410
+ const contract = await readDesignContract(repoPath);
411
+ if (!contract) {
412
+ return textResult({
413
+ status: "no-contract",
414
+ message: "No sp-vise/design-contract.json found. Run `vise design extract` first.",
415
+ });
416
+ }
417
+ const html = await generateDesignReference(repoPath, contract, title);
418
+ let written;
419
+ if (write) {
420
+ const target = path.join(path.resolve(repoPath), "sp-vise", "design-reference.html");
421
+ await mkdir(path.dirname(target), { recursive: true });
422
+ await writeFile(target, html, "utf8");
423
+ written = target;
424
+ }
425
+ return textResult({
426
+ status: "rendered",
427
+ written,
428
+ digest: contract.digest,
429
+ title,
430
+ note: "Advisory reference document — not an enforcement gate. Open the HTML in a browser to see the full design system.",
431
+ where: written ? `Open ${written} in a browser.` : "Not written (write=false).",
432
+ });
433
+ },
434
+ };
435
+ /**
436
+ * Generate a self-contained HTML design-system reference from the contract.
437
+ * Reads source CSS files for full var() resolution; falls back to contract tokens
438
+ * for non-CSS projects (Android XML, Flutter Dart, iOS, TS module sources).
439
+ */
440
+ export async function generateDesignReference(repoPath, contract, title) {
441
+ const root = path.resolve(repoPath);
442
+ const cssTexts = await Promise.all((contract.source?.inputs ?? []).map(async (rel) => {
443
+ try {
444
+ const info = await stat(path.join(root, rel));
445
+ if (info.size > MAX_FILE_BYTES)
446
+ return "";
447
+ return readFile(path.join(root, rel), "utf8");
448
+ }
449
+ catch {
450
+ return "";
451
+ }
452
+ }));
453
+ const tokenCss = cssTexts.join("\n");
454
+ // `ref` = how this token is referenced in a CSS style attribute.
455
+ // CSS projects: `var(--name)` so the token resolves live via the inlined :root.
456
+ // Non-CSS projects (Android/Flutter/iOS/TS module): use the concrete value directly
457
+ // since there is no :root to resolve from.
458
+ const hasCssInputs = (contract.source?.inputs ?? []).some((rel) => /\.(css|scss)$/i.test(rel));
459
+ let allTokens;
460
+ if (hasCssInputs && tokenCss.trim()) {
461
+ const contractNames = new Set(contract.tokens.map((t) => t.name).filter((n) => n !== null));
462
+ allTokens = [...tokenCss.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)].map((m) => ({
463
+ name: m[1],
464
+ value: m[2].trim(),
465
+ ref: `var(${m[1]})`,
466
+ inContract: contractNames.has(m[1]),
467
+ }));
468
+ }
469
+ else {
470
+ // Non-CSS project: render from contract tokens directly, using concrete values.
471
+ allTokens = contract.tokens
472
+ .filter((t) => t.name !== null)
473
+ .map((t) => ({ name: t.name, value: t.value, ref: safeCss(t.value), inContract: true, category: t.category }));
474
+ }
475
+ const GROUP_DEFS = [
476
+ { id: "brand", label: "Brand", kind: "color", match: (n) => n.startsWith("--color-brand") },
477
+ { id: "bg", label: "Background", kind: "color", match: (n) => /^--color-(bg|surface|overlay)/.test(n) },
478
+ { id: "text", label: "Text", kind: "color", match: (n) => n.startsWith("--color-text") },
479
+ { id: "line", label: "Lines & semantic", kind: "color", match: (n) => /^--color-(border|success|warning|danger|info)/.test(n) },
480
+ { id: "font", label: "Font families", kind: "family", match: (n) => n.startsWith("--font-") },
481
+ { id: "fs", label: "Type scale", kind: "fontsize", match: (n) => n.startsWith("--fs-") },
482
+ { id: "fw", label: "Font weight", kind: "weight", match: (n) => n.startsWith("--fw-") },
483
+ { id: "lh", label: "Line height / tracking", kind: "chip", match: (n) => /^--(lh|ls)-/.test(n) },
484
+ { id: "space", label: "Spacing", kind: "space", match: (n) => n.startsWith("--space-") },
485
+ { id: "size", label: "Sizing (controls & icons)", kind: "size", match: (n) => /^--(size|control)-/.test(n) },
486
+ { id: "radius", label: "Radius", kind: "radius", match: (n) => n.startsWith("--radius-") },
487
+ { id: "border", label: "Border width", kind: "border", match: (n) => n.startsWith("--border-width-") },
488
+ { id: "shadow", label: "Elevation", kind: "shadow", match: (n) => n.startsWith("--shadow-") },
489
+ { id: "opacity", label: "Opacity", kind: "opacity", match: (n) => n.startsWith("--opacity-") },
490
+ { id: "motion", label: "Motion", kind: "chip", match: (n) => /^--(duration|ease)-/.test(n) },
491
+ { id: "bp", label: "Breakpoints", kind: "chip", match: (n) => n.startsWith("--bp-") },
492
+ { id: "z", label: "Z-index", kind: "chip", match: (n) => n.startsWith("--z-") },
493
+ ];
494
+ // Maps contract category → rendering kind; used as fallback for non-CSS tokens
495
+ // whose names don't carry the --prefix conventions above.
496
+ const CATEGORY_TO_KIND = {
497
+ color: "color", space: "space", radius: "radius", shadow: "shadow",
498
+ fontFamily: "family", fontSize: "fontsize", motion: "chip", opacity: "opacity",
499
+ };
500
+ const CATEGORY_LABEL = {
501
+ color: "Colors", space: "Spacing", radius: "Radius", shadow: "Elevation",
502
+ fontFamily: "Font families", fontSize: "Type scale", motion: "Motion", opacity: "Opacity",
503
+ };
504
+ const used = new Set();
505
+ const grouped = GROUP_DEFS.map((g) => {
506
+ const items = allTokens.filter((t) => g.match(t.name));
507
+ items.forEach((t) => used.add(t.name));
508
+ return { id: g.id, label: g.label, kind: g.kind, items };
509
+ }).filter((g) => g.items.length > 0);
510
+ // For non-CSS tokens not matched by name-prefix above, fall back to grouping by
511
+ // the contract category field so native projects produce a properly-sectioned doc.
512
+ const unmatchedByName = allTokens.filter((t) => !used.has(t.name));
513
+ const catBuckets = new Map();
514
+ const trueUngrouped = [];
515
+ for (const t of unmatchedByName) {
516
+ const cat = t.category;
517
+ if (cat && CATEGORY_TO_KIND[cat]) {
518
+ const bucket = catBuckets.get(cat) ?? [];
519
+ bucket.push(t);
520
+ catBuckets.set(cat, bucket);
521
+ }
522
+ else {
523
+ trueUngrouped.push(t);
524
+ }
525
+ }
526
+ for (const [cat, items] of catBuckets) {
527
+ grouped.push({ id: cat, label: CATEGORY_LABEL[cat] ?? cat, kind: CATEGORY_TO_KIND[cat] ?? "chip", items });
528
+ }
529
+ if (trueUngrouped.length > 0)
530
+ grouped.push({ id: "other", label: "Other", kind: "chip", items: trueUngrouped });
531
+ const provTag = (t) => (t.inContract === false ? ` <span class="prov">not extracted</span>` : "");
532
+ function renderRefItem(t, kind) {
533
+ const n = esc(t.name);
534
+ const v = esc(t.value);
535
+ const r = t.ref;
536
+ const meta = `<div class="tk-name">${n}${provTag(t)}</div><div class="tk-val">${v}</div>`;
537
+ switch (kind) {
538
+ case "color": return `<div class="tk"><div class="sw" style="background:${r}"></div>${meta}</div>`;
539
+ case "family": return `<div class="tk wide"><div class="fam" style="font-family:${r}">${esc(title)} — The quick brown fox</div>${meta}</div>`;
540
+ case "fontsize": return `<div class="tk wide"><div class="samp" style="font-size:${r}">${esc(title)} ${v}</div>${meta}</div>`;
541
+ case "weight": return `<div class="tk wide"><div class="samp" style="font-weight:${r};font-size:20px">${esc(title)} ${v}</div>${meta}</div>`;
542
+ case "space": return `<div class="tk"><div class="bar" style="width:${r}"></div>${meta}</div>`;
543
+ case "size": return `<div class="tk"><div class="bar" style="width:${r};height:${r};max-height:48px"></div>${meta}</div>`;
544
+ case "radius": return `<div class="tk"><div class="rad" style="border-radius:${r}"></div>${meta}</div>`;
545
+ case "border": return `<div class="tk"><div class="rad" style="border:${r} solid var(--color-border-strong,#404040)"></div>${meta}</div>`;
546
+ case "opacity": return `<div class="tk"><div class="sw" style="background:var(--color-brand,#e50914);opacity:${r}"></div>${meta}</div>`;
547
+ case "shadow": return `<div class="tk"><div class="elev" style="box-shadow:${r}"></div>${meta}</div>`;
548
+ default: return `<div class="tk chip">${meta}</div>`;
549
+ }
550
+ }
551
+ function renderTokenRow(t) {
552
+ 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>`;
553
+ }
554
+ // Super-group assignment for nav + section tags.
555
+ const GROUP_SUPER = {
556
+ brand: "COLOR", bg: "COLOR", text: "COLOR", line: "COLOR",
557
+ font: "TYPOGRAPHY", fs: "TYPOGRAPHY", fw: "TYPOGRAPHY", lh: "TYPOGRAPHY",
558
+ space: "LAYOUT", size: "LAYOUT", bp: "LAYOUT",
559
+ radius: "SURFACE", border: "SURFACE", shadow: "SURFACE",
560
+ opacity: "EFFECTS", motion: "EFFECTS", z: "EFFECTS",
561
+ // category-based (native projects)
562
+ color: "COLOR", fontFamily: "TYPOGRAPHY", fontSize: "TYPOGRAPHY",
563
+ other: "OTHER",
564
+ };
565
+ const SUPER_ORDER = ["COLOR", "TYPOGRAPHY", "LAYOUT", "SURFACE", "EFFECTS", "OTHER"];
566
+ const superNav = new Map();
567
+ for (const g of grouped) {
568
+ const sg = GROUP_SUPER[g.id] ?? "OTHER";
569
+ const list = superNav.get(sg) ?? [];
570
+ list.push(g);
571
+ superNav.set(sg, list);
572
+ }
573
+ const navHtml = SUPER_ORDER.filter((sg) => superNav.has(sg))
574
+ .map((sg) => {
575
+ const items = (superNav.get(sg) ?? [])
576
+ .map((g) => {
577
+ const dot = g.kind === "color" && g.items.length > 0
578
+ ? `style="background:${g.items[0].ref}"`
579
+ : `style="background:var(--color-text-faint,var(--text-subdued,#888))"`;
580
+ return `<a class="ds-nav-item" href="#${esc(g.id)}"><span class="ds-nav-dot" ${dot}></span>${esc(g.label)}</a>`;
581
+ })
582
+ .join("\n ");
583
+ return `<div class="ds-nav-section">
584
+ <div class="ds-nav-group-label">${esc(sg)}</div>
585
+ ${items}
586
+ </div>`;
587
+ })
588
+ .join("\n ");
589
+ const sectionsHtml = grouped
590
+ .map((g) => {
591
+ const sg = GROUP_SUPER[g.id] ?? "OTHER";
592
+ const useList = g.kind === "chip";
593
+ const body = useList
594
+ ? `<div class="ds-token-list">${g.items.map((t) => renderTokenRow(t)).join("")}</div>`
595
+ : `<div class="grid">${g.items.map((t) => renderRefItem(t, g.kind)).join("")}</div>`;
596
+ return `<section id="${esc(g.id)}" class="ds-section">
597
+ <div class="ds-section-header">
598
+ <div class="ds-section-tag">${esc(sg)}</div>
599
+ <h2 class="ds-section-title">${esc(g.label)}</h2>
600
+ <div class="ds-section-count">${g.items.length} token${g.items.length === 1 ? "" : "s"}</div>
601
+ </div>
602
+ ${body}
603
+ </section>`;
604
+ })
605
+ .join("\n ");
606
+ const digestShort = esc(contract.digest.slice(0, 23));
607
+ const logoLetter = esc(title.slice(0, 1).toUpperCase());
608
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
609
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
610
+ <title>${esc(title)} — Design System</title>
611
+ <style>
612
+ ${tokenCss}
613
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
614
+ html{scroll-behavior:smooth}
615
+ body{
616
+ font-family:var(--font-body,var(--encore-body-font-stack,system-ui,sans-serif));
617
+ background:var(--color-bg,var(--background-base,var(--bg-base,#fafafa)));
618
+ color:var(--color-text,var(--text-base,var(--text-primary,#1a1a1a)));
619
+ min-height:100vh;font-size:13px;line-height:1.5;
620
+ }
621
+ .ds-layout{display:flex;min-height:100vh}
622
+
623
+ /* ── Sidebar ── */
624
+ .ds-sidebar{
625
+ position:fixed;top:0;left:0;width:240px;height:100vh;
626
+ background:var(--color-surface,var(--background-elevated-base,var(--bg-elevated,#f0f0f0)));
627
+ border-right:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
628
+ display:flex;flex-direction:column;overflow:hidden;z-index:200;
629
+ }
630
+ .ds-sidebar-header{
631
+ padding:20px 16px 16px;flex-shrink:0;
632
+ border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
633
+ }
634
+ .ds-logo{display:flex;align-items:center;gap:10px}
635
+ .ds-logo-mark{
636
+ width:32px;height:32px;border-radius:6px;flex-shrink:0;
637
+ background:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
638
+ display:flex;align-items:center;justify-content:center;
639
+ font-size:15px;font-weight:700;color:#fff;
640
+ }
641
+ .ds-logo-name{
642
+ font-size:13px;font-weight:700;line-height:1.2;
643
+ color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
644
+ }
645
+ .ds-logo-sub{
646
+ font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;margin-top:1px;
647
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
648
+ }
649
+ .ds-nav{flex:1;overflow-y:auto;padding:12px 8px;scrollbar-width:thin}
650
+ .ds-nav-section{margin-bottom:20px}
651
+ .ds-nav-group-label{
652
+ font-size:10px;font-weight:600;letter-spacing:.10em;text-transform:uppercase;
653
+ padding:0 8px;margin-bottom:4px;
654
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
655
+ }
656
+ .ds-nav-item{
657
+ display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;
658
+ font-size:13px;font-weight:500;
659
+ color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
660
+ text-decoration:none;cursor:pointer;transition:background .1s,color .1s;
661
+ }
662
+ .ds-nav-item:hover{
663
+ background:var(--color-surface-raised,var(--background-elevated-highlight,rgba(0,0,0,.06)));
664
+ color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
665
+ }
666
+ .ds-nav-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
667
+ .ds-sidebar-footer{
668
+ padding:12px 16px;flex-shrink:0;
669
+ border-top:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
670
+ font-size:11px;
671
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
672
+ }
673
+
674
+ /* ── Main ── */
675
+ .ds-main{flex:1;margin-left:240px;min-height:100vh}
676
+ .ds-topbar{
677
+ position:sticky;top:0;z-index:150;
678
+ background:var(--color-overlay,rgba(0,0,0,.55));
679
+ backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
680
+ border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
681
+ padding:10px 48px;display:flex;align-items:center;justify-content:space-between;
682
+ }
683
+ .ds-topbar-title{
684
+ font-size:13px;font-weight:600;letter-spacing:.02em;
685
+ color:var(--color-text-muted,var(--text-subdued,#888));
686
+ }
687
+ .ds-topbar-meta{
688
+ font-size:11px;font-family:ui-monospace,'SF Mono',monospace;
689
+ color:var(--color-text-faint,var(--text-subdued,#aaa));
690
+ }
691
+
692
+ /* ── Section scaffold ── */
693
+ .ds-section{
694
+ padding:48px;
695
+ border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.06)));
696
+ }
697
+ .ds-section-header{margin-bottom:32px}
698
+ .ds-section-tag{
699
+ font-size:10px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;margin-bottom:6px;
700
+ color:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
701
+ }
702
+ .ds-section-title{
703
+ font-family:var(--font-display,var(--encore-title-font-stack,inherit));
704
+ font-size:22px;font-weight:700;line-height:1.1;margin-bottom:4px;
705
+ color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
706
+ transition:color .15s;
707
+ }
708
+ .ds-section-count{
709
+ font-size:12px;
710
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
711
+ }
712
+ section:target .ds-section-title{
713
+ color:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
714
+ }
715
+
716
+ /* ── Swatch grid ── */
717
+ .grid{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end}
718
+ .tk{width:150px}
719
+ .tk.wide{width:100%}
720
+ .tk.chip{width:auto;
721
+ background:var(--color-surface,var(--background-elevated-base,rgba(0,0,0,.04)));
722
+ border:1px solid var(--color-border,rgba(0,0,0,.08));
723
+ border-radius:var(--radius-md,8px);padding:8px 12px;
724
+ }
725
+ .tk-name{
726
+ font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;margin-top:6px;
727
+ color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
728
+ }
729
+ .tk-val{
730
+ font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
731
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
732
+ }
733
+ .prov{
734
+ color:var(--color-warning,var(--essential-warning,var(--status-warning,#e8a020)));
735
+ font-size:10px;
736
+ }
737
+ .sw{height:64px;border-radius:var(--radius-md,8px);border:1px solid var(--color-border,rgba(0,0,0,.1))}
738
+ .bar{height:12px;background:var(--color-brand,var(--essential-bright-accent,#888));border-radius:3px;min-width:2px}
739
+ .rad{
740
+ width:80px;height:64px;
741
+ background:var(--color-surface-raised,var(--background-elevated-highlight,rgba(0,0,0,.06)));
742
+ border:1px solid var(--color-border-strong,var(--border-default,rgba(0,0,0,.15)));
743
+ }
744
+ .elev{
745
+ width:120px;height:64px;border-radius:var(--radius-md,8px);
746
+ background:var(--color-surface-raised,var(--background-elevated-base,#fff));
747
+ }
748
+ .samp,.fam{color:var(--color-text,var(--text-base,var(--text-primary,inherit)))}
749
+
750
+ /* ── Token list (motion / bp / z-index / lh) ── */
751
+ .ds-token-list{display:flex;flex-direction:column;max-width:680px}
752
+ .ds-token-row{
753
+ display:flex;align-items:baseline;gap:16px;padding:8px 0;
754
+ border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.06)));
755
+ }
756
+ .ds-token-row:last-child{border-bottom:none}
757
+ .ds-token-name{
758
+ font-size:12px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
759
+ color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
760
+ min-width:220px;flex-shrink:0;
761
+ }
762
+ .ds-token-pill{
763
+ font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
764
+ background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.05)));
765
+ color:var(--color-text,var(--text-base,inherit));
766
+ border-radius:4px;padding:2px 8px;
767
+ }
768
+
769
+ /* ── Components ── */
770
+ .comps{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
771
+ .btn{
772
+ border:none;cursor:pointer;font-weight:var(--fw-medium,500);
773
+ font-size:var(--fs-sm,var(--encore-text-size-smaller,13px));
774
+ padding:8px 20px;
775
+ border-radius:var(--radius-sm,var(--encore-corner-radius-base,4px));
776
+ font-family:inherit;transition:opacity .12s;
777
+ }
778
+ .btn-primary{
779
+ background:var(--color-brand,var(--essential-bright-accent,#888));
780
+ color:var(--color-text-on-brand,var(--text-bright-accent,#fff));
781
+ }
782
+ .btn-secondary{
783
+ background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.06)));
784
+ color:var(--color-text,var(--text-base,inherit));
785
+ border:1px solid var(--color-border,rgba(0,0,0,.1));
786
+ }
787
+ .card{
788
+ width:200px;height:112px;
789
+ background:var(--color-surface,var(--background-elevated-base,#f4f4f4));
790
+ border-radius:var(--radius-md,var(--encore-corner-radius-larger-2,8px));
791
+ box-shadow:var(--shadow-2,var(--encore-overlay-box-shadow,0 4px 12px rgba(0,0,0,.2)));
792
+ display:flex;align-items:flex-end;padding:12px;
793
+ border:1px solid var(--color-border,rgba(0,0,0,.08));
794
+ }
795
+ .badge{
796
+ background:var(--color-brand,var(--essential-bright-accent,#888));
797
+ color:var(--color-text-on-brand,#fff);font-size:11px;padding:2px 8px;
798
+ border-radius:var(--radius-pill,var(--encore-border-radius-rounded,999px));
799
+ }
800
+
801
+ /* ── Growth layer ── */
802
+ .growth-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}
803
+ .growth-card{
804
+ padding:16px;border-radius:var(--radius-md,8px);
805
+ background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.03)));
806
+ border:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.08)));
807
+ }
808
+ .growth-card-title{
809
+ font-size:13px;font-weight:600;margin-bottom:4px;
810
+ color:var(--color-text,var(--text-base,inherit));
811
+ }
812
+ .growth-card-desc{
813
+ font-size:12px;line-height:1.5;
814
+ color:var(--color-text-faint,var(--text-subdued,#888));
815
+ }
816
+
817
+ /* ── Footer ── */
818
+ .ds-footer{
819
+ padding:24px 48px;font-size:11px;
820
+ color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
821
+ border-top:1px solid var(--color-border,rgba(0,0,0,.06));
822
+ }
823
+ </style></head>
824
+ <body>
825
+ <div class="ds-layout">
826
+
827
+ <aside class="ds-sidebar">
828
+ <div class="ds-sidebar-header">
829
+ <div class="ds-logo">
830
+ <div class="ds-logo-mark">${logoLetter}</div>
831
+ <div>
832
+ <div class="ds-logo-name">${esc(title.toUpperCase())}</div>
833
+ <div class="ds-logo-sub">Design System</div>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ <nav class="ds-nav">
838
+ ${navHtml}
839
+ <div class="ds-nav-section">
840
+ <div class="ds-nav-group-label">REFERENCE</div>
841
+ <a class="ds-nav-item" href="#components"><span class="ds-nav-dot" style="background:var(--color-brand,var(--essential-bright-accent,#888))"></span>Components</a>
842
+ <a class="ds-nav-item" href="#growth"><span class="ds-nav-dot" style="background:var(--color-text-faint,#888)"></span>Growth layer</a>
843
+ </div>
844
+ </nav>
845
+ <div class="ds-sidebar-footer">
846
+ <div>v1.0 &middot; ${digestShort}</div>
847
+ <div style="margin-top:2px">${contract.tokens.length} tokens extracted</div>
848
+ </div>
849
+ </aside>
850
+
851
+ <div class="ds-main">
852
+ <div class="ds-topbar">
853
+ <span class="ds-topbar-title">${esc(title)} &mdash; Design Reference</span>
854
+ <span class="ds-topbar-meta">${digestShort}</span>
855
+ </div>
856
+ <main>
857
+ ${sectionsHtml}
858
+ <section id="components" class="ds-section">
859
+ <div class="ds-section-header">
860
+ <div class="ds-section-tag">REFERENCE</div>
861
+ <h2 class="ds-section-title">Component Samples</h2>
862
+ <div class="ds-section-count">built from the tokens above</div>
863
+ </div>
864
+ <div class="comps">
865
+ <button class="btn btn-primary">&#9654; Play</button>
866
+ <button class="btn btn-secondary">+ My List</button>
867
+ <span class="badge">NEW</span>
868
+ <div class="card"><span style="font-size:12px;color:var(--color-text-muted,var(--text-subdued,#888))">${esc(title)}</span></div>
869
+ </div>
870
+ </section>
871
+ <section id="growth" class="ds-section">
872
+ <div class="ds-section-header">
873
+ <div class="ds-section-tag">GROWTH LAYER</div>
874
+ <h2 class="ds-section-title">Extends This Template</h2>
875
+ <div class="ds-section-count">advisory — not an enforcement gate</div>
876
+ </div>
877
+ <div class="growth-grid">
878
+ <div class="growth-card">
879
+ <div class="growth-card-title">Component specs &#10003;</div>
880
+ <div class="growth-card-desc">Buttons, card, badge shown above. Every component is a composition of the tokens in this document.</div>
881
+ </div>
882
+ <div class="growth-card">
883
+ <div class="growth-card-title">Usage guidelines</div>
884
+ <div class="growth-card-desc">Do / don&apos;t, when-to-use, pairing rules per token group. Defined by the team, not by extraction.</div>
885
+ </div>
886
+ <div class="growth-card">
887
+ <div class="growth-card-title">Accessibility</div>
888
+ <div class="growth-card-desc">Contrast ratios, focus states, motion-reduce overrides. Requires computation against real backgrounds.</div>
889
+ </div>
890
+ </div>
891
+ <p style="margin-top:20px;font-size:12px;color:var(--color-text-faint,var(--text-subdued,#888))">
892
+ The token taxonomy above is the <strong>minimum template</strong> — deterministically extracted, never fabricated.
893
+ Components, usage guidelines, and accessibility are the advisory growth layer: they extend the source-of-truth tokens but are never gates.
894
+ </p>
895
+ </section>
896
+ </main>
897
+ <footer class="ds-footer">
898
+ Generated by Vise design-reference &mdash; advisory documentation, not an enforcement gate.
899
+ Contract digest: ${esc(contract.digest)}. Source: ${esc((contract.source?.inputs ?? []).join(", "))}.
900
+ </footer>
901
+ </div>
902
+
903
+ </div>
904
+ </body></html>`;
905
+ }
369
906
  const ADVISORY_NOTE = "Advisory only — non-blocking and NOT part of `vise check`. One-off literals (overlays, scrims, pure #fff/#000, gradients) are expected and fine; off-contract literals are review hints, not violations.";
370
907
  export const designCheckTool = {
371
908
  name: "design_check",
@@ -912,14 +1449,21 @@ function categorizeDeclaredVar(name, value) {
912
1449
  if (/radius|radii|corner|\bround/.test(n)) {
913
1450
  return "radius";
914
1451
  }
1452
+ // borderWidth: name-and-value gated, MUST come before the color branch because
1453
+ // the color regex matches /border/ — `--border-width-thin: 1px` is a length, not a color.
1454
+ if (/border-?width|stroke-?width|outline-?width/.test(n) && LENGTH.test(v)) {
1455
+ return "borderWidth";
1456
+ }
915
1457
  if (/(color|colour|bg|background|fg|foreground|surface|border|brand|primary|secondary|accent|fill|stroke|ink|text-color)/.test(n) || isColor(v)) {
916
1458
  return "color";
917
1459
  }
918
1460
  if (/(font-family|fontfamily|typeface|family)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v)) {
919
1461
  return "fontFamily";
920
1462
  }
921
- if (/(font-size|fontsize|text-size|leading|line-height)/.test(n) || /\btext-(xs|sm|base|md|lg|xl|\dxl|\d)\b/.test(n)) {
922
- return "fontSize"; // incl. the Tailwind text-scale convention (--text-sm/base/lg)
1463
+ // fontSize: broaden to include the common --fs-* naming convention (e.g. --fs-sm: 14px).
1464
+ // 'leading' and 'line-height' are moved to lineHeight below.
1465
+ if (/(font-size|fontsize|text-size)/.test(n) || /\btext-(xs|sm|base|md|lg|xl|\dxl|\d)\b/.test(n) || /^--fs-\w/.test(n)) {
1466
+ return "fontSize"; // incl. the Tailwind text-scale convention (--text-sm/base/lg) and --fs-* shorthand
923
1467
  }
924
1468
  // Motion is value-gated: only time/easing values become motion tokens. This
925
1469
  // stops substring matches like "increase"/"decrease" (which contain "ease")
@@ -935,6 +1479,30 @@ function categorizeDeclaredVar(name, value) {
935
1479
  if (/(font|type)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v) && !TIME.test(v)) {
936
1480
  return "fontFamily";
937
1481
  }
1482
+ // fontWeight: name-gated (bare integers collide with z-index). Value must be a
1483
+ // 3-digit integer in the valid CSS font-weight range 100–950.
1484
+ if (/\bfw\b|font-?weight|\bweight\b/.test(n)) {
1485
+ const num = Number(v);
1486
+ if (/^\d{3}$/.test(v) && num >= 100 && num <= 950) {
1487
+ return "fontWeight";
1488
+ }
1489
+ }
1490
+ // lineHeight: name-gated + unitless value (1.1, 1.4, 1.6 etc.). Captures both
1491
+ // 'lh-*' shorthand and 'leading-*'/'line-height-*' naming conventions.
1492
+ if (/\blh\b|leading|line-?height|lineheight/.test(n) && /^-?[0-9]*\.?[0-9]+$/.test(v) && !LENGTH.test(v)) {
1493
+ return "lineHeight";
1494
+ }
1495
+ // letterSpacing: name-gated + any CSS length. Must come before the generic
1496
+ // LENGTH→space fallback so --ls-* tokens aren't misfiled as spacing.
1497
+ if (/\bls\b|letter-?spacing|letterspacing|\btracking\b/.test(n) && LENGTH.test(v)) {
1498
+ return "letterSpacing";
1499
+ }
1500
+ // breakpoint: name-gated + length. Must precede the LENGTH fallback. The
1501
+ // contract already captures @media breakpoints in contract.breakpoints[]; this
1502
+ // catches explicit --bp-* / --breakpoint-* custom-property tokens.
1503
+ if (/\bbp\b|break-?point|viewport-?width|screen-?size/.test(n) && LENGTH.test(v)) {
1504
+ return "breakpoint";
1505
+ }
938
1506
  if (/(space|spacing|gap|size|gutter|inset|pad|margin)/.test(n) && LENGTH.test(v)) {
939
1507
  return "space";
940
1508
  }
@@ -944,6 +1512,16 @@ function categorizeDeclaredVar(name, value) {
944
1512
  if (LENGTH.test(v)) {
945
1513
  return "space";
946
1514
  }
1515
+ // Opacity: declared-only (a bare 0.4 in code could be line-height/scale/anything — never infer).
1516
+ if (/opacity/.test(n) && /^(0(\.\d+)?|\.\d+|1(\.0*)?)$/.test(v)) {
1517
+ return "opacity";
1518
+ }
1519
+ // zIndex: name-gated strictly; integers collide with fontWeight so the name is
1520
+ // the only reliable signal. Use z-index / z-idx or the common --z-* prefix.
1521
+ // Note: bare `/z/` would catch --zoom, --size, etc. — do NOT relax this guard.
1522
+ if ((/z-?index|z-?idx/.test(n) || /^--z-\w/.test(n)) && /^-?\d+$/.test(v)) {
1523
+ return "zIndex";
1524
+ }
947
1525
  return null;
948
1526
  }
949
1527
  /** Emit zero or more (category, normalizedValue) observations from one literal declaration. */
@@ -1118,7 +1696,7 @@ function friendlyComponentName(cls) {
1118
1696
  // ---------------------------------------------------------------------------
1119
1697
  // Sorting, summarizing, digest
1120
1698
  // ---------------------------------------------------------------------------
1121
- const CATEGORY_ORDER = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "motion"];
1699
+ const CATEGORY_ORDER = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "borderWidth", "breakpoint", "motion", "opacity", "zIndex"];
1122
1700
  function sortTokens(tokens) {
1123
1701
  return [...tokens].sort((a, b) => {
1124
1702
  const cat = CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
@@ -1435,8 +2013,11 @@ function categorizeTokenModuleValue(keyPath, value) {
1435
2013
  return null; // reference / computed — not a concrete token
1436
2014
  }
1437
2015
  const key = keyPath.toLowerCase();
1438
- if (/screen|breakpoint|media|z-?index|opacity|weight/.test(key)) {
1439
- return null; // not a design token we model (or handled as breakpoints elsewhere)
2016
+ // These categories are CSS-only (declared custom properties): the name-gating
2017
+ // that makes them safe against false positives only works in CSS. Module/native
2018
+ // extraction skips them rather than risk inventing wrong tokens.
2019
+ if (/screen|breakpoint|media|z-?index|opacity|weight|\blh\b|leading|line-?height|letter-?spacing/.test(key)) {
2020
+ return null;
1440
2021
  }
1441
2022
  // Colors: unambiguous from the value alone.
1442
2023
  if (isColor(v)) {
@@ -499,8 +499,16 @@ async function validateEnvSecretHygiene(root, platform, sourceContent) {
499
499
  const findings = [];
500
500
  if (platform === "typescript") {
501
501
  const envSecretFiles = await committedEnvSecretFiles(root);
502
- for (const file of envSecretFiles) {
503
- findings.push(finding("typescript.secret.committed-env", "warning", "A local env file appears to contain a social.plus secret.", file, "Do not commit .env files with real API keys. Commit a placeholder .env.example and keep local env files ignored."));
502
+ if (envSecretFiles.length > 0) {
503
+ // Only fire when the secret-bearing env file is NOT gitignored. A gitignored local `.env` (the
504
+ // exact practice this rule's own recommendation endorses) won't be committed, so flagging it is
505
+ // a false positive — committed-env is about a `.env` that WILL be committed (no .gitignore cover).
506
+ const gitignore = await readIfExists(path.join(root, ".gitignore"));
507
+ if (!gitignore || !gitignoreIgnoresEnvFiles(gitignore)) {
508
+ for (const file of envSecretFiles) {
509
+ findings.push(finding("typescript.secret.committed-env", "warning", "A local env file appears to contain a social.plus secret.", file, "Do not commit .env files with real API keys. Commit a placeholder .env.example and keep local env files ignored."));
510
+ }
511
+ }
504
512
  }
505
513
  }
506
514
  if (platform === "typescript" || platform === "react-native") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
package/rules/feed.yaml CHANGED
@@ -1469,10 +1469,11 @@
1469
1469
  },
1470
1470
  {
1471
1471
  "id": "typescript.reactions.configured-name-used",
1472
- "version": 1,
1472
+ "version": 2,
1473
1473
  "title": "TypeScript reaction name matches console config",
1474
1474
  "severity": "warning",
1475
- "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1475
+ "advisory": true,
1476
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors. Advisory: Vise cannot verify tenant console config, so this surfaces as informational — it never blocks vise check.",
1476
1477
  "applies_when": {
1477
1478
  "platforms": [
1478
1479
  "typescript"
@@ -1506,7 +1507,8 @@
1506
1507
  },
1507
1508
  {
1508
1509
  "id": "react-native.reactions.configured-name-used",
1509
- "version": 1,
1510
+ "version": 2,
1511
+ "advisory": true,
1510
1512
  "title": "React Native reaction name matches console config",
1511
1513
  "severity": "warning",
1512
1514
  "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
@@ -1543,7 +1545,8 @@
1543
1545
  },
1544
1546
  {
1545
1547
  "id": "android.reactions.configured-name-used",
1546
- "version": 1,
1548
+ "version": 2,
1549
+ "advisory": true,
1547
1550
  "title": "Android reaction name matches console config",
1548
1551
  "severity": "warning",
1549
1552
  "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
@@ -1580,7 +1583,8 @@
1580
1583
  },
1581
1584
  {
1582
1585
  "id": "flutter.reactions.configured-name-used",
1583
- "version": 1,
1586
+ "version": 2,
1587
+ "advisory": true,
1584
1588
  "title": "Flutter reaction name matches console config",
1585
1589
  "severity": "warning",
1586
1590
  "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
@@ -1617,7 +1621,8 @@
1617
1621
  },
1618
1622
  {
1619
1623
  "id": "ios.reactions.configured-name-used",
1620
- "version": 1,
1624
+ "version": 2,
1625
+ "advisory": true,
1621
1626
  "title": "iOS reaction name matches console config",
1622
1627
  "severity": "warning",
1623
1628
  "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",