@calibrate-ds/cli 0.1.89 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,7 +150,7 @@ ptb stamp component Button # mark Button as implemented against the current desi
150
150
 
151
151
  ## MCP Server — AI IDE Integration
152
152
 
153
- PTB exposes 22 tools over a stdio MCP server. Your AI IDE can query component design context, run scans, generate code, and manage stamps — all from chat. Everything you can do in the terminal, you can do from chat.
153
+ PTB exposes 23 tools over a stdio MCP server. Your AI IDE can query component design context, run scans, generate code, and manage stamps — all from chat. Everything you can do in the terminal, you can do from chat.
154
154
 
155
155
  ### Available tools
156
156
 
@@ -768,6 +768,6 @@ See [PTB.md](./PTB.md) for architecture, type definitions, the generator interfa
768
768
  ```bash
769
769
  pnpm install
770
770
  pnpm build
771
- pnpm test # 593 tests across core, generator, and CLI
771
+ pnpm test # 609 tests across core, generator, and CLI
772
772
  node apps/cli/dist/index.js --help
773
773
  ```
@@ -562,7 +562,7 @@ export function createInitCommand() {
562
562
  console.log(` \x1b[36mptb export context\x1b[0m \x1b[2m— write .ptb/context/ files for AI IDEs (commit these)\x1b[0m`);
563
563
  console.log(` \x1b[2mNext.js App Router? Add \x1b[0m\x1b[33m"useClient": true\x1b[0m\x1b[2m to the \x1b[0m\x1b[33mgeneration\x1b[0m\x1b[2m section of ptb.config.json\x1b[0m`);
564
564
  console.log(` \x1b[2mso every generated component emits \x1b[0m\x1b[36m"use client"\x1b[0m\x1b[2m as its first line.\x1b[0m\n`);
565
- console.log(` ${step++}. Connect your AI IDE via MCP — PTB exposes 22 tools (scan, generate, inspect, stamp, and more):\n`);
565
+ console.log(` ${step++}. Connect your AI IDE via MCP — PTB exposes 23 tools (scan, generate, inspect, stamp, and more):\n`);
566
566
  console.log(` \x1b[1mClaude Code:\x1b[0m already configured via \x1b[36m.mcp.json\x1b[0m — just restart Claude Code.\n`);
567
567
  console.log(` \x1b[1mOther IDEs\x1b[0m (Cursor, Windsurf, Antigravity, VS Code):`);
568
568
  console.log(` Once Claude Code is connected, ask in chat: \x1b[36m"set up MCP for cursor"\x1b[0m`);
@@ -299,6 +299,10 @@ TYPICAL WORKFLOW:
299
299
  3. Read contextFile for the full implementation brief (it contains promptText, the
300
300
  render tree, and variantDiffs — these are intentionally NOT inlined in this
301
301
  response because they exceed the tool-call size limit on large components)
302
+ 3b. For a multi-variant component set, call get_variant_tokens("<name>") for the
303
+ compact per-variant build matrix — every variant's resolved --ptb-* bindings,
304
+ colors, radius, padding, and gap in one small response. Prefer this over
305
+ excavating variantDiffs from the contextFile.
302
306
  4. Implement the requested component
303
307
  5. Call submit_work("<name>") when done`, { name: z.string().describe("Component to start working on, e.g. 'SearchBar'") }, async ({ name }) => {
304
308
  let ctx;
@@ -401,11 +405,17 @@ TYPICAL WORKFLOW:
401
405
  options: ax.options ?? [],
402
406
  defaultValue: ax.defaultValue,
403
407
  })),
408
+ // B-5: emit real binding data, not just slot names. The old map
409
+ // read b.token/b.cssVar/b.collection — fields that don't exist on
410
+ // a TokenBinding — so every entry serialized to just {slot}. The
411
+ // actual value (rawValue) and token identity live on resolvedToken.
404
412
  tokenBindings: (context.tokenBindings ?? []).map((b) => ({
405
413
  slot: b.slot,
406
- token: b.token,
407
- cssVar: b.cssVar,
408
- collection: b.collection,
414
+ rawValue: b.rawValue ?? null,
415
+ token: b.resolvedToken?.name ?? b.boundToken?.name ?? null,
416
+ cssVar: b.resolvedToken?.cssVariable ?? null,
417
+ collection: b.resolvedToken?.collection?.name ?? null,
418
+ bound: !!b.boundToken,
409
419
  })),
410
420
  dependencies: context.dependencies.map((d) => ({
411
421
  name: d.name,
@@ -694,11 +704,17 @@ recursively when you call it for that dependency.`, { name: z.string().describe(
694
704
  options: ax.options ?? [],
695
705
  defaultValue: ax.defaultValue,
696
706
  })),
