@amityco/social-plus-vise 0.13.0 → 0.14.1
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 +23 -0
- package/dist/tools/design.js +511 -0
- package/dist/tools/integration.js +25 -3
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.1 — 2026-06-03
|
|
8
|
+
|
|
9
|
+
**Theme:** Retract the enumerative DesignBuildBrief from plan output — our own benchmark said so.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **`vise plan` no longer emits `designContract.brief`** (the 0.14.0 roles / component hints / outcome recipes). The pre-registered ablation we shipped 0.14.0 with came back negative — twice: agents given the enumerative brief produced LESS on-contract UI than agents given the design contract alone (by-name token usage ~75% vs ~89–92%; two runs, n=6 per arm, Cursor/Composer 2.5; full evidence in `benchmarks/brief-ablation/`). The mechanism: enumerative guidance narrows what capable agents build and style, while the contract + `design check` loop drives wholesale token adoption. We measure before we claim — and this one measured against us, so it's out.
|
|
13
|
+
- **Kept:** the non-blocking `primary_action_token` intake question (the brief is still computed internally to detect a missing primary-action token — it asks instead of steering). The brief generator and its 17-scenario test suite remain in-tree for future redesign.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 0.14.0 — 2026-06-03
|
|
18
|
+
|
|
19
|
+
**Theme:** DesignBuildBrief — plan-time, grounded UI-building guidance for coding agents (advisory).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
- 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.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
7
30
|
## 0.13.0 — 2026-06-03
|
|
8
31
|
|
|
9
32
|
**Theme:** Deterministic-gate soundness, CLI reliability, and post-rebrand coherence (driven by two repo reviews).
|
package/dist/tools/design.js
CHANGED
|
@@ -2323,3 +2323,514 @@ 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
|
+
// EXCLUSION: action roles bind interactive-family names only.
|
|
2395
|
+
// A token whose name contains a text-family noun (text/foreground/fg) MUST
|
|
2396
|
+
// NEVER bind primaryAction or secondaryAction — it represents a text colour,
|
|
2397
|
+
// not an interactive accent (e.g. --text-bright-accent is a text accent, while
|
|
2398
|
+
// --essential-bright-accent is the real interactive accent).
|
|
2399
|
+
{
|
|
2400
|
+
test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2401
|
+
role: "primaryAction",
|
|
2402
|
+
confidence: "high",
|
|
2403
|
+
reason: "name contains 'primary' (and is not a text-family name)",
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
test: (n) => /\bbrand\b|\baccent\b/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2407
|
+
role: "primaryAction",
|
|
2408
|
+
confidence: "medium",
|
|
2409
|
+
reason: "name contains 'brand' or 'accent' (and is not a text-family name)",
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
test: (n) => /\bsecondary\b/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2413
|
+
role: "secondaryAction",
|
|
2414
|
+
confidence: "high",
|
|
2415
|
+
reason: "name contains 'secondary' (and is not a text-family name)",
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
test: (n) => /\bdanger\b|\berror\b|\bdestructive\b/.test(n),
|
|
2419
|
+
role: "danger",
|
|
2420
|
+
confidence: "high",
|
|
2421
|
+
reason: "name contains 'danger', 'error', or 'destructive'",
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
test: (n) => /\bsuccess\b|\bpositive\b/.test(n),
|
|
2425
|
+
role: "success",
|
|
2426
|
+
confidence: "high",
|
|
2427
|
+
reason: "name contains 'success' or 'positive'",
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
test: (n) => /surface|background|\bbg\b/.test(n),
|
|
2431
|
+
role: "surface",
|
|
2432
|
+
confidence: "high",
|
|
2433
|
+
reason: "name contains 'surface', 'background', or 'bg'",
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
test: (n) => /\btext\b|foreground|\bfg\b/.test(n),
|
|
2437
|
+
role: "textPrimary",
|
|
2438
|
+
confidence: "high",
|
|
2439
|
+
reason: "name contains 'text', 'foreground', or 'fg'",
|
|
2440
|
+
},
|
|
2441
|
+
{
|
|
2442
|
+
test: (n) => /\bborder\b|\boutline\b|\bdivider\b/.test(n),
|
|
2443
|
+
role: "border",
|
|
2444
|
+
confidence: "high",
|
|
2445
|
+
reason: "name contains 'border', 'outline', or 'divider'",
|
|
2446
|
+
},
|
|
2447
|
+
{
|
|
2448
|
+
test: (n) => /\bfocus\b|\bring\b/.test(n),
|
|
2449
|
+
role: "focus",
|
|
2450
|
+
confidence: "high",
|
|
2451
|
+
reason: "name contains 'focus' or 'ring'",
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
test: (n) => /\bavatar\b/.test(n),
|
|
2455
|
+
role: "avatarFallback",
|
|
2456
|
+
confidence: "high",
|
|
2457
|
+
reason: "name contains 'avatar'",
|
|
2458
|
+
},
|
|
2459
|
+
];
|
|
2460
|
+
/** Infer a (role, confidence, reason) from a token name, or null if no rule matches. */
|
|
2461
|
+
function inferRole(tokenName) {
|
|
2462
|
+
const n = tokenName.toLowerCase();
|
|
2463
|
+
for (const rule of ROLE_RULES) {
|
|
2464
|
+
if (rule.test(n)) {
|
|
2465
|
+
return { role: rule.role, confidence: rule.confidence, reason: rule.reason };
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Among multiple candidates for the same role, prefer:
|
|
2472
|
+
* 1. high confidence over medium
|
|
2473
|
+
* 2. shorter name (e.g. "--color-primary" beats "--color-primary-hover")
|
|
2474
|
+
*/
|
|
2475
|
+
function bestCandidate(a, b) {
|
|
2476
|
+
if (a.confidence === "high" && b.confidence !== "high")
|
|
2477
|
+
return a;
|
|
2478
|
+
if (b.confidence === "high" && a.confidence !== "high")
|
|
2479
|
+
return b;
|
|
2480
|
+
return a.token.length <= b.token.length ? a : b;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Build a DesignBuildBrief from a DesignContract.
|
|
2484
|
+
*
|
|
2485
|
+
* - Pure: no I/O, no side effects.
|
|
2486
|
+
* - Never persisted or digested.
|
|
2487
|
+
* - Every BriefLine has non-empty groundedIn citing actual contract tokens/roles.
|
|
2488
|
+
* - Roles inferred from NAME only (never from value).
|
|
2489
|
+
* - Inferred tokens (name: null) are never cited by name; they may be cited only
|
|
2490
|
+
* as a count for do/avoid prose, but only if the contract has declared color
|
|
2491
|
+
* tokens with recognizable names to ground the line instead.
|
|
2492
|
+
*/
|
|
2493
|
+
export function buildDesignBrief(contract) {
|
|
2494
|
+
const strength = contract.stats.strength;
|
|
2495
|
+
// ── Role inference ──────────────────────────────────────────────────────────
|
|
2496
|
+
// Walk only declared color tokens (provenance=declared, category=color, name != null).
|
|
2497
|
+
// Inferred tokens always have name: null; value-only matching is forbidden.
|
|
2498
|
+
const colorTokens = contract.tokens.filter((t) => t.category === "color" && t.name !== null);
|
|
2499
|
+
const roleMap = new Map();
|
|
2500
|
+
for (const token of colorTokens) {
|
|
2501
|
+
const inferred = inferRole(token.name);
|
|
2502
|
+
if (!inferred) {
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
const candidate = {
|
|
2506
|
+
role: inferred.role,
|
|
2507
|
+
token: token.name,
|
|
2508
|
+
value: token.value,
|
|
2509
|
+
confidence: inferred.confidence,
|
|
2510
|
+
reason: inferred.reason,
|
|
2511
|
+
};
|
|
2512
|
+
const existing = roleMap.get(inferred.role);
|
|
2513
|
+
roleMap.set(inferred.role, existing ? bestCandidate(existing, candidate) : candidate);
|
|
2514
|
+
}
|
|
2515
|
+
const roles = [...roleMap.values()];
|
|
2516
|
+
// Helper: is a role name in this brief?
|
|
2517
|
+
const roleNames = new Set(roles.map((r) => r.role));
|
|
2518
|
+
// String-typed version for use in contexts where we compare arbitrary strings against role names.
|
|
2519
|
+
const roleNameStrings = new Set(roles.map((r) => r.role));
|
|
2520
|
+
// ── Component hints ─────────────────────────────────────────────────────────
|
|
2521
|
+
// Reference ONLY tokens that actually exist in the contract.
|
|
2522
|
+
/**
|
|
2523
|
+
* Representative token selector: among a category's declared tokens, prefer
|
|
2524
|
+
* (in order) a name containing "base", then "default", then "md"/"medium",
|
|
2525
|
+
* then the SHORTEST name, then first-in-order.
|
|
2526
|
+
*
|
|
2527
|
+
* Using first-in-contract-order (old behaviour) selected unrepresentative tokens
|
|
2528
|
+
* like --encore-corner-radius-smaller over --encore-corner-radius-base, causing
|
|
2529
|
+
* agents to anchor on non-base variants and miss the authoritative base token.
|
|
2530
|
+
*/
|
|
2531
|
+
function representativeToken(cat) {
|
|
2532
|
+
const candidates = contract.tokens.filter((t) => t.category === cat && t.name !== null && t.provenance === "declared");
|
|
2533
|
+
if (candidates.length === 0)
|
|
2534
|
+
return undefined;
|
|
2535
|
+
// Scoring: lower is better. 0 = base, 1 = default, 2 = md/medium, 3 = shortest, 4 = first.
|
|
2536
|
+
const score = (name) => {
|
|
2537
|
+
const n = name.toLowerCase();
|
|
2538
|
+
if (/\bbase\b/.test(n))
|
|
2539
|
+
return 0;
|
|
2540
|
+
if (/\bdefault\b/.test(n))
|
|
2541
|
+
return 1;
|
|
2542
|
+
if (/\bmd\b|\bmedium\b/.test(n))
|
|
2543
|
+
return 2;
|
|
2544
|
+
return 3;
|
|
2545
|
+
};
|
|
2546
|
+
return candidates.reduce((best, t) => {
|
|
2547
|
+
const bs = score(best.name);
|
|
2548
|
+
const ts = score(t.name);
|
|
2549
|
+
if (ts < bs)
|
|
2550
|
+
return t;
|
|
2551
|
+
if (ts > bs)
|
|
2552
|
+
return best;
|
|
2553
|
+
// Same bucket: prefer shorter name, then first-in-order (best wins ties).
|
|
2554
|
+
return t.name.length < best.name.length ? t : best;
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
const radiusToken = representativeToken("radius");
|
|
2558
|
+
const spaceToken = representativeToken("space");
|
|
2559
|
+
// Border colour: sourced from the inferred border role (a color token named *border*/*outline*/*divider*).
|
|
2560
|
+
// There is no "border" TokenCategory — border colours live in the "color" category.
|
|
2561
|
+
const borderRoleColor = roleMap.get("border");
|
|
2562
|
+
const shadowToken = representativeToken("shadow");
|
|
2563
|
+
const primaryRole = roleMap.get("primaryAction");
|
|
2564
|
+
// card hint
|
|
2565
|
+
const cardGuidanceLines = [];
|
|
2566
|
+
if (radiusToken) {
|
|
2567
|
+
cardGuidanceLines.push(line(`Use ${radiusToken.name} (${radiusToken.value}) for card corner radius.`, [radiusToken.name]));
|
|
2568
|
+
}
|
|
2569
|
+
if (spaceToken) {
|
|
2570
|
+
cardGuidanceLines.push(line(`Use ${spaceToken.name} (${spaceToken.value}) for card internal padding.`, [spaceToken.name]));
|
|
2571
|
+
}
|
|
2572
|
+
if (borderRoleColor) {
|
|
2573
|
+
cardGuidanceLines.push(line(`Apply ${borderRoleColor.token} for card border colour.`, [borderRoleColor.role]));
|
|
2574
|
+
}
|
|
2575
|
+
else if (shadowToken) {
|
|
2576
|
+
cardGuidanceLines.push(line(`Apply ${shadowToken.name} for card shadow/elevation.`, [shadowToken.name]));
|
|
2577
|
+
}
|
|
2578
|
+
const cardHint = cardGuidanceLines.length > 0
|
|
2579
|
+
? { kind: "card", guidance: cardGuidanceLines, confidence: radiusToken && spaceToken ? "high" : "medium" }
|
|
2580
|
+
: { kind: "card", absent: true, note: "No card pattern confidently identified — reuse the host app's existing card styles." };
|
|
2581
|
+
// button hint
|
|
2582
|
+
const buttonGuidanceLines = [];
|
|
2583
|
+
if (primaryRole) {
|
|
2584
|
+
buttonGuidanceLines.push(line(`Use ${primaryRole.token} (${primaryRole.value}) as the primary button background.`, [primaryRole.role]));
|
|
2585
|
+
}
|
|
2586
|
+
if (radiusToken) {
|
|
2587
|
+
buttonGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for button corner radius.`, [radiusToken.name]));
|
|
2588
|
+
}
|
|
2589
|
+
if (spaceToken) {
|
|
2590
|
+
buttonGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for button horizontal padding.`, [spaceToken.name]));
|
|
2591
|
+
}
|
|
2592
|
+
const buttonHint = buttonGuidanceLines.length > 0
|
|
2593
|
+
? { kind: "button", guidance: buttonGuidanceLines, confidence: primaryRole ? "high" : "medium" }
|
|
2594
|
+
: { kind: "button", absent: true, note: "No button pattern confidently identified — reuse the host app's existing button styles." };
|
|
2595
|
+
// input hint
|
|
2596
|
+
const inputGuidanceLines = [];
|
|
2597
|
+
if (borderRoleColor) {
|
|
2598
|
+
inputGuidanceLines.push(line(`Use ${borderRoleColor.token} for input border colour.`, [borderRoleColor.role]));
|
|
2599
|
+
}
|
|
2600
|
+
if (radiusToken) {
|
|
2601
|
+
inputGuidanceLines.push(line(`Apply ${radiusToken.name} (${radiusToken.value}) for input corner radius.`, [radiusToken.name]));
|
|
2602
|
+
}
|
|
2603
|
+
if (spaceToken) {
|
|
2604
|
+
inputGuidanceLines.push(line(`Apply ${spaceToken.name} (${spaceToken.value}) for input internal padding.`, [spaceToken.name]));
|
|
2605
|
+
}
|
|
2606
|
+
const inputHint = inputGuidanceLines.length > 0
|
|
2607
|
+
? { kind: "input", guidance: inputGuidanceLines, confidence: borderRoleColor ? "high" : "medium" }
|
|
2608
|
+
: { kind: "input", absent: true, note: "No input pattern confidently identified — reuse the host app's existing input styles." };
|
|
2609
|
+
const componentHints = [cardHint, buttonHint, inputHint];
|
|
2610
|
+
// ── Do/Avoid lines ──────────────────────────────────────────────────────────
|
|
2611
|
+
// Every line MUST be grounded in tokens/roles that actually exist in this brief.
|
|
2612
|
+
const doLines = [];
|
|
2613
|
+
const avoidLines = [];
|
|
2614
|
+
// Only emit do/avoid lines that are grounded in actually-present tokens.
|
|
2615
|
+
const declaredColorTokens = colorTokens.filter((t) => t.provenance === "declared");
|
|
2616
|
+
const declaredSpaceTokens = contract.tokens.filter((t) => t.category === "space" && t.name !== null && t.provenance === "declared");
|
|
2617
|
+
const declaredRadiusTokens = contract.tokens.filter((t) => t.category === "radius" && t.name !== null && t.provenance === "declared");
|
|
2618
|
+
// Do: use declared color tokens
|
|
2619
|
+
if (declaredColorTokens.length > 0) {
|
|
2620
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2621
|
+
doLines.push(line(`Reference declared color tokens (e.g. ${tokenNames.join(", ")}) — never introduce new hex literals.`, tokenNames));
|
|
2622
|
+
}
|
|
2623
|
+
// Do: use declared space tokens
|
|
2624
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2625
|
+
const tokenNames = declaredSpaceTokens.slice(0, 3).map((t) => t.name);
|
|
2626
|
+
doLines.push(line(`Reference declared spacing tokens (e.g. ${tokenNames.join(", ")}) for margins, padding, and gaps.`, tokenNames));
|
|
2627
|
+
}
|
|
2628
|
+
// Do: use declared radius tokens
|
|
2629
|
+
if (declaredRadiusTokens.length > 0) {
|
|
2630
|
+
const tokenNames = declaredRadiusTokens.slice(0, 2).map((t) => t.name);
|
|
2631
|
+
doLines.push(line(`Use declared radius tokens (e.g. ${tokenNames.join(", ")}) for corner rounding.`, tokenNames));
|
|
2632
|
+
}
|
|
2633
|
+
// Do: use primary-role token for interactive elements
|
|
2634
|
+
if (primaryRole) {
|
|
2635
|
+
doLines.push(line(`Use the primary colour token (${primaryRole.token}) for primary interactive elements (buttons, CTAs).`, [primaryRole.role]));
|
|
2636
|
+
}
|
|
2637
|
+
// Avoid: hex literals (grounded in declared color tokens)
|
|
2638
|
+
if (declaredColorTokens.length > 0) {
|
|
2639
|
+
const tokenNames = declaredColorTokens.slice(0, 3).map((t) => t.name);
|
|
2640
|
+
avoidLines.push(line(`Do not introduce new hex or colour literals — use the ${declaredColorTokens.length} declared colour token(s) (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2641
|
+
}
|
|
2642
|
+
// Avoid: raw spacing literals (grounded in declared space tokens)
|
|
2643
|
+
if (declaredSpaceTokens.length > 0) {
|
|
2644
|
+
const tokenNames = declaredSpaceTokens.slice(0, 2).map((t) => t.name);
|
|
2645
|
+
avoidLines.push(line(`Do not hardcode raw spacing values — use declared spacing tokens (e.g. ${tokenNames.join(", ")}).`, tokenNames));
|
|
2646
|
+
}
|
|
2647
|
+
// Avoid: overriding the primary colour token on interactive elements
|
|
2648
|
+
if (primaryRole) {
|
|
2649
|
+
avoidLines.push(line(`Do not override the primary colour token (${primaryRole.token}) with ad-hoc colours on interactive elements.`, [primaryRole.role]));
|
|
2650
|
+
}
|
|
2651
|
+
// ── Breadth instruction (Fix 3 — anti-anchoring) ─────────────────────────────
|
|
2652
|
+
//
|
|
2653
|
+
// The roles and hints above are a starting lens — a minimal named anchor set.
|
|
2654
|
+
// Without an explicit instruction, agents anchor on the named tokens and stop
|
|
2655
|
+
// reading the full token file. This breadth line counters that by directing the
|
|
2656
|
+
// agent to the FULL declared token set, grounded in one representative token
|
|
2657
|
+
// per category so the grounding itself spans the system's breadth.
|
|
2658
|
+
//
|
|
2659
|
+
// Grounding rule: up to one representative declared token per category present,
|
|
2660
|
+
// using the representativeToken() selector (Fix 2). Only emitted when ≥1
|
|
2661
|
+
// declared token exists — weak/empty contracts do NOT get an invented breadth line.
|
|
2662
|
+
const ALL_TOKEN_CATEGORIES = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "borderWidth", "breakpoint", "motion", "opacity", "zIndex"];
|
|
2663
|
+
// Collect one representative declared token per category that has any declared tokens.
|
|
2664
|
+
const breadthGroundingTokens = [];
|
|
2665
|
+
for (const cat of ALL_TOKEN_CATEGORIES) {
|
|
2666
|
+
const rep = representativeToken(cat);
|
|
2667
|
+
if (rep)
|
|
2668
|
+
breadthGroundingTokens.push(rep);
|
|
2669
|
+
}
|
|
2670
|
+
const totalDeclaredTokens = contract.tokens.filter((t) => t.provenance === "declared" && t.name !== null).length;
|
|
2671
|
+
if (breadthGroundingTokens.length > 0) {
|
|
2672
|
+
// Build the grounding set (names of the representative tokens).
|
|
2673
|
+
const breadthGrounding = breadthGroundingTokens.map((t) => t.name);
|
|
2674
|
+
doLines.push(line(`The roles and hints above are a starting lens, not the full design system — reference the FULL declared token set (${totalDeclaredTokens} declared tokens) and prefer an existing token over any new value.`, breadthGrounding));
|
|
2675
|
+
}
|
|
2676
|
+
// ── Review notes ─────────────────────────────────────────────────────────────
|
|
2677
|
+
const reviewNotes = [];
|
|
2678
|
+
if (strength === "weak") {
|
|
2679
|
+
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.");
|
|
2680
|
+
}
|
|
2681
|
+
if (roles.length === 0) {
|
|
2682
|
+
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.");
|
|
2683
|
+
}
|
|
2684
|
+
// Suggest missing roles using name examples, not camelCase role identifiers
|
|
2685
|
+
// (camelCase role names must not appear in prose to keep the weak/neutral brief JSON clean).
|
|
2686
|
+
if (!roleNames.has("primaryAction") && contract.stats.declared_tokens > 0) {
|
|
2687
|
+
reviewNotes.push("No primary action colour found — consider naming a token --color-primary (or --color-brand / --color-accent) for primary interactive elements.");
|
|
2688
|
+
}
|
|
2689
|
+
if (!roleNames.has("surface") && contract.stats.declared_tokens > 0) {
|
|
2690
|
+
reviewNotes.push("No surface colour found — consider naming a token --color-surface or --color-background.");
|
|
2691
|
+
}
|
|
2692
|
+
if (!roleNames.has("border") && contract.stats.declared_tokens > 0) {
|
|
2693
|
+
reviewNotes.push("No border colour found — consider naming a token --color-border or --color-outline.");
|
|
2694
|
+
}
|
|
2695
|
+
// Conservative-inference reviewNote (Fix 3):
|
|
2696
|
+
// When roles bind fewer than half the declared COLOR tokens, the role inference
|
|
2697
|
+
// was conservative on this vocabulary (e.g. Encore's essential-/decorative-
|
|
2698
|
+
// prefixed names are correct restraint but result in thin role coverage).
|
|
2699
|
+
// Alert the agent that it MUST read the full token file rather than relying on roles alone.
|
|
2700
|
+
const declaredColorCount = declaredColorTokens.length;
|
|
2701
|
+
const colorTokensBoundToRoles = roles.filter((r) => declaredColorTokens.some((t) => t.name === r.token)).length;
|
|
2702
|
+
if (declaredColorCount > 0 && colorTokensBoundToRoles < declaredColorCount / 2) {
|
|
2703
|
+
reviewNotes.push(`Role inference was conservative on this vocabulary — only ${colorTokensBoundToRoles} of ${declaredColorCount} declared colour token(s) are bound to roles. The full token file must be read to discover the complete colour system.`);
|
|
2704
|
+
}
|
|
2705
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
2706
|
+
const tokenCount = contract.tokens.filter((t) => t.name !== null).length;
|
|
2707
|
+
// Count how many distinct declared token names are referenced by roles + hints.
|
|
2708
|
+
const briefReferencedTokenNames = new Set();
|
|
2709
|
+
for (const r of roles)
|
|
2710
|
+
briefReferencedTokenNames.add(r.token);
|
|
2711
|
+
for (const h of componentHints) {
|
|
2712
|
+
if (!("absent" in h)) {
|
|
2713
|
+
for (const l of h.guidance) {
|
|
2714
|
+
for (const g of l.groundedIn) {
|
|
2715
|
+
// Only add entries that are actual declared token names (not role names).
|
|
2716
|
+
if (!roleNameStrings.has(g))
|
|
2717
|
+
briefReferencedTokenNames.add(g);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const briefCoveredCount = [...briefReferencedTokenNames].filter((n) => contract.tokens.some((t) => t.name === n && t.provenance === "declared")).length;
|
|
2723
|
+
const summary = roles.length > 0
|
|
2724
|
+
? `Brief grounded in ${tokenCount} named token(s) and ${roles.length} inferred role(s). Contract strength: ${strength}. roles/hints cover ${briefCoveredCount} of ${totalDeclaredTokens} declared tokens — consult the full set.`
|
|
2725
|
+
: tokenCount > 0
|
|
2726
|
+
? `Brief grounded in ${tokenCount} named token(s); no colour roles could be inferred from token names. Contract strength: ${strength}. roles/hints cover ${briefCoveredCount} of ${totalDeclaredTokens} declared tokens — consult the full set.`
|
|
2727
|
+
: `Contract has no named tokens — guidance is unavailable. Contract strength: ${strength}. Run \`vise design extract --from-project\` to derive tokens from the host project.`;
|
|
2728
|
+
return {
|
|
2729
|
+
summary,
|
|
2730
|
+
strength,
|
|
2731
|
+
roles,
|
|
2732
|
+
componentHints,
|
|
2733
|
+
do: doLines,
|
|
2734
|
+
avoid: avoidLines,
|
|
2735
|
+
reviewNotes,
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Build outcome-specific design recipe items grounded in an existing brief.
|
|
2740
|
+
*
|
|
2741
|
+
* HARD INVARIANT: every item is grounded ONLY in roles/tokens already in the brief.
|
|
2742
|
+
* Items for absent roles are silently omitted. Returns `undefined` when zero items
|
|
2743
|
+
* can be grounded (e.g. empty brief).
|
|
2744
|
+
*
|
|
2745
|
+
* Pure — no I/O, no side effects. Generated at plan time; never persisted.
|
|
2746
|
+
*/
|
|
2747
|
+
export function buildOutcomeDesignRecipe(brief, outcome) {
|
|
2748
|
+
const roleMap = new Map(brief.roles.map((r) => [r.role, r]));
|
|
2749
|
+
// Collect groundedIn entries from a given component hint (absent hints contribute nothing).
|
|
2750
|
+
const hintGrounding = (kind) => {
|
|
2751
|
+
const hint = brief.componentHints.find((h) => h.kind === kind);
|
|
2752
|
+
if (!hint || "absent" in hint)
|
|
2753
|
+
return [];
|
|
2754
|
+
return hint.guidance.flatMap((l) => l.groundedIn);
|
|
2755
|
+
};
|
|
2756
|
+
// Collect radius-specific grounding from the card hint by looking for guidance
|
|
2757
|
+
// lines that mention "corner radius" — avoids mis-citing a space token as radius.
|
|
2758
|
+
const cardRadiusGrounding = () => {
|
|
2759
|
+
const hint = brief.componentHints.find((h) => h.kind === "card");
|
|
2760
|
+
if (!hint || "absent" in hint)
|
|
2761
|
+
return [];
|
|
2762
|
+
return hint.guidance
|
|
2763
|
+
.filter((l) => l.text.includes("corner radius"))
|
|
2764
|
+
.flatMap((l) => l.groundedIn);
|
|
2765
|
+
};
|
|
2766
|
+
const items = [];
|
|
2767
|
+
if (outcome === "add-feed") {
|
|
2768
|
+
// Composer / action button — only when primaryAction exists.
|
|
2769
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2770
|
+
if (primaryAction) {
|
|
2771
|
+
items.push(line(`The post composer action button uses the primary action colour token (${primaryAction.token}).`, ["primaryAction"]));
|
|
2772
|
+
}
|
|
2773
|
+
// Post cards — only when the card hint has grounding tokens.
|
|
2774
|
+
const cardGrounding = hintGrounding("card");
|
|
2775
|
+
if (cardGrounding.length > 0) {
|
|
2776
|
+
items.push(line("Post cards follow the card component hint: apply the card hint tokens for corner radius, padding, and border/shadow.", cardGrounding));
|
|
2777
|
+
}
|
|
2778
|
+
// Post metadata and timestamps.
|
|
2779
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2780
|
+
if (textSecondary) {
|
|
2781
|
+
items.push(line(`Post metadata and timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2782
|
+
}
|
|
2783
|
+
// Report / delete affordances.
|
|
2784
|
+
const danger = roleMap.get("danger");
|
|
2785
|
+
if (danger) {
|
|
2786
|
+
items.push(line(`Report and delete affordances use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
else {
|
|
2790
|
+
// add-chat
|
|
2791
|
+
// Message bubbles use surface.
|
|
2792
|
+
const surface = roleMap.get("surface");
|
|
2793
|
+
if (surface) {
|
|
2794
|
+
const radiusGrounding = cardRadiusGrounding();
|
|
2795
|
+
if (radiusGrounding.length > 0) {
|
|
2796
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}) with the card corner radius token applied.`, ["surface", ...radiusGrounding]));
|
|
2797
|
+
}
|
|
2798
|
+
else {
|
|
2799
|
+
items.push(line(`Message bubbles use the surface colour token (${surface.token}).`, ["surface"]));
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
// Own-message vs other-message contrast — ONLY when BOTH primaryAction AND surface exist.
|
|
2803
|
+
const primaryAction = roleMap.get("primaryAction");
|
|
2804
|
+
if (primaryAction && surface) {
|
|
2805
|
+
items.push(line(`Own messages use the primary action colour (${primaryAction.token}) as background; other messages use the surface colour (${surface.token}).`, ["primaryAction", "surface"]));
|
|
2806
|
+
}
|
|
2807
|
+
// Timestamps.
|
|
2808
|
+
const textSecondary = roleMap.get("textSecondary");
|
|
2809
|
+
if (textSecondary) {
|
|
2810
|
+
items.push(line(`Message timestamps use the secondary text colour token (${textSecondary.token}).`, ["textSecondary"]));
|
|
2811
|
+
}
|
|
2812
|
+
// Composer follows the input hint tokens.
|
|
2813
|
+
const inputGrounding = hintGrounding("input");
|
|
2814
|
+
if (inputGrounding.length > 0) {
|
|
2815
|
+
items.push(line("The message composer follows the input component hint: apply the input hint tokens for border colour, corner radius, and padding.", inputGrounding));
|
|
2816
|
+
}
|
|
2817
|
+
// Moderation actions.
|
|
2818
|
+
const danger = roleMap.get("danger");
|
|
2819
|
+
if (danger) {
|
|
2820
|
+
items.push(line(`Moderation actions (report, block, mute) use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
// Breadth audit item (Fix 3 — anti-anchoring):
|
|
2824
|
+
// Append a final item instructing the agent to audit against the FULL token set.
|
|
2825
|
+
// Grounded the same way as the brief's breadth do-line: reuse the groundedIn of
|
|
2826
|
+
// the "starting lens" do-line if it exists in the brief (spans the breadth of
|
|
2827
|
+
// the system's categories). Only emitted when the brief has declared tokens
|
|
2828
|
+
// (the breadth do-line only exists when there are declared tokens).
|
|
2829
|
+
const breadthDoLine = brief.do.find((l) => l.text.includes("starting lens"));
|
|
2830
|
+
if (breadthDoLine) {
|
|
2831
|
+
items.push(line("Before finishing, audit the UI against the full declared token set and replace any near-miss values with the matching token.", breadthDoLine.groundedIn));
|
|
2832
|
+
}
|
|
2833
|
+
if (items.length === 0)
|
|
2834
|
+
return undefined;
|
|
2835
|
+
return { outcome, items };
|
|
2836
|
+
}
|
|
@@ -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, 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,15 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
71
71
|
answers,
|
|
72
72
|
});
|
|
73
73
|
const definition = getOutcomeDefinition(outcome);
|
|
74
|
-
|
|
74
|
+
// The design brief is computed INTERNALLY only, to drive the missing-primary-action
|
|
75
|
+
// intake fallback. It is deliberately NOT emitted in plan output: a pre-registered
|
|
76
|
+
// ablation (benchmarks/brief-ablation/RESULTS.md, two runs, n=6/arm) showed that
|
|
77
|
+
// enumerative plan-time design guidance NARROWS what capable agents build and style —
|
|
78
|
+
// the contract + design-check loop alone scored higher on by-name token conformance.
|
|
79
|
+
// Retracted in 0.14.1; the generator stays in design.ts for future redesign.
|
|
75
80
|
const designContract = await readDesignContract(repoRoot);
|
|
81
|
+
const designBrief = designContract ? buildDesignBrief(designContract) : undefined;
|
|
82
|
+
const intake = intakeFor(ctx, definition.intakeQuestions(ctx), outcome, designBrief);
|
|
76
83
|
// Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
|
|
77
84
|
// for native). Best-effort — degrades to greenfield "install latest + pin" if the
|
|
78
85
|
// registry is unreachable. Never gates.
|
|
@@ -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.1",
|
|
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",
|