@amityco/social-plus-vise 0.13.0 → 0.14.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 +13 -0
- package/dist/tools/design.js +409 -0
- package/dist/tools/integration.js +27 -5
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.14.0 — 2026-06-03
|
|
8
|
+
|
|
9
|
+
**Theme:** DesignBuildBrief — plan-time, grounded UI-building guidance for coding agents (advisory).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **DesignBuildBrief in `vise plan`** (`designContract.brief`): conservative semantic token roles inferred from token NAMES only (noun-first compounds — `--text-primary` binds `textPrimary`, never `primaryAction`; value-based inference is forbidden), token-derived component hints (card/button/input) with explicit absent variants, and grounded do/avoid lines — every line cites the declared tokens/roles it derives from, and an ungrounded line is structurally impossible. For `add-feed` / `add-chat` outcomes the brief carries a conditional **outcome recipe** whose items only reference roles that were actually inferred. Generated at plan time — never persisted to `sp-vise/`, never part of any digest.
|
|
13
|
+
- **Non-blocking `primary_action_token` intake question** when a design contract exists for a feed/chat outcome but no primary-action token was confidently identified — the agent asks instead of guessing.
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Advisory only; never gates `vise check`. **No design-conformance improvement is claimed for the brief yet** — a pre-registered ablation (`benchmarks/brief-ablation/PROTOCOL.md`: 0.13.0 vs 0.14.0, same contract, n=3, Cursor/Composer 2.5) measures its effect; documentation claims will follow the measurement.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
7
20
|
## 0.13.0 — 2026-06-03
|
|
8
21
|
|
|
9
22
|
**Theme:** Deterministic-gate soundness, CLI reliability, and post-rebrand coherence (driven by two repo reviews).
|
package/dist/tools/design.js
CHANGED
|
@@ -2323,3 +2323,412 @@ function stableStringify(value) {
|
|
|
2323
2323
|
}
|
|
2324
2324
|
return JSON.stringify(value);
|
|
2325
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Structural grounding helper: builds a BriefLine and throws a TypeError at
|
|
2328
|
+
* construction time if groundedIn is empty. This makes the grounding invariant
|
|
2329
|
+
* structural — an ungrounded line is impossible rather than a runtime surprise.
|
|
2330
|
+
*/
|
|
2331
|
+
function line(text, groundedIn, confidence = "high") {
|
|
2332
|
+
if (groundedIn.length === 0) {
|
|
2333
|
+
throw new TypeError(`BriefLine created with empty groundedIn: "${text}"`);
|
|
2334
|
+
}
|
|
2335
|
+
return { text, groundedIn, confidence };
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Per-role keyword rules in spec-defined order.
|
|
2339
|
+
* Each entry: [pattern, role, confidence].
|
|
2340
|
+
* Rules are applied with first-match-wins semantics.
|
|
2341
|
+
* Compound rules (muted+text, muted+bg/surface) must precede their plain
|
|
2342
|
+
* counterparts so "--color-text-muted" resolves to textSecondary, not textPrimary.
|
|
2343
|
+
*/
|
|
2344
|
+
const ROLE_RULES = [
|
|
2345
|
+
// Compound rules — must precede their plain counterparts
|
|
2346
|
+
{
|
|
2347
|
+
test: (n) => /muted/.test(n) && /\btext\b|foreground|fg/.test(n),
|
|
2348
|
+
role: "textSecondary",
|
|
2349
|
+
confidence: "medium",
|
|
2350
|
+
reason: "name contains 'muted' and 'text'/'foreground'/'fg'",
|
|
2351
|
+
},
|
|
2352
|
+
{
|
|
2353
|
+
test: (n) => /muted/.test(n) && /\bbg\b|surface|background/.test(n),
|
|
2354
|
+
role: "surfaceMuted",
|
|
2355
|
+
confidence: "medium",
|
|
2356
|
+
reason: "name contains 'muted' and 'bg'/'surface'/'background'",
|
|
2357
|
+
},
|
|
2358
|
+
// Noun-first compounds — in real design systems the NOUN keyword (text/surface/
|
|
2359
|
+
// bg/border) sets the role family and primary/secondary act as modifiers within
|
|
2360
|
+
// it: "--text-primary" is the primary BODY-TEXT color, not the action color.
|
|
2361
|
+
// These must precede the plain primary/secondary rules below, or first-match-wins
|
|
2362
|
+
// would misbind some of the most common token names in the wild.
|
|
2363
|
+
{
|
|
2364
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n) && /\bprimary\b/.test(n),
|
|
2365
|
+
role: "textPrimary",
|
|
2366
|
+
confidence: "high",
|
|
2367
|
+
reason: "name contains 'text'/'foreground'/'fg' with 'primary' — the noun keyword sets the role family",
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n) && /\bsecondary\b/.test(n),
|
|
2371
|
+
role: "textSecondary",
|
|
2372
|
+
confidence: "high",
|
|
2373
|
+
reason: "name contains 'text'/'foreground'/'fg' with 'secondary' — the noun keyword sets the role family",
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
test: (n) => /surface|background|\bbg\b/.test(n) && /\bprimary\b/.test(n),
|
|
2377
|
+
role: "surface",
|
|
2378
|
+
confidence: "high",
|
|
2379
|
+
reason: "name contains 'surface'/'background'/'bg' with 'primary' — a primary surface, not an action color",
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
test: (n) => /surface|background|\bbg\b/.test(n) && /\bsecondary\b/.test(n),
|
|
2383
|
+
role: "surfaceMuted",
|
|
2384
|
+
confidence: "medium",
|
|
2385
|
+
reason: "name contains 'surface'/'background'/'bg' with 'secondary' — a secondary surface",
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
test: (n) => /\bborder\b|\boutline\b|\bdivider\b/.test(n) && (/\bprimary\b/.test(n) || /\bsecondary\b/.test(n)),
|
|
2389
|
+
role: "border",
|
|
2390
|
+
confidence: "medium",
|
|
2391
|
+
reason: "name contains a border keyword with a primary/secondary modifier — the noun keyword sets the role family",
|
|
2392
|
+
},
|
|
2393
|
+
// Primary: plain "primary" → high; "brand"/"accent" → medium
|
|
2394
|
+
{
|
|
2395
|
+
test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n),
|
|
2396
|
+
role: "primaryAction",
|
|
2397
|
+
confidence: "high",
|
|
2398
|
+
reason: "name contains 'primary'",
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
test: (n) => /\bbrand\b|\baccent\b/.test(n),
|
|
2402
|
+
role: "primaryAction",
|
|
2403
|
+
confidence: "medium",
|
|
2404
|
+
reason: "name contains 'brand' or 'accent'",
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
test: (n) => /\bsecondary\b/.test(n),
|
|
2408
|
+
role: "secondaryAction",
|
|
2409
|
+
confidence: "high",
|
|
2410
|
+
reason: "name contains 'secondary'",
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
test: (n) => /\bdanger\b|\berror\b|\bdestructive\b/.test(n),
|
|
2414
|
+
role: "danger",
|
|
2415
|
+
confidence: "high",
|
|
2416
|
+
reason: "name contains 'danger', 'error', or 'destructive'",
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
test: (n) => /\bsuccess\b|\bpositive\b/.test(n),
|
|
2420
|
+
role: "success",
|
|
2421
|
+
confidence: "high",
|
|
2422
|
+
reason: "name contains 'success' or 'positive'",
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
test: (n) => /surface|background|\bbg\b/.test(n),
|
|
2426
|
+
role: "surface",
|
|
2427
|
+
confidence: "high",
|
|
2428
|
+
reason: "name contains 'surface', 'background', or 'bg'",
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n),
|
|
2432
|
+
role: "textPrimary",
|
|
2433
|
+
confidence: "high",
|
|
2434
|
+
reason: "name contains 'text', 'foreground', or 'fg'",
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
test: (n) => /\bborder\b|\boutline\b|\bdivider\b/.test(n),
|
|
2438
|
+
role: "border",
|
|
2439
|
+
confidence: "high",
|
|
2440
|
+
reason: "name contains 'border', 'outline', or 'divider'",
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
test: (n) => /\bfocus\b|\bring\b/.test(n),
|
|
2444
|
+
role: "focus",
|
|
2445
|
+
confidence: "high",
|
|
2446
|
+
reason: "name contains 'focus' or 'ring'",
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
test: (n) => /\bavatar\b/.test(n),
|
|
2450
|
+
role: "avatarFallback",
|
|
2451
|
+
confidence: "high",
|
|
2452
|
+
reason: "name contains 'avatar'",
|
|
2453
|
+
},
|
|
2454
|
+
];
|
|
2455
|
+
/** Infer a (role, confidence, reason) from a token name, or null if no rule matches. */
|
|
2456
|
+
function inferRole(tokenName) {
|
|
2457
|
+
const n = tokenName.toLowerCase();
|
|
2458
|
+
for (const rule of ROLE_RULES) {
|
|
2459
|
+
if (rule.test(n)) {
|
|
2460
|
+
return { role: rule.role, confidence: rule.confidence, reason: rule.reason };
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Among multiple candidates for the same role, prefer:
|
|
2467
|
+
* 1. high confidence over medium
|
|
2468
|
+
* 2. shorter name (e.g. "--color-primary" beats "--color-primary-hover")
|
|
2469
|
+
*/
|
|
2470
|
+
function bestCandidate(a, b) {
|
|
2471
|
+
if (a.confidence === "high" && b.confidence !== "high")
|
|
2472
|
+
return a;
|
|
2473
|
+
if (b.confidence === "high" && a.confidence !== "high")
|
|
2474
|
+
return b;
|
|
2475
|
+
return a.token.length <= b.token.length ? a : b;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Build a DesignBuildBrief from a DesignContract.
|
|
2479
|
+
*
|
|
2480
|
+
* - Pure: no I/O, no side effects.
|
|
2481
|
+
* - Never persisted or digested.
|
|
2482
|
+
* - Every BriefLine has non-empty groundedIn citing actual contract tokens/roles.
|
|
2483
|
+
* - Roles inferred from NAME only (never from value).
|
|
2484
|
+
* - Inferred tokens (name: null) are never cited by name; they may be cited only
|
|
2485
|
+
* as a count for do/avoid prose, but only if the contract has declared color
|
|
2486
|
+
* tokens with recognizable names to ground the line instead.
|
|
2487
|
+
*/
|
|
2488
|
+
export function buildDesignBrief(contract) {
|
|
2489
|
+
const strength = contract.stats.strength;
|
|
2490
|
+
// ── Role inference ──────────────────────────────────────────────────────────
|
|
2491
|
+
// Walk only declared color tokens (provenance=declared, category=color, name != null).
|
|
2492
|
+
// Inferred tokens always have name: null; value-only matching is forbidden.
|
|
2493
|
+
const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
|
|
2494
|
+
const roleMap = new Map();
|
|
2495
|
+
for (const token of colorTokens) {
|
|
2496
|
+
const inferred = inferRole(token.name);
|
|
2497
|
+
if (!inferred) {
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
const candidate = {
|
|
2501
|
+
role: inferred.role,
|
|
2502
|
+
token: token.name,
|
|
2503
|
+
value: token.value,
|
|
2504
|
+
confidence: inferred.confidence,
|
|
2505
|
+
reason: inferred.reason,
|
|
2506
|
+
};
|
|
2507
|
+
const existing = roleMap.get(inferred.role);
|
|
2508
|
+
roleMap.set(inferred.role, existing ? bestCandidate(existing, candidate) : candidate);
|
|
2509
|
+
}
|
|
2510
|
+
const roles = [...roleMap.values()];
|
|
2511
|
+
// Helper: is a role name in this brief?
|
|
2512
|
+
const roleNames = new Set(roles.map((r) => r.role));
|
|
2513
|
+
// ── Component hints ─────────────────────────────────────────────────────────
|
|
2514
|
+
// Reference ONLY tokens that actually exist in the contract.
|
|
2515
|
+
const firstToken = (cat) => contract.tokens.find((t) => t.category === cat && t.name !== null);
|
|
2516
|
+
const radiusToken = firstToken("radius");
|
|
2517
|
+
const spaceToken = firstToken("space");
|
|
2518
|
+
// Border colour: sourced from the inferred border role (a color token named *border*/*outline*/*divider*).
|
|
2519
|
+
// There is no "border" TokenCategory — border colours live in the "color" category.
|
|
2520
|
+
const borderRoleColor = roleMap.get("border");
|
|
2521
|
+
const shadowToken = firstToken("shadow");
|
|
2522
|
+
const primaryRole = roleMap.get("primaryAction");
|
|
2523
|
+
// card hint
|
|
2524
|
+
const cardGuidanceLines = [];
|
|
2525
|
+
if (radiusToken) {
|
|
2526
|
+
cardGuidanceLines.push(line(`Use ${radiusToken.name} (${radiusToken.value}) for card corner radius.`, [radiusToken.name]));
|
|
2527
|
+
}
|
|
2528
|
+
if (spaceToken) {
|
|
2529
|
+
cardGuidanceLines.push(line(`Use ${spaceToken.name} (${spaceToken.value}) for card internal padding.`, [spaceToken.name]));
|
|
2530
|
+
}
|
|
2531
|
+
if (borderRoleColor) {
|
|
2532
|
+
cardGuidanceLines.push(line(`Apply ${borderRoleColor.token} for card border colour.`, [borderRoleColor.role]));
|
|
2533
|
+
}
|
|
2534
|
+
else if (shadowToken) {
|
|
2535
|
+
cardGuidanceLines.push(line(`Apply ${shadowToken.name} for card shadow/elevation.`, [shadowToken.name]));
|
|
2536
|
+
}
|
|
2537
|
+
const cardHint = cardGuidanceLines.length > 0
|
|
2538
|
+
? { kind: "card", guidance: cardGuidanceLines, confidence: radiusToken && spaceToken ? "high" : "medium" }
|
|
2539
|
+
: { kind: "card", absent: true, note: "No card pattern confidently identified — reuse the host app's existing card styles." };
|
|
2540
|
+
// button hint
|
|
2541
|
+
const buttonGuidanceLines = [];
|
|
2542
|
+
if (primaryRole) {
|
|
2543
|
+
buttonGuidanceLines.push(line(`Use ${primaryRole.token} (${primaryRole.value}) as the primary button background.`, [primaryRole.role]));
|
|
2544
|
+
}
|
|
2545
|
+
if (radiusToken) {
|
|
2546
|
+
buttonGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for button corner radius.`, [radiusToken.name]));
|
|
2547
|
+
}
|
|
2548
|
+
if (spaceToken) {
|
|
2549
|
+
buttonGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for button horizontal padding.`, [spaceToken.name]));
|
|
2550
|
+
}
|
|
2551
|
+
const buttonHint = buttonGuidanceLines.length > 0
|
|
2552
|
+
? { kind: "button", guidance: buttonGuidanceLines, confidence: primaryRole ? "high" : "medium" }
|
|
2553
|
+
: { kind: "button", absent: true, note: "No button pattern confidently identified — reuse the host app's existing button styles." };
|
|
2554
|
+
// input hint
|
|
2555
|
+
const inputGuidanceLines = [];
|
|
2556
|
+
if (borderRoleColor) {
|
|
2557
|
+
inputGuidanceLines.push(line(`Use ${borderRoleColor.token} for input border colour.`, [borderRoleColor.role]));
|
|
2558
|
+
}
|
|
2559
|
+
if (radiusToken) {
|
|
2560
|
+
inputGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for input corner radius.`, [radiusToken.name]));
|
|
2561
|
+
}
|
|
2562
|
+
if (spaceToken) {
|
|
2563
|
+
inputGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for input internal padding.`, [spaceToken.name]));
|
|
2564
|
+
}
|
|
2565
|
+
const inputHint = inputGuidanceLines.length > 0
|
|
2566
|
+
? { kind: "input", guidance: inputGuidanceLines, confidence: borderRoleColor ? "high" : "medium" }
|
|
2567
|
+
: { kind: "input", absent: true, note: "No input pattern confidently identified — reuse the host app's existing input styles." };
|
|
2568
|
+
const componentHints = [cardHint, buttonHint, inputHint];
|
|
2569
|
+
// ── Do/Avoid lines ──────────────────────────────────────────────────────────
|
|
2570
|
+
// Every line MUST be grounded in tokens/roles that actually exist in this brief.
|
|
2571
|
+
const doLines = [];
|
|
2572
|
+
const avoidLines = [];
|
|
2573
|
+
// Only emit do/avoid lines that are grounded in actually-present tokens.
|
|
2574
|
+
const declaredColorTokens = colorTokens.filter((t) => t.provenance === "declared");
|
|
2575
|
+
const declaredSpaceTokens = contract.tokens.filter((t) => t.category === "space" && t.name !== null && t.provenance === "declared");
|
|
2576
|
+
const declaredRadiusTokens = contract.tokens.filter((t) => t.category === "radius" && t.name !== null && t.provenance === "declared");
|
|
2577
|
+
// Do: use declared color tokens
|
|
2578
|
+
if (declaredColorTokens.length > 0) {
|
|
2579
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2580
|
+
doLines.push(line(`Reference declared color tokens (e.g. ${tokenNames.join(", ")}) — never introduce new hex literals.`, tokenNames));
|
|
2581
|
+
}
|
|
2582
|
+
// Do: use declared space tokens
|
|
2583
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2584
|
+
const tokenNames = declaredSpaceTokens.slice(0, 3).map((t) => t.name);
|
|
2585
|
+
doLines.push(line(`Reference declared spacing tokens (e.g. ${tokenNames.join(", ")}) for margins, padding, and gaps.`, tokenNames));
|
|
2586
|
+
}
|
|
2587
|
+
// Do: use declared radius tokens
|
|
2588
|
+
if (declaredRadiusTokens.length > 0) {
|
|
2589
|
+
const tokenNames = declaredRadiusTokens.slice(0, 2).map((t) => t.name);
|
|
2590
|
+
doLines.push(line(`Use declared radius tokens (e.g. ${tokenNames.join(", ")}) for corner rounding.`, tokenNames));
|
|
2591
|
+
}
|
|
2592
|
+
// Do: use primary-role token for interactive elements
|
|
2593
|
+
if (primaryRole) {
|
|
2594
|
+
doLines.push(line(`Use the primary colour token (${primaryRole.token}) for primary interactive elements (buttons, CTAs).`, [primaryRole.role]));
|
|
2595
|
+
}
|
|
2596
|
+
// Avoid: hex literals (grounded in declared color tokens)
|
|
2597
|
+
if (declaredColorTokens.length > 0) {
|
|
2598
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2599
|
+
avoidLines.push(line(`Do not introduce new hex or colour literals — use the ${declaredColorTokens.length} declared colour token(s) (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2600
|
+
}
|
|
2601
|
+
// Avoid: raw spacing literals (grounded in declared space tokens)
|
|
2602
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2603
|
+
const tokenNames = declaredSpaceTokens.slice(0, 2).map((t) => t.name);
|
|
2604
|
+
avoidLines.push(line(`Do not hardcode raw spacing values — use declared spacing tokens (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2605
|
+
}
|
|
2606
|
+
// Avoid: overriding the primary colour token on interactive elements
|
|
2607
|
+
if (primaryRole) {
|
|
2608
|
+
avoidLines.push(line(`Do not override the primary colour token (${primaryRole.token}) with ad-hoc colours on interactive elements.`, [primaryRole.role]));
|
|
2609
|
+
}
|
|
2610
|
+
// ── Review notes ─────────────────────────────────────────────────────────────
|
|
2611
|
+
const reviewNotes = [];
|
|
2612
|
+
if (strength === "weak") {
|
|
2613
|
+
reviewNotes.push("Contract is weak — very few named tokens were found. Guidance above is minimal. Run `vise design extract --from-project` to derive a richer contract from the host project's design system, or provide a prototype.");
|
|
2614
|
+
}
|
|
2615
|
+
if (roles.length === 0) {
|
|
2616
|
+
reviewNotes.push("No colour roles could be inferred from token names. Role-based guidance is unavailable. Ensure tokens use recognisable names (e.g. --color-primary, --color-surface) and run `vise design extract --from-project` again.");
|
|
2617
|
+
}
|
|
2618
|
+
// Suggest missing roles using name examples, not camelCase role identifiers
|
|
2619
|
+
// (camelCase role names must not appear in prose to keep the weak/neutral brief JSON clean).
|
|
2620
|
+
if (!roleNames.has("primaryAction") && contract.stats.declared_tokens > 0) {
|
|
2621
|
+
reviewNotes.push("No primary action colour found — consider naming a token --color-primary (or --color-brand / --color-accent) for primary interactive elements.");
|
|
2622
|
+
}
|
|
2623
|
+
if (!roleNames.has("surface") && contract.stats.declared_tokens > 0) {
|
|
2624
|
+
reviewNotes.push("No surface colour found — consider naming a token --color-surface or --color-background.");
|
|
2625
|
+
}
|
|
2626
|
+
if (!roleNames.has("border") && contract.stats.declared_tokens > 0) {
|
|
2627
|
+
reviewNotes.push("No border colour found — consider naming a token --color-border or --color-outline.");
|
|
2628
|
+
}
|
|
2629
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
2630
|
+
const tokenCount = contract.tokens.filter((t) => t.name !== null).length;
|
|
2631
|
+
const summary = roles.length > 0
|
|
2632
|
+
? `Brief grounded in ${tokenCount} named token(s) and ${roles.length} inferred role(s). Contract strength: ${strength}.`
|
|
2633
|
+
: tokenCount > 0
|
|
2634
|
+
? `Brief grounded in ${tokenCount} named token(s); no colour roles could be inferred from token names. Contract strength: ${strength}.`
|
|
2635
|
+
: `Contract has no named tokens — guidance is unavailable. Contract strength: ${strength}. Run \`vise design extract --from-project\` to derive tokens from the host project.`;
|
|
2636
|
+
return {
|
|
2637
|
+
summary,
|
|
2638
|
+
strength,
|
|
2639
|
+
roles,
|
|
2640
|
+
componentHints,
|
|
2641
|
+
do: doLines,
|
|
2642
|
+
avoid: avoidLines,
|
|
2643
|
+
reviewNotes,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Build outcome-specific design recipe items grounded in an existing brief.
|
|
2648
|
+
*
|
|
2649
|
+
* HARD INVARIANT: every item is grounded ONLY in roles/tokens already in the brief.
|
|
2650
|
+
* Items for absent roles are silently omitted. Returns `undefined` when zero items
|
|
2651
|
+
* can be grounded (e.g. empty brief).
|
|
2652
|
+
*
|
|
2653
|
+
* Pure — no I/O, no side effects. Generated at plan time; never persisted.
|
|
2654
|
+
*/
|
|
2655
|
+
export function buildOutcomeDesignRecipe(brief, outcome) {
|
|
2656
|
+
const roleMap = new Map(brief.roles.map((r) => [r.role, r]));
|
|
2657
|
+
// Collect groundedIn entries from a given component hint (absent hints contribute nothing).
|
|
2658
|
+
const hintGrounding = (kind) => {
|
|
2659
|
+
const hint = brief.componentHints.find((h) => h.kind === kind);
|
|
2660
|
+
if (!hint || "absent" in hint)
|
|
2661
|
+
return [];
|
|
2662
|
+
return hint.guidance.flatMap((l) => l.groundedIn);
|
|
2663
|
+
};
|
|
2664
|
+
// Collect radius-specific grounding from the card hint by looking for guidance
|
|
2665
|
+
// lines that mention "corner radius" — avoids mis-citing a space token as radius.
|
|
2666
|
+
const cardRadiusGrounding = () => {
|
|
2667
|
+
const hint = brief.componentHints.find((h) => h.kind === "card");
|
|
2668
|
+
if (!hint || "absent" in hint)
|
|
2669
|
+
return [];
|
|
2670
|
+
return hint.guidance
|
|
2671
|
+
.filter((l) => l.text.includes("corner radius"))
|
|
2672
|
+
.flatMap((l) => l.groundedIn);
|
|
2673
|
+
};
|
|
2674
|
+
const items = [];
|
|
2675
|
+
if (outcome === "add-feed") {
|
|
2676
|
+
// Composer / action button — only when primaryAction exists.
|
|
2677
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2678
|
+
if (primaryAction) {
|
|
2679
|
+
items.push(line(`The post composer action button uses the primary action colour token (${primaryAction.token}).`, ["primaryAction"]));
|
|
2680
|
+
}
|
|
2681
|
+
// Post cards — only when the card hint has grounding tokens.
|
|
2682
|
+
const cardGrounding = hintGrounding("card");
|
|
2683
|
+
if (cardGrounding.length > 0) {
|
|
2684
|
+
items.push(line("Post cards follow the card component hint: apply the card hint tokens for corner radius, padding, and border/shadow.", cardGrounding));
|
|
2685
|
+
}
|
|
2686
|
+
// Post metadata and timestamps.
|
|
2687
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2688
|
+
if (textSecondary) {
|
|
2689
|
+
items.push(line(`Post metadata and timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2690
|
+
}
|
|
2691
|
+
// Report / delete affordances.
|
|
2692
|
+
const danger = roleMap.get("danger");
|
|
2693
|
+
if (danger) {
|
|
2694
|
+
items.push(line(`Report and delete affordances use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
else {
|
|
2698
|
+
// add-chat
|
|
2699
|
+
// Message bubbles use surface.
|
|
2700
|
+
const surface = roleMap.get("surface");
|
|
2701
|
+
if (surface) {
|
|
2702
|
+
const radiusGrounding = cardRadiusGrounding();
|
|
2703
|
+
if (radiusGrounding.length > 0) {
|
|
2704
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}) with the card corner radius token applied.`, ["surface", ...radiusGrounding]));
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}).`, ["surface"]));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
// Own-message vs other-message contrast — ONLY when BOTH primaryAction AND surface exist.
|
|
2711
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2712
|
+
if (primaryAction && surface) {
|
|
2713
|
+
items.push(line(`Own messages use the primary action colour (${primaryAction.token}) as background; other messages use the surface colour (${surface.token}).`, ["primaryAction", "surface"]));
|
|
2714
|
+
}
|
|
2715
|
+
// Timestamps.
|
|
2716
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2717
|
+
if (textSecondary) {
|
|
2718
|
+
items.push(line(`Message timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2719
|
+
}
|
|
2720
|
+
// Composer follows the input hint tokens.
|
|
2721
|
+
const inputGrounding = hintGrounding("input");
|
|
2722
|
+
if (inputGrounding.length > 0) {
|
|
2723
|
+
items.push(line("The message composer follows the input component hint: apply the input hint tokens for border colour, corner radius, and padding.", inputGrounding));
|
|
2724
|
+
}
|
|
2725
|
+
// Moderation actions.
|
|
2726
|
+
const danger = roleMap.get("danger");
|
|
2727
|
+
if (danger) {
|
|
2728
|
+
items.push(line(`Moderation actions (report, block, mute) use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (items.length === 0)
|
|
2732
|
+
return undefined;
|
|
2733
|
+
return { outcome, items };
|
|
2734
|
+
}
|
|
@@ -4,7 +4,7 @@ import { BROAD_SOCIAL_REGEX, DESIGN_REGEX, classifyOutcome, getOutcomeDefinition
|
|
|
4
4
|
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
5
5
|
import { capabilityChecklist } from "../capabilities.js";
|
|
6
6
|
import { applicableComplianceRuleSummaries } from "./compliance.js";
|
|
7
|
-
import { readDesignContract } from "./design.js";
|
|
7
|
+
import { buildDesignBrief, buildOutcomeDesignRecipe, readDesignContract } from "./design.js";
|
|
8
8
|
import { sdkVersionGuidance } from "./sdkVersion.js";
|
|
9
9
|
import { detectCommandSensors } from "./harness.js";
|
|
10
10
|
import { inspectProject } from "./project.js";
|
|
@@ -71,8 +71,14 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
71
71
|
answers,
|
|
72
72
|
});
|
|
73
73
|
const definition = getOutcomeDefinition(outcome);
|
|
74
|
-
|
|
74
|
+
// Design contract is loaded before intake so the brief can inform the fallback
|
|
75
|
+
// intake question (missing primary-action token) at assembly time.
|
|
75
76
|
const designContract = await readDesignContract(repoRoot);
|
|
77
|
+
const designBrief = designContract ? buildDesignBrief(designContract) : undefined;
|
|
78
|
+
if (designBrief && (outcome === "add-feed" || outcome === "add-chat")) {
|
|
79
|
+
designBrief.outcomeRecipe = buildOutcomeDesignRecipe(designBrief, outcome) ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
const intake = intakeFor(ctx, definition.intakeQuestions(ctx), outcome, designBrief);
|
|
76
82
|
// Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
|
|
77
83
|
// for native). Best-effort — degrades to greenfield "install latest + pin" if the
|
|
78
84
|
// registry is unreachable. Never gates.
|
|
@@ -113,7 +119,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
113
119
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
114
120
|
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
|
|
115
121
|
evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
|
|
116
|
-
designContract: designContract ? designContractGuidance(designContract) : undefined,
|
|
122
|
+
designContract: designContract && designBrief ? designContractGuidance(designContract, designBrief) : undefined,
|
|
117
123
|
completenessChecklist: completenessChecklistFor(outcome),
|
|
118
124
|
sdkVersion,
|
|
119
125
|
};
|
|
@@ -137,7 +143,7 @@ function completenessChecklistFor(outcome) {
|
|
|
137
143
|
// references `var(--x)` / maps it per platform); inferred tokens carry their
|
|
138
144
|
// raw value plus a usage count and an explicit "inferred" marker so they are
|
|
139
145
|
// never mistaken for authoritative brand values.
|
|
140
|
-
function designContractGuidance(contract) {
|
|
146
|
+
function designContractGuidance(contract, brief) {
|
|
141
147
|
const byCategory = (category) => contract.tokens
|
|
142
148
|
.filter((token) => token.category === category)
|
|
143
149
|
.map((token) => token.provenance === "declared" && token.name
|
|
@@ -162,6 +168,7 @@ function designContractGuidance(contract) {
|
|
|
162
168
|
breakpoints: contract.breakpoints.map((breakpoint) => breakpoint.raw),
|
|
163
169
|
attestation: `When you record a design attestation, cite this contract digest (${contract.digest}) so the generated feed can be claimed conformant to the customer's prototype.`,
|
|
164
170
|
advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
|
|
171
|
+
brief,
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
174
|
function intentFor(request, interpretation) {
|
|
@@ -173,7 +180,7 @@ function intentFor(request, interpretation) {
|
|
|
173
180
|
ambiguity: broadSocialRequest || designRequest ? "high" : "medium",
|
|
174
181
|
};
|
|
175
182
|
}
|
|
176
|
-
function intakeFor(ctx, outcomeQuestions) {
|
|
183
|
+
function intakeFor(ctx, outcomeQuestions, outcome, brief) {
|
|
177
184
|
const questions = [...outcomeQuestions];
|
|
178
185
|
if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
|
|
179
186
|
questions.push({
|
|
@@ -194,6 +201,21 @@ function intakeFor(ctx, outcomeQuestions) {
|
|
|
194
201
|
options: ["yes", "use another source"],
|
|
195
202
|
});
|
|
196
203
|
}
|
|
204
|
+
// Graceful-degradation fallback: when a design contract exists for a feed or chat
|
|
205
|
+
// outcome but no primary-action token was confidently inferred, ask the developer
|
|
206
|
+
// to name the correct token. Non-blocking so it doesn't stall implementation.
|
|
207
|
+
if (brief &&
|
|
208
|
+
(outcome === "add-feed" || outcome === "add-chat") &&
|
|
209
|
+
!brief.roles.some((r) => r.role === "primaryAction") &&
|
|
210
|
+
!hasAnswer(ctx.answers, "primary_action_token")) {
|
|
211
|
+
questions.push({
|
|
212
|
+
id: "primary_action_token",
|
|
213
|
+
question: "Which design token (or color value) should be used as the primary action color? No primary-action token was confidently identified in the design contract.",
|
|
214
|
+
why: "A primary action colour is needed for interactive elements (composer button, own-message bubble). Without a confident token, the agent must guess or omit it.",
|
|
215
|
+
required: false,
|
|
216
|
+
blocksImplementationWhenMissing: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
197
219
|
const remainingBlocking = questions.filter((question) => question.blocksImplementationWhenMissing).length;
|
|
198
220
|
return {
|
|
199
221
|
status: remainingBlocking > 0 ? "needs-clarification" : "ready",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amityco/social-plus-vise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -62,9 +62,10 @@
|
|
|
62
62
|
"test:sdk-version": "npm run build && node test/run-sdk-version.mjs",
|
|
63
63
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
64
|
"test:e2e-package": "npm run build && node test/run-e2e-package.mjs",
|
|
65
|
-
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
65
|
+
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
66
66
|
"test:ast": "node test/run-ast-helpers.mjs",
|
|
67
67
|
"test:design-extract": "npm run build && node test/run-design-extract.mjs",
|
|
68
|
+
"test:design-brief": "npm run build && node test/run-design-brief.mjs",
|
|
68
69
|
"test:capabilities": "npm run build && node test/run-capabilities.mjs",
|
|
69
70
|
"test:classify": "npm run build && node test/run-classify.mjs",
|
|
70
71
|
"test:debug": "npm run build && node test/run-debug.mjs",
|