@amityco/social-plus-vise 0.12.3 → 0.12.5
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 +1 -0
- package/dist/server.js +18 -3
- package/dist/tools/compliance.js +19 -8
- package/dist/tools/design.js +248 -1
- package/package.json +1 -1
- package/rules/feed.yaml +11 -6
package/README.md
CHANGED
|
@@ -251,6 +251,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
|
|
|
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
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 |
|
|
254
|
+
| `vise design init-tokens [path] [--force]` | Scaffold `src/styles/social-plus-tokens.css` — the dedicated, customer-editable token file for social.plus features. **Greenfield:** neutral defaults (full `--sp-*` token set). **Brownfield:** seeded from your existing concrete tokens. Idempotent — never overwrites an existing file (use `--force` to override). After editing, run `vise design extract --from-project` to refresh the contract. `design_init_tokens` |
|
|
254
255
|
|
|
255
256
|
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).
|
|
256
257
|
|
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, designReferenceTool } from "./tools/design.js";
|
|
10
|
+
import { designCheckTool, designExtractTool, designInitTokensTool, 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";
|
|
@@ -38,6 +38,7 @@ const tools = new Map([
|
|
|
38
38
|
designCheckTool,
|
|
39
39
|
designPreviewTool,
|
|
40
40
|
designReferenceTool,
|
|
41
|
+
designInitTokensTool,
|
|
41
42
|
].map((tool) => [tool.name, tool]));
|
|
42
43
|
const bundledSkillName = "social-plus-vise";
|
|
43
44
|
const cliResult = await handleCli(process.argv.slice(2));
|
|
@@ -305,7 +306,15 @@ async function handleCli(args) {
|
|
|
305
306
|
});
|
|
306
307
|
return "exit";
|
|
307
308
|
}
|
|
308
|
-
|
|
309
|
+
if (sub === "init-tokens") {
|
|
310
|
+
assertOnlyKnownFlags(subArgs, ["force"], "design init-tokens");
|
|
311
|
+
await printToolResult(designInitTokensTool, {
|
|
312
|
+
repoPath: positionalRepoPath(subArgs),
|
|
313
|
+
force: hasFlag(subArgs, "force"),
|
|
314
|
+
});
|
|
315
|
+
return "exit";
|
|
316
|
+
}
|
|
317
|
+
console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", "reference", or "init-tokens".`);
|
|
309
318
|
process.exitCode = 1;
|
|
310
319
|
return "exit";
|
|
311
320
|
}
|
|
@@ -515,6 +524,7 @@ Usage:
|
|
|
515
524
|
vise design check [repoPath]
|
|
516
525
|
vise design preview [repoPath] [--reference <prototypePath>]
|
|
517
526
|
vise design reference [repoPath] [--title "Streamly"] [--no-write]
|
|
527
|
+
vise design init-tokens [repoPath] [--force]
|
|
518
528
|
|
|
519
529
|
extract Build a graded design contract and write it to sp-vise/design-contract.json.
|
|
520
530
|
Declared CSS custom properties become exact tokens; repeated literal values
|
|
@@ -535,7 +545,12 @@ preview Write a self-contained sp-vise/design-preview.html: the contract's to
|
|
|
535
545
|
reference Write a self-contained sp-vise/design-reference.html: human/VLM-readable
|
|
536
546
|
design-system spec (token swatches, type samples, component demos, growth-layer
|
|
537
547
|
summary). Pairs with the machine-readable design-contract.json. Use --title to
|
|
538
|
-
set the design system name. Advisory — not an enforcement gate
|
|
548
|
+
set the design system name. Advisory — not an enforcement gate.
|
|
549
|
+
init-tokens Scaffold src/styles/social-plus-tokens.css in the customer's project —
|
|
550
|
+
the dedicated editable token file for social.plus features. Greenfield:
|
|
551
|
+
neutral defaults. Brownfield (existing tokens found): seeded from their
|
|
552
|
+
concrete values. Idempotent (never overwrites); use --force to overwrite.
|
|
553
|
+
After editing, run vise design extract --from-project to refresh the contract.`;
|
|
539
554
|
}
|
|
540
555
|
return `${packageName}
|
|
541
556
|
|
package/dist/tools/compliance.js
CHANGED
|
@@ -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:
|
|
366
|
-
reason: rule.
|
|
367
|
-
? "
|
|
368
|
-
:
|
|
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:
|
|
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 =
|
|
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 (
|
|
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.
|
package/dist/tools/design.js
CHANGED
|
@@ -157,7 +157,16 @@ export async function extractDesignContractFromProject(repoPath) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
inputs.sort();
|
|
160
|
-
|
|
160
|
+
// Hash each source file so design check can detect staleness without re-extracting.
|
|
161
|
+
const input_digests = {};
|
|
162
|
+
for (const rel of inputs) {
|
|
163
|
+
try {
|
|
164
|
+
const content = await readFile(path.join(root, rel), "utf8");
|
|
165
|
+
input_digests[rel] = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
166
|
+
}
|
|
167
|
+
catch { /* file read already succeeded above; defensive only */ }
|
|
168
|
+
}
|
|
169
|
+
return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
|
|
161
170
|
}
|
|
162
171
|
export async function writeDesignContract(repoPath, contract) {
|
|
163
172
|
const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
|
|
@@ -381,6 +390,215 @@ function safeCss(value) {
|
|
|
381
390
|
return value.replace(/[<>"]/g, "").slice(0, 200);
|
|
382
391
|
}
|
|
383
392
|
// ---------------------------------------------------------------------------
|
|
393
|
+
// Social-plus token scaffold (vise design init-tokens)
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
//
|
|
396
|
+
// Creates a dedicated `src/styles/social-plus-tokens.css` in the customer's
|
|
397
|
+
// project — the single editable source for social.plus feature styling.
|
|
398
|
+
// The contract always points at this file; customers edit it freely without
|
|
399
|
+
// needing an AI agent. Design check detects changes and prompts re-extract.
|
|
400
|
+
//
|
|
401
|
+
// Two cases:
|
|
402
|
+
// Greenfield (no existing design system): scaffold Option-B neutral defaults.
|
|
403
|
+
// Brownfield (existing tokens found): seed from their concrete values.
|
|
404
|
+
// Already exists: leave it untouched (idempotent — never clobbers edits).
|
|
405
|
+
/** Relative path within the customer's project where sp tokens live. */
|
|
406
|
+
export const SP_TOKENS_PATH = "src/styles/social-plus-tokens.css";
|
|
407
|
+
/** Option-B neutral default — a clean, adaptive light-mode system using system
|
|
408
|
+
* fonts. All token names use `--sp-` prefix to avoid collision with the
|
|
409
|
+
* customer's own design system. Will be replaced with the official social.plus
|
|
410
|
+
* palette (Option A) once that palette is finalised. */
|
|
411
|
+
export const NEUTRAL_SP_TOKENS_DEFAULT = `/* social-plus-tokens.css — social.plus feature design system.
|
|
412
|
+
* This file controls the look of all social.plus features in your app.
|
|
413
|
+
* Edit freely. Run: vise design extract --from-project . to refresh the contract.
|
|
414
|
+
* NOTE: design check scans the whole project; the main-app palette appearing as
|
|
415
|
+
* "off-contract" is expected and advisory only — not a failure. */
|
|
416
|
+
:root {
|
|
417
|
+
/* ── Brand / interactive ───────────────────────────────── */
|
|
418
|
+
--sp-color-brand: #1054DE;
|
|
419
|
+
--sp-color-brand-hover: #0D47C5;
|
|
420
|
+
--sp-color-brand-subtle: #EEF3FF;
|
|
421
|
+
--sp-color-brand-text: #FFFFFF;
|
|
422
|
+
|
|
423
|
+
/* ── Backgrounds ───────────────────────────────────────── */
|
|
424
|
+
--sp-color-bg: #FFFFFF;
|
|
425
|
+
--sp-color-surface: #F8F9FA;
|
|
426
|
+
--sp-color-surface-raised: #FFFFFF;
|
|
427
|
+
--sp-color-surface-hover: #F0F2F4;
|
|
428
|
+
--sp-color-overlay: rgba(0, 0, 0, 0.5);
|
|
429
|
+
|
|
430
|
+
/* ── Text ──────────────────────────────────────────────── */
|
|
431
|
+
--sp-color-text: #0D1017;
|
|
432
|
+
--sp-color-text-muted: #5C6370;
|
|
433
|
+
--sp-color-text-faint: #9AA0AD;
|
|
434
|
+
--sp-color-text-disabled: #C1C7CE;
|
|
435
|
+
--sp-color-text-on-brand: #FFFFFF;
|
|
436
|
+
|
|
437
|
+
/* ── Semantic ──────────────────────────────────────────── */
|
|
438
|
+
--sp-color-success: #1FAF64;
|
|
439
|
+
--sp-color-warning: #F59E0B;
|
|
440
|
+
--sp-color-error: #EF4444;
|
|
441
|
+
--sp-color-info: #3B82F6;
|
|
442
|
+
|
|
443
|
+
/* ── Border ────────────────────────────────────────────── */
|
|
444
|
+
--sp-color-border: #E5E7EB;
|
|
445
|
+
--sp-color-border-strong: #D1D5DB;
|
|
446
|
+
|
|
447
|
+
/* ── Typography ────────────────────────────────────────── */
|
|
448
|
+
--sp-font-body: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
449
|
+
--sp-font-display: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
450
|
+
--sp-fs-xs: 12px;
|
|
451
|
+
--sp-fs-sm: 14px;
|
|
452
|
+
--sp-fs-md: 16px;
|
|
453
|
+
--sp-fs-lg: 20px;
|
|
454
|
+
--sp-fs-xl: 24px;
|
|
455
|
+
--sp-fs-2xl: 32px;
|
|
456
|
+
--sp-fw-regular: 400;
|
|
457
|
+
--sp-fw-medium: 500;
|
|
458
|
+
--sp-fw-bold: 700;
|
|
459
|
+
--sp-lh-tight: 1.2;
|
|
460
|
+
--sp-lh-normal: 1.5;
|
|
461
|
+
--sp-lh-relaxed: 1.7;
|
|
462
|
+
--sp-ls-tight: -0.01em;
|
|
463
|
+
--sp-ls-wide: 0.04em;
|
|
464
|
+
|
|
465
|
+
/* ── Spacing ───────────────────────────────────────────── */
|
|
466
|
+
--sp-space-1: 4px;
|
|
467
|
+
--sp-space-2: 8px;
|
|
468
|
+
--sp-space-3: 12px;
|
|
469
|
+
--sp-space-4: 16px;
|
|
470
|
+
--sp-space-5: 24px;
|
|
471
|
+
--sp-space-6: 32px;
|
|
472
|
+
--sp-space-8: 48px;
|
|
473
|
+
--sp-space-10: 64px;
|
|
474
|
+
|
|
475
|
+
/* ── Radius ────────────────────────────────────────────── */
|
|
476
|
+
--sp-radius-sm: 6px;
|
|
477
|
+
--sp-radius-md: 10px;
|
|
478
|
+
--sp-radius-lg: 16px;
|
|
479
|
+
--sp-radius-xl: 24px;
|
|
480
|
+
--sp-radius-pill: 999px;
|
|
481
|
+
|
|
482
|
+
/* ── Border width ──────────────────────────────────────── */
|
|
483
|
+
--sp-border-width-thin: 1px;
|
|
484
|
+
--sp-border-width-base: 2px;
|
|
485
|
+
--sp-border-width-thick: 4px;
|
|
486
|
+
|
|
487
|
+
/* ── Elevation ─────────────────────────────────────────── */
|
|
488
|
+
--sp-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
489
|
+
--sp-shadow-2: 0 4px 16px rgba(0, 0, 0, 0.10);
|
|
490
|
+
--sp-shadow-3: 0 16px 40px rgba(0, 0, 0, 0.14);
|
|
491
|
+
|
|
492
|
+
/* ── Opacity ───────────────────────────────────────────── */
|
|
493
|
+
--sp-opacity-disabled: 0.4;
|
|
494
|
+
--sp-opacity-muted: 0.7;
|
|
495
|
+
|
|
496
|
+
/* ── Motion ────────────────────────────────────────────── */
|
|
497
|
+
--sp-duration-fast: 120ms;
|
|
498
|
+
--sp-duration-base: 200ms;
|
|
499
|
+
--sp-duration-slow: 400ms;
|
|
500
|
+
--sp-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
501
|
+
--sp-ease-emphasized:cubic-bezier(0.2, 0, 0, 1);
|
|
502
|
+
|
|
503
|
+
/* ── Sizing ────────────────────────────────────────────── */
|
|
504
|
+
--sp-size-icon-sm: 16px;
|
|
505
|
+
--sp-size-icon-md: 24px;
|
|
506
|
+
--sp-size-icon-lg: 32px;
|
|
507
|
+
--sp-control-height-sm: 32px;
|
|
508
|
+
--sp-control-height-md: 40px;
|
|
509
|
+
--sp-control-height-lg: 48px;
|
|
510
|
+
|
|
511
|
+
/* ── Z-index ───────────────────────────────────────────── */
|
|
512
|
+
--sp-z-nav: 10;
|
|
513
|
+
--sp-z-dropdown: 100;
|
|
514
|
+
--sp-z-modal: 1000;
|
|
515
|
+
--sp-z-toast: 2000;
|
|
516
|
+
|
|
517
|
+
/* ── Breakpoints ───────────────────────────────────────── */
|
|
518
|
+
--sp-bp-sm: 640px;
|
|
519
|
+
--sp-bp-md: 768px;
|
|
520
|
+
--sp-bp-lg: 1024px;
|
|
521
|
+
--sp-bp-xl: 1280px;
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
export const designInitTokensTool = {
|
|
525
|
+
name: "design_init_tokens",
|
|
526
|
+
description: "Scaffold src/styles/social-plus-tokens.css in the customer's project — the dedicated, customer-editable token file for social.plus features. Greenfield: neutral defaults. Brownfield (existing tokens found): seed from their concrete values. Idempotent: never overwrites an existing file.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
repoPath: { type: "string", description: "Project root. Defaults to the current directory." },
|
|
531
|
+
force: { type: "boolean", description: "Overwrite existing social-plus-tokens.css (default false — never clobbers)." },
|
|
532
|
+
},
|
|
533
|
+
additionalProperties: false,
|
|
534
|
+
},
|
|
535
|
+
async call(input) {
|
|
536
|
+
const args = objectInput(input);
|
|
537
|
+
const repoPath = optionalStringField(args, "repoPath") ?? ".";
|
|
538
|
+
const force = args.force === true;
|
|
539
|
+
return textResult(await initSpTokens(repoPath, force));
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
export async function initSpTokens(repoPath, force = false) {
|
|
543
|
+
const root = path.resolve(repoPath);
|
|
544
|
+
const target = path.join(root, SP_TOKENS_PATH);
|
|
545
|
+
// Idempotent: don't overwrite unless forced.
|
|
546
|
+
try {
|
|
547
|
+
await stat(target);
|
|
548
|
+
if (!force) {
|
|
549
|
+
return {
|
|
550
|
+
status: "exists",
|
|
551
|
+
file: target,
|
|
552
|
+
message: `${SP_TOKENS_PATH} already exists — skipping. Use --force to overwrite.`,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch { /* file doesn't exist — proceed to scaffold */ }
|
|
557
|
+
// Try brownfield seeding: extract concrete token values from the existing project.
|
|
558
|
+
const existingContract = await extractDesignContractFromProject(root);
|
|
559
|
+
const hasBrownfieldTokens = existingContract.tokens.length > 0;
|
|
560
|
+
let css;
|
|
561
|
+
let seededFrom;
|
|
562
|
+
if (hasBrownfieldTokens) {
|
|
563
|
+
// Seed from their existing concrete values, namespaced as --sp-*.
|
|
564
|
+
const lines = [
|
|
565
|
+
`/* social-plus-tokens.css — social.plus feature design system.`,
|
|
566
|
+
` * Seeded from your existing design tokens on ${new Date().toISOString().slice(0, 10)}.`,
|
|
567
|
+
` * Edit freely to customize social.plus features independently from your main app.`,
|
|
568
|
+
` * Run: vise design extract --from-project . to refresh the contract. */`,
|
|
569
|
+
`:root {`,
|
|
570
|
+
];
|
|
571
|
+
const categories = [...new Set(existingContract.tokens.map((t) => t.category))];
|
|
572
|
+
for (const cat of categories) {
|
|
573
|
+
const tokensInCat = existingContract.tokens.filter((t) => t.category === cat && t.name);
|
|
574
|
+
if (tokensInCat.length === 0)
|
|
575
|
+
continue;
|
|
576
|
+
lines.push(``, ` /* ── ${cat} ──────────────────────────────────────────── */`);
|
|
577
|
+
for (const t of tokensInCat) {
|
|
578
|
+
const spName = `--sp-${t.name.replace(/^--/, "")}`;
|
|
579
|
+
lines.push(` ${spName}: ${t.value}; /* seeded from ${t.name} */`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
lines.push(`}`);
|
|
583
|
+
css = lines.join("\n") + "\n";
|
|
584
|
+
seededFrom = existingContract.source.inputs;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// Greenfield: neutral Option-B defaults.
|
|
588
|
+
css = NEUTRAL_SP_TOKENS_DEFAULT;
|
|
589
|
+
}
|
|
590
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
591
|
+
await writeFile(target, css, "utf8");
|
|
592
|
+
return {
|
|
593
|
+
status: hasBrownfieldTokens ? "seeded" : "scaffolded",
|
|
594
|
+
file: target,
|
|
595
|
+
...(seededFrom ? { seeded_from: seededFrom } : {}),
|
|
596
|
+
message: hasBrownfieldTokens
|
|
597
|
+
? `Seeded ${SP_TOKENS_PATH} from your existing design tokens. Edit it to customize social.plus features, then run vise design extract --from-project .`
|
|
598
|
+
: `Scaffolded ${SP_TOKENS_PATH} with neutral defaults. Fill in your colors, then run vise design extract --from-project .`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
384
602
|
// Design-system reference document (human/VLM-readable, advisory)
|
|
385
603
|
// ---------------------------------------------------------------------------
|
|
386
604
|
//
|
|
@@ -927,6 +1145,9 @@ export async function runDesignCheck(repoPath) {
|
|
|
927
1145
|
note: ADVISORY_NOTE,
|
|
928
1146
|
};
|
|
929
1147
|
}
|
|
1148
|
+
// Freshness check: compare source file content hashes to those recorded at extract time.
|
|
1149
|
+
// Advisory only — never blocks, just surfaces a nudge to re-extract.
|
|
1150
|
+
const staleContract = await checkContractFreshness(repoRoot, contract);
|
|
930
1151
|
const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => SCAN_EXTS.has(path.extname(file).toLowerCase()));
|
|
931
1152
|
if (files.length === 0) {
|
|
932
1153
|
return { status: "no-sources", message: "No UI source files found to check against the contract.", contract: contractSummary(contract), note: ADVISORY_NOTE };
|
|
@@ -1018,9 +1239,35 @@ export async function runDesignCheck(repoPath) {
|
|
|
1018
1239
|
count: undefinedRefs.length,
|
|
1019
1240
|
sample: undefinedRefs.slice(0, OFF_CONTRACT_SAMPLE),
|
|
1020
1241
|
},
|
|
1242
|
+
...(staleContract ? { staleContract } : {}),
|
|
1021
1243
|
note: ADVISORY_NOTE,
|
|
1022
1244
|
};
|
|
1023
1245
|
}
|
|
1246
|
+
/** Compare source.inputs file content against hashes recorded at extract time.
|
|
1247
|
+
* Returns null if the contract is fresh or has no recorded digests. */
|
|
1248
|
+
async function checkContractFreshness(repoRoot, contract) {
|
|
1249
|
+
const recorded = contract.source?.input_digests;
|
|
1250
|
+
if (!recorded || Object.keys(recorded).length === 0)
|
|
1251
|
+
return undefined;
|
|
1252
|
+
const changed = [];
|
|
1253
|
+
for (const [rel, storedDigest] of Object.entries(recorded)) {
|
|
1254
|
+
try {
|
|
1255
|
+
const content = await readFile(path.join(repoRoot, rel), "utf8");
|
|
1256
|
+
const currentDigest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
1257
|
+
if (currentDigest !== storedDigest)
|
|
1258
|
+
changed.push(rel);
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
changed.push(rel); // file deleted or unreadable — also stale
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (changed.length === 0)
|
|
1265
|
+
return undefined;
|
|
1266
|
+
return {
|
|
1267
|
+
changedFiles: changed,
|
|
1268
|
+
hint: `Run \`vise design extract --from-project\` to refresh the contract against the updated file(s).`,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1024
1271
|
function dedupeByToken(refs) {
|
|
1025
1272
|
const seen = new Set();
|
|
1026
1273
|
const out = [];
|
package/package.json
CHANGED
package/rules/feed.yaml
CHANGED
|
@@ -1469,10 +1469,11 @@
|
|
|
1469
1469
|
},
|
|
1470
1470
|
{
|
|
1471
1471
|
"id": "typescript.reactions.configured-name-used",
|
|
1472
|
-
"version":
|
|
1472
|
+
"version": 2,
|
|
1473
1473
|
"title": "TypeScript reaction name matches console config",
|
|
1474
1474
|
"severity": "warning",
|
|
1475
|
-
"
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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.",
|