@amityco/social-plus-vise 0.14.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 +10 -0
- package/dist/tools/design.js +114 -12
- package/dist/tools/integration.js +9 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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
|
+
|
|
7
17
|
## 0.14.0 — 2026-06-03
|
|
8
18
|
|
|
9
19
|
**Theme:** DesignBuildBrief — plan-time, grounded UI-building guidance for coding agents (advisory).
|
package/dist/tools/design.js
CHANGED
|
@@ -2391,23 +2391,28 @@ const ROLE_RULES = [
|
|
|
2391
2391
|
reason: "name contains a border keyword with a primary/secondary modifier — the noun keyword sets the role family",
|
|
2392
2392
|
},
|
|
2393
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).
|
|
2394
2399
|
{
|
|
2395
|
-
test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n),
|
|
2400
|
+
test: (n) => /\bprimary\b/.test(n) && !/brand|accent/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2396
2401
|
role: "primaryAction",
|
|
2397
2402
|
confidence: "high",
|
|
2398
|
-
reason: "name contains 'primary'",
|
|
2403
|
+
reason: "name contains 'primary' (and is not a text-family name)",
|
|
2399
2404
|
},
|
|
2400
2405
|
{
|
|
2401
|
-
test: (n) => /\bbrand\b|\baccent\b/.test(n),
|
|
2406
|
+
test: (n) => /\bbrand\b|\baccent\b/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2402
2407
|
role: "primaryAction",
|
|
2403
2408
|
confidence: "medium",
|
|
2404
|
-
reason: "name contains 'brand' or 'accent'",
|
|
2409
|
+
reason: "name contains 'brand' or 'accent' (and is not a text-family name)",
|
|
2405
2410
|
},
|
|
2406
2411
|
{
|
|
2407
|
-
test: (n) => /\bsecondary\b/.test(n),
|
|
2412
|
+
test: (n) => /\bsecondary\b/.test(n) && !/\btext\b|foreground|\bfg\b/.test(n),
|
|
2408
2413
|
role: "secondaryAction",
|
|
2409
2414
|
confidence: "high",
|
|
2410
|
-
reason: "name contains 'secondary'",
|
|
2415
|
+
reason: "name contains 'secondary' (and is not a text-family name)",
|
|
2411
2416
|
},
|
|
2412
2417
|
{
|
|
2413
2418
|
test: (n) => /\bdanger\b|\berror\b|\bdestructive\b/.test(n),
|
|
@@ -2510,15 +2515,51 @@ export function buildDesignBrief(contract) {
|
|
|
2510
2515
|
const roles = [...roleMap.values()];
|
|
2511
2516
|
// Helper: is a role name in this brief?
|
|
2512
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));
|
|
2513
2520
|
// ── Component hints ─────────────────────────────────────────────────────────
|
|
2514
2521
|
// Reference ONLY tokens that actually exist in the contract.
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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");
|
|
2518
2559
|
// Border colour: sourced from the inferred border role (a color token named *border*/*outline*/*divider*).
|
|
2519
2560
|
// There is no "border" TokenCategory — border colours live in the "color" category.
|
|
2520
2561
|
const borderRoleColor = roleMap.get("border");
|
|
2521
|
-
const shadowToken =
|
|
2562
|
+
const shadowToken = representativeToken("shadow");
|
|
2522
2563
|
const primaryRole = roleMap.get("primaryAction");
|
|
2523
2564
|
// card hint
|
|
2524
2565
|
const cardGuidanceLines = [];
|
|
@@ -2607,6 +2648,31 @@ export function buildDesignBrief(contract) {
|
|
|
2607
2648
|
if (primaryRole) {
|
|
2608
2649
|
avoidLines.push(line(`Do not override the primary colour token (${primaryRole.token}) with ad-hoc colours on interactive elements.`, [primaryRole.role]));
|
|
2609
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
|
+
}
|
|
2610
2676
|
// ── Review notes ─────────────────────────────────────────────────────────────
|
|
2611
2677
|
const reviewNotes = [];
|
|
2612
2678
|
if (strength === "weak") {
|
|
@@ -2626,12 +2692,38 @@ export function buildDesignBrief(contract) {
|
|
|
2626
2692
|
if (!roleNames.has("border") && contract.stats.declared_tokens > 0) {
|
|
2627
2693
|
reviewNotes.push("No border colour found — consider naming a token --color-border or --color-outline.");
|
|
2628
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
|
+
}
|
|
2629
2705
|
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
2630
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;
|
|
2631
2723
|
const summary = roles.length > 0
|
|
2632
|
-
? `Brief grounded in ${tokenCount} named token(s) and ${roles.length} inferred role(s). Contract strength: ${strength}.`
|
|
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.`
|
|
2633
2725
|
: tokenCount > 0
|
|
2634
|
-
? `Brief grounded in ${tokenCount} named token(s); no colour roles could be inferred from token names. Contract strength: ${strength}.`
|
|
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.`
|
|
2635
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.`;
|
|
2636
2728
|
return {
|
|
2637
2729
|
summary,
|
|
@@ -2728,6 +2820,16 @@ export function buildOutcomeDesignRecipe(brief, outcome) {
|
|
|
2728
2820
|
items.push(line(`Moderation actions (report, block, mute) use the danger colour token (${danger.token}).`, ["danger"]));
|
|
2729
2821
|
}
|
|
2730
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
|
+
}
|
|
2731
2833
|
if (items.length === 0)
|
|
2732
2834
|
return undefined;
|
|
2733
2835
|
return { outcome, items };
|
|
@@ -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 { buildDesignBrief,
|
|
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,13 +71,14 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
71
71
|
answers,
|
|
72
72
|
});
|
|
73
73
|
const definition = getOutcomeDefinition(outcome);
|
|
74
|
-
//
|
|
75
|
-
// intake
|
|
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.
|
|
76
80
|
const designContract = await readDesignContract(repoRoot);
|
|
77
81
|
const designBrief = designContract ? buildDesignBrief(designContract) : undefined;
|
|
78
|
-
if (designBrief && (outcome === "add-feed" || outcome === "add-chat")) {
|
|
79
|
-
designBrief.outcomeRecipe = buildOutcomeDesignRecipe(designBrief, outcome) ?? undefined;
|
|
80
|
-
}
|
|
81
82
|
const intake = intakeFor(ctx, definition.intakeQuestions(ctx), outcome, designBrief);
|
|
82
83
|
// Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
|
|
83
84
|
// for native). Best-effort — degrades to greenfield "install latest + pin" if the
|
|
@@ -119,7 +120,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
119
120
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
120
121
|
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
|
|
121
122
|
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.",
|
|
122
|
-
designContract: designContract
|
|
123
|
+
designContract: designContract ? designContractGuidance(designContract) : undefined,
|
|
123
124
|
completenessChecklist: completenessChecklistFor(outcome),
|
|
124
125
|
sdkVersion,
|
|
125
126
|
};
|
|
@@ -143,7 +144,7 @@ function completenessChecklistFor(outcome) {
|
|
|
143
144
|
// references `var(--x)` / maps it per platform); inferred tokens carry their
|
|
144
145
|
// raw value plus a usage count and an explicit "inferred" marker so they are
|
|
145
146
|
// never mistaken for authoritative brand values.
|
|
146
|
-
function designContractGuidance(contract
|
|
147
|
+
function designContractGuidance(contract) {
|
|
147
148
|
const byCategory = (category) => contract.tokens
|
|
148
149
|
.filter((token) => token.category === category)
|
|
149
150
|
.map((token) => token.provenance === "declared" && token.name
|
|
@@ -168,7 +169,6 @@ function designContractGuidance(contract, brief) {
|
|
|
168
169
|
breakpoints: contract.breakpoints.map((breakpoint) => breakpoint.raw),
|
|
169
170
|
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.`,
|
|
170
171
|
advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
|
|
171
|
-
brief,
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
function intentFor(request, interpretation) {
|
package/package.json
CHANGED