707
+ // B-5: emit real binding data, not just slot names. The old map
708
+ // read b.token/b.cssVar/b.collection — fields that don't exist on
709
+ // a TokenBinding — so every entry serialized to just {slot}. The
710
+ // actual value (rawValue) and token identity live on resolvedToken.
697
711
  tokenBindings: (context.tokenBindings ?? []).map((b) => ({
698
712
  slot: b.slot,
699
- token: b.token,
700
- cssVar: b.cssVar,
701
- collection: b.collection,
713
+ rawValue: b.rawValue ?? null,
714
+ token: b.resolvedToken?.name ?? b.boundToken?.name ?? null,
715
+ cssVar: b.resolvedToken?.cssVariable ?? null,
716
+ collection: b.resolvedToken?.collection?.name ?? null,
717
+ bound: !!b.boundToken,
702
718
  })),
703
719
  dependencies: context.dependencies.map((d) => ({
704
720
  name: d.name,
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { z } from "zod";
4
- import { getComponentContext } from "@calibrate-ds/core";
4
+ import { getComponentContext, buildVariantTokenMap } from "@calibrate-ds/core";
5
5
  function toSlug(name) {
6
6
  return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7
7
  }
@@ -201,6 +201,33 @@ EVERYTHING YOU NEED IS IN PTB:
201
201
  catch { /* non-fatal */ }
202
202
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
203
203
  });
204
+ server.tool("get_variant_tokens", `Get the compact per-variant token/style build matrix for a component set.
205
+
206
+ For EVERY variant (all combinations of the variant axes), returns the resolved
207
+ design-system bindings and spacing needed to implement it:
208
+ - background / text-color / border-color → { cssVar (--ptb-*), value (hex), token }
209
+ - border-radius, padding, gap → resolved values (px)
210
+ - per-node text content, stroke weight + align (inside/outside)
211
+
212
+ This is the data an agent needs to build all variants WITHOUT reading the large
213
+ context render tree or reconstructing it from variantDiffs. Emit each slot as
214
+ var(<cssVar>, <value>) when cssVar is present; use value directly when it is not.
215
+ Returns variantCount:0 with a note for single (non-set) components.`, { name: z.string().describe("Component set name, e.g. 'ChipsWeb'") }, async ({ name }) => {
216
+ let ctx;
217
+ try {
218
+ ctx = await getContext();
219
+ }
220
+ catch (e) {
221
+ return { content: [{ type: "text", text: e.message }] };
222
+ }
223
+ const comp = findComponent(ctx.snapshot, name);
224
+ if (!comp) {
225
+ const available = ctx.snapshot.components.map((c) => c.name).slice(0, 15).join(", ");
226
+ return { content: [{ type: "text", text: `Component '${name}' not found.\nAvailable (first 15): ${available}` }] };
227
+ }
228
+ const map = buildVariantTokenMap(comp);
229
+ return { content: [{ type: "text", text: JSON.stringify(map, null, 2) }] };
230
+ });
204
231
  },
205
232
  };
206
233
  //# sourceMappingURL=component-tools.js.map
