@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 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).
@@ -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
- const firstToken = (cat) => contract.tokens.find((t) => t.category === cat && t.name !== null);
2516
- const radiusToken = firstToken("radius");
2517
- const spaceToken = firstToken("space");
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 = firstToken("shadow");
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, buildOutcomeDesignRecipe, 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,13 +71,14 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
71
71
  answers,
72
72
  });
73
73
  const definition = getOutcomeDefinition(outcome);
74
- // Design contract is loaded before intake so the brief can inform the fallback
75
- // intake question (missing primary-action token) at assembly time.
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 && designBrief ? designContractGuidance(designContract, designBrief) : undefined,
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, brief) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.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",