@@ -3,6 +3,17 @@ import * as path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { resolveToken } from "@calibrate-ds/core";
5
5
  const UPGRADE_CHECKLIST = {
6
+ "0.1.90": [
7
+ "BAKE-OFF audit fixes (docs/PTB_VS_FIGMA_BAKEOFF.md). A cold PTB-only vs Figma-only head-to-head exposed that PTB's verifier declined to enforce the one thing PTB uniquely provides: design-system token binding.",
8
+ "FIX (B-1, the moat gate): verify computed cssVarScore (fraction of the design's bound --ptb-* vars present in the CSS) then NEVER read it — a token-blind component that renders correctly but binds ZERO design tokens PASSED. cssVarScore is now a HARD GATE: cssVarScore === 0 → fail, and tokenScore cannot rescue it. cssVarScore is now surfaced in the run_verify / get_verify_report responses.",
9
+ "FIX (B-1, naming): tokenScore was misnamed — it measures rendered TEXT content, not token binding. The response now also returns textContentScore (the accurate name); tokenScore is kept as a deprecated alias so nothing breaks.",
10
+ "FIX (B-2, scaffold CSS): a token-bound border emitted 'border: var(--x, #hex)' — a bare color with no width/style, which is INVALID and dropped, leaving outlined components with no border. Now emits 'border: 1px solid var(...)'. Also: slots that map to the same CSS property (text/fill/iconColor → color) no longer emit a duplicate declaration. Regenerate components (generate-components --force) to pick these up.",
11
+ "FIX (B-5): start_work's tokenBindings returned just {slot} with no values (it read fields that don't exist on the type). It now returns rawValue, token name, cssVar, collection, and bound flag.",
12
+ "NEW TOOL (B-3, the cost win): get_variant_tokens(name) returns a compact PRE-RESOLVED per-variant token matrix — for every variant, the --ptb-* binding + value for background/text/border, plus radius/padding/gap/text and stroke weight+align. No more scripting through variantDiffs: ChipsWeb's full 42-variant matrix is ~6k tokens (vs ~86k to read context naively). start_work now points agents to it for multi-variant sets. Restart your MCP server to see the new tool.",
13
+ "FIX (B-4): Storybook loaded no fonts, so text rendered in a fallback and CAPPED every text component's pixel score (~0.82) regardless of correctness. setup now writes .storybook/preview-head.html loading the design's fonts (from typography.css) via Google Fonts. Re-run Storybook setup / ptb init to get it.",
14
+ "FIX (B-8): per-variant verify now names how many variants had no reference thumbnail and were skipped (was silently undercounting). FIX (B-9): the primary diff image is retained + its path kept in the verify report, so a low score is inspectable (was deleting all pngs).",
15
+ "B-7 strokeAlign is now available to the agent via get_variant_tokens (weight+align). Deferred (verified, not changed): B-2 double-padding, B-6 radius alias tier (risky core change), B-10 unbound raw hexes (design-side, not a PTB bug). Restart MCP; regenerate components for the CSS fixes. 609 tests.",
16
+ ],
6
17
  "0.1.89": [
7
18
  "INDEPENDENCE release — PTB no longer recommends or depends on any external tool. Previously several MCP tool descriptions told the agent to 'cross-check with Figma MCP' (get_variable_defs / get_screenshot) for exact hex and visual truth.",
8
19
  "That guidance was redundant: PTB already has that data from its own scan. tokenConflicts now direct the agent to rawValue — the hex the design actually renders, captured by the scan — with var(--ptb-token, <rawValue>) as the fallback; and the scan thumbnail at thumbnailPath is the visual ground truth for colors/spacing/arrangement.",
@@ -86,6 +86,16 @@ Returns: pixel match score, AI assessment ("matches" / "minor-differences" / "si
86
86
  passFail,
87
87
  verdictScore,
88
88
  verdictMetric,
89
+ // B-1: cssVarScore is the design-system BINDING signal — fraction
90
+ // of the design's bound --ptb-* vars present in the CSS. 0 = the
91
+ // component renders correctly but is disconnected from the design
92
+ // system (hard fail via the verdict gate). Surfaced so agents/users
93
+ // can see it, not just its warning string.
94
+ cssVarScore: result.cssVarScore ?? null,
95
+ // textContentScore is the accurate name for what tokenScore measures
96
+ // (rendered TEXT content, NOT token binding). tokenScore is a
97
+ // deprecated alias kept for existing consumers.
98
+ textContentScore: result.textContentScore ?? result.tokenScore ?? null,
89
99
  tokenScore: result.tokenScore ?? null,
90
100
  structureScore: result.structureScore ?? null,
91
101
  pixelMatchScore: score,
@@ -122,6 +132,8 @@ Returns: pixel match score, AI assessment ("matches" / "minor-differences" / "si
122
132
  const summary = {
123
133
  component: report.componentName,
124
134
  checkedAt: report.checkedAt,
135
+ cssVarScore: report.cssVarScore ?? null,
136
+ textContentScore: report.textContentScore ?? report.tokenScore ?? null,
125
137
  pixelMatchScore: report.pixelMatch?.score ?? null,
126
138
  assessment: report.aiFeedback?.overallAssessment ?? null,
127
139
  summary: report.aiFeedback?.summary ?? null,
@@ -493,6 +493,17 @@ export async function runVerify(component, config, snapshot, options = {}) {
493
493
  // warning gate can check whether per-variant thumbnails are available before
494
494
  // deciding whether to fire a "no design reference" warning.
495
495
  const variantThumbs = component.variantThumbnails ?? [];
496
+ // B-8: the true number of variants in the set (every variant child), which can
497
+ // exceed the number of reference thumbnails when some variants failed to export.
498
+ // Previously the "capped at N of M" message used only the thumbnail count, so
499
+ // missing variants were silently unaccounted for. Surface the real gap.
500
+ const declaredVariantCount = component.variantNodeRefs?.length ??
501
+ component.variantSnapshots?.length ??
502
+ variantThumbs.length;
503
+ if (declaredVariantCount > variantThumbs.length) {
504
+ warnings.push(`${declaredVariantCount - variantThumbs.length} of ${declaredVariantCount} variants have no reference ` +
505
+ `thumbnail and were not pixel-verified (re-run 'ptb scan' to regenerate variant references).`);
506
+ }
496
507
  // GAP-022: prefer the single-variant reference for pixel-diff (a single-variant
497
508
  // story can't match the full variant grid). Fall back to the grid thumbnail.
498
509
  const thumbnailPath = component.variantThumbnailPath ?? component.thumbnailPath ?? null;
@@ -621,6 +632,9 @@ export async function runVerify(component, config, snapshot, options = {}) {
621
632
  report.designTokenCount = designTokens.length;
622
633
  report.foundTokenCount = found.length;
623
634
  report.tokenScore = parseFloat((found.length / designTokens.length).toFixed(4));
635
+ // B-1: same value under the accurate name (tokenScore measures
636
+ // rendered TEXT content, not token binding). tokenScore kept as alias.
637
+ report.textContentScore = report.tokenScore;
624
638
  }
625
639
  }
626
640
  }
@@ -756,14 +770,23 @@ export async function runVerify(component, config, snapshot, options = {}) {
756
770
  }
757
771
  }
758
772
  // ── Step 5: Cleanup PNG artifacts ───────────────────────────────────────
759
- // All scores and feedback are in report.json; the PNGs are no longer needed.
760
- // At scale (100+ components × 20 variants) keeping them wastes significant disk.
773
+ // B-9: retain the single primary diff image so a low score is inspectable —
774
+ // previously ALL pngs were deleted and screenshots.diff nulled, so an agent
775
+ // seeing a low pixelMatch had no way to see WHERE it diverged (it had to find
776
+ // report.json on disk and re-run Playwright by hand). The figma/impl and
777
+ // per-variant pngs are still removed; one diff per component is negligible disk.
761
778
  // Thumbnails live in .ptb/thumbnails/ — outside verifyDir — and are not touched.
762
- report.screenshots = { figma: null, figmaSource: report.screenshots.figmaSource, impl: null, diff: null };
779
+ const keepDiff = report.screenshots.diff ? path.basename(report.screenshots.diff) : null;
780
+ report.screenshots = {
781
+ figma: null,
782
+ figmaSource: report.screenshots.figmaSource,
783
+ impl: null,
784
+ diff: report.screenshots.diff ?? null,
785
+ };
763
786
  // Persist report
764
787
  const reportPath = path.join(verifyDir, "report.json");
765
788
  await fs.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
766
- const pngs = (await fs.readdir(verifyDir)).filter(e => e.endsWith(".png"));
789
+ const pngs = (await fs.readdir(verifyDir)).filter(e => e.endsWith(".png") && e !== keepDiff);
767
790
  await Promise.all(pngs.map(e => fs.unlink(path.join(verifyDir, e)).catch(() => undefined)));
768
791
  return { ...report, warnings };
769
792
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calibrate-ds/cli",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,10 +44,10 @@
44
44
  "commander": "^14.0.3",
45
45
  "openai": "^6.34.0",
46
46
  "zod": "^4.3.6",
47
- "@calibrate-ds/core": "^0.1.89",
48
- "@calibrate-ds/figma-client": "^0.1.89",
49
- "@calibrate-ds/generator-react": "^0.1.89",
50
- "@calibrate-ds/shared-types": "^0.1.89"
47
+ "@calibrate-ds/core": "^0.1.90",
48
+ "@calibrate-ds/figma-client": "^0.1.90",
49
+ "@calibrate-ds/generator-react": "^0.1.90",
50
+ "@calibrate-ds/shared-types": "^0.1.90"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.13.5",