@bridge-ds/mcp 0.3.1 → 0.3.3

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.
@@ -433,7 +433,7 @@ function groupsPhrase(groups) {
433
433
  }
434
434
  function whoCanShipSentence(policy) {
435
435
  const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
436
- return `Only ${groupsPhrase(policy.groups)} can ship this${names ? ` (${names})` : ""}.`;
436
+ return `Only ${groupsPhrase(policy.groups)} can merge this${names ? ` (${names})` : ""}.`;
437
437
  }
438
438
  function visualImpactProblems(raw) {
439
439
  if (raw === void 0) return [];
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  VERSION,
4
4
  optionsFromArgv,
5
5
  serveStdio
6
- } from "./chunk-I3TL76GK.js";
7
- import "./chunk-X455XY4E.js";
6
+ } from "./chunk-7HYTZVRW.js";
7
+ import "./chunk-K3HY24MZ.js";
8
8
 
9
9
  // src/index.ts
10
10
  var argv = process.argv.slice(2);
package/dist/lib.js CHANGED
@@ -23,12 +23,12 @@ import {
23
23
  parseDocsReply,
24
24
  requests_exports,
25
25
  serveStdio
26
- } from "./chunk-I3TL76GK.js";
26
+ } from "./chunk-7HYTZVRW.js";
27
27
  import {
28
28
  CONFIG_FILE,
29
29
  loadConfig,
30
30
  toConfigJson
31
- } from "./chunk-X455XY4E.js";
31
+ } from "./chunk-K3HY24MZ.js";
32
32
  export {
33
33
  BridgeContext,
34
34
  CONFIG_FILE,
package/dist/ops.d.ts CHANGED
@@ -651,6 +651,7 @@ export declare function auditOp(ctx: BridgeContext, a: {
651
651
  label?: string;
652
652
  limit?: number;
653
653
  findings?: boolean;
654
+ allowlist?: string;
654
655
  }): Promise<{
655
656
  next: string;
656
657
  findings?: {
@@ -671,23 +672,6 @@ export declare function auditOp(ctx: BridgeContext, a: {
671
672
  library: string;
672
673
  }[];
673
674
  }[] | undefined;
674
- byCategory: Partial<Record<import("@bridge-ds/core/audit").FindingCategory, import("@bridge-ds/core/audit").CategoryCount>>;
675
- components: {
676
- ds: Record<string, number>;
677
- foreign: {
678
- [k: string]: {
679
- instances: number;
680
- top: string[];
681
- };
682
- };
683
- native: Record<string, number>;
684
- recreated: {
685
- name: string;
686
- file: string;
687
- line: number;
688
- instances: number;
689
- }[];
690
- };
691
675
  topFiles: {
692
676
  path: string;
693
677
  hardcoded: number;
@@ -710,6 +694,37 @@ export declare function auditOp(ctx: BridgeContext, a: {
710
694
  category: import("@bridge-ds/core/audit").FindingCategory;
711
695
  count: number;
712
696
  })[];
697
+ ignoreErrors?: {
698
+ line: number;
699
+ message: string;
700
+ file: string;
701
+ }[] | undefined;
702
+ ignored?: {
703
+ total: number;
704
+ comment: number;
705
+ allowlist: number;
706
+ items: (import("@bridge-ds/core/audit").IgnoredItem & {
707
+ file: string;
708
+ })[];
709
+ staleAllowlist: import("@bridge-ds/core/audit").AllowlistEntry[];
710
+ } | undefined;
711
+ byCategory: Partial<Record<import("@bridge-ds/core/audit").FindingCategory, import("@bridge-ds/core/audit").CategoryCount>>;
712
+ components: {
713
+ ds: Record<string, number>;
714
+ foreign: {
715
+ [k: string]: {
716
+ instances: number;
717
+ top: string[];
718
+ };
719
+ };
720
+ native: Record<string, number>;
721
+ recreated: {
722
+ name: string;
723
+ file: string;
724
+ line: number;
725
+ instances: number;
726
+ }[];
727
+ };
713
728
  sinceBaseline?: {
714
729
  tokenAdoption: {
715
730
  before: number | null;
@@ -7,7 +7,7 @@ import {
7
7
  loadConfig,
8
8
  selectTokenFiles,
9
9
  showFile
10
- } from "./chunk-X455XY4E.js";
10
+ } from "./chunk-K3HY24MZ.js";
11
11
 
12
12
  // ../visual/src/run.ts
13
13
  import { existsSync as existsSync3 } from "node:fs";
@@ -123,6 +123,148 @@ function impactedModules(index, input) {
123
123
  return { modules, globalHits };
124
124
  }
125
125
 
126
+ // ../core/src/component-spec.ts
127
+ var COMPONENT_SPECS_VERSION = 1;
128
+ var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
129
+ function parseComponentSpecs(text) {
130
+ let data;
131
+ try {
132
+ data = JSON.parse(text);
133
+ } catch {
134
+ throw new Error("component-specs.json is not valid JSON.");
135
+ }
136
+ const f = data;
137
+ if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
138
+ if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
139
+ return f;
140
+ }
141
+
142
+ // ../core/src/fidelity-references.ts
143
+ var FIGMA_REFERENCES_DIR = "figma-references";
144
+ var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
145
+ var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
146
+ var FIGMA_FILL_WIDTH_TOKEN = "size.80";
147
+ var STATE_PSEUDO = {
148
+ default: "rest",
149
+ rest: "rest",
150
+ enabled: "rest",
151
+ hover: "hover",
152
+ hovered: "hover",
153
+ pressed: "active",
154
+ active: "active",
155
+ focus: "focus-visible",
156
+ focused: "focus-visible",
157
+ "focus visible": "focus-visible",
158
+ disabled: "disabled"
159
+ };
160
+ var iconName = (component) => component.replace(/^Icon\//, "");
161
+ var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
162
+ function planReference(spec, variant, file) {
163
+ if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
164
+ const props = { ...spec.fixed ?? {} };
165
+ let pseudo;
166
+ for (const axis of spec.axes) {
167
+ const value = variant.props[axis.name];
168
+ if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
169
+ if (axis.prop) {
170
+ props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
171
+ continue;
172
+ }
173
+ if (axis.name === "Checked") {
174
+ props.checked = value === "true";
175
+ if (value === "mixed") props.indeterminate = true;
176
+ continue;
177
+ }
178
+ if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
179
+ const s = STATE_PSEUDO[value.toLowerCase()];
180
+ if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
181
+ if (s === "disabled") props.disabled = true;
182
+ else if (s !== "rest") pseudo = s;
183
+ }
184
+ const root = spec.parts.find((p) => !p.parent);
185
+ if (!root) return { ok: false, reason: "the spec has no root layer" };
186
+ const children = (id) => spec.parts.filter((p) => p.parent === id);
187
+ const textOf = (part) => {
188
+ const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
189
+ if (part.swap && swapTo) {
190
+ const slot = file?.slots?.find((s) => s.name === swapTo);
191
+ if (!slot) return { reason: `${swapTo} is not in the specs` };
192
+ if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
193
+ return slot.text.default;
194
+ }
195
+ const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
196
+ return typeof prop?.default === "string" ? prop.default : part.name;
197
+ };
198
+ let text;
199
+ const icons = {};
200
+ for (const prop of spec.properties) {
201
+ if (prop.type === "TEXT") {
202
+ const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
203
+ if (hidden && prop.prop) continue;
204
+ if (prop.prop === "children") text = String(prop.default);
205
+ else if (prop.prop) props[prop.prop] = String(prop.default);
206
+ continue;
207
+ }
208
+ const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
209
+ if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
210
+ }
211
+ for (const part of spec.parts) {
212
+ if (part.kind !== "icon" || !part.swap) continue;
213
+ const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
214
+ if (!shown) continue;
215
+ if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
216
+ icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
217
+ }
218
+ let parts;
219
+ const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
220
+ const frames = children(root.id).filter((p) => p.kind === "frame");
221
+ const compound = text === void 0 && frames.length > 0 && frames.every((f) => spec.parts.some((p) => p.parent === f.id && p.kind === "text" && (p.swap || p.text === f.name)));
222
+ if (compound) {
223
+ parts = [];
224
+ for (const f of frames) {
225
+ const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
226
+ const value = textOf(t);
227
+ if (typeof value !== "string") return { ok: false, reason: value.reason };
228
+ parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
229
+ }
230
+ }
231
+ for (const p of loose) {
232
+ if (p.name === "Value") continue;
233
+ const part = spec.parts.find((x) => x.text === p.name);
234
+ if (compound && part && part.parent !== root.id) continue;
235
+ if (part && /placeholder$/.test(part.id)) {
236
+ props.placeholder = String(p.default);
237
+ continue;
238
+ }
239
+ return { ok: false, reason: `the ${p.name} text has no matching option in code` };
240
+ }
241
+ const example = spec.example;
242
+ const exampleChildren = example?.children?.length ? example.children : void 0;
243
+ return {
244
+ ok: true,
245
+ render: {
246
+ component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
247
+ props: { ...example?.p ?? {}, ...props },
248
+ ...exampleChildren ? { children: exampleChildren } : {},
249
+ ...text !== void 0 && !exampleChildren ? { text } : {},
250
+ ...parts && !exampleChildren ? { parts } : {},
251
+ ...Object.keys(icons).length ? { icons } : {},
252
+ ...pseudo ? { pseudo } : {},
253
+ ...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
254
+ }
255
+ };
256
+ }
257
+ function referenceImagePath(component, key) {
258
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
259
+ const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
260
+ return `${slug(component)}/${file || "default"}.png`;
261
+ }
262
+ function planReferences(file) {
263
+ return file.components.flatMap(
264
+ (spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
265
+ );
266
+ }
267
+
126
268
  // ../visual/src/capture.ts
127
269
  import { mkdir, writeFile } from "node:fs/promises";
128
270
  import { dirname } from "node:path";
@@ -146,8 +288,8 @@ async function launch(o = {}) {
146
288
  args: ["--lang=en-US", "--disable-gpu", "--font-render-hinting=none", "--disable-lcd-text", "--force-color-profile=srgb", "--hide-scrollbars", "--disable-skia-runtime-opts", "--disable-renderer-backgrounding"]
147
289
  });
148
290
  }
149
- async function newContext(browser, viewport) {
150
- const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
291
+ async function newContext(browser, viewport, deviceScaleFactor = 1) {
292
+ const ctx = await browser.newContext({ viewport, deviceScaleFactor, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
151
293
  await ctx.addInitScript(INIT_SCRIPT);
152
294
  await ctx.route(
153
295
  /^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
@@ -580,9 +722,9 @@ function selectImpact(input) {
580
722
  app
581
723
  };
582
724
  }
583
- function storyId(title, exportName) {
725
+ function storyId(title, exportName2) {
584
726
  const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
585
- const startCase = exportName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
727
+ const startCase = exportName2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
586
728
  return `${sanitize(title)}--${sanitize(startCase)}`;
587
729
  }
588
730
  function storiesFromSource(files) {
@@ -1096,12 +1238,106 @@ async function runImpact(o) {
1096
1238
  log(`${result.summary} Manifest: ${relative2(process.cwd(), result.manifestPath) || result.manifestPath}`);
1097
1239
  return result;
1098
1240
  }
1241
+
1242
+ // ../visual/src/references.ts
1243
+ import { existsSync as existsSync4 } from "node:fs";
1244
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
1245
+ import { dirname as dirname3, join as join4 } from "node:path";
1246
+ async function captureFigmaReferences(o) {
1247
+ const log = o.log ?? (() => void 0);
1248
+ const scale = o.scale ?? 2;
1249
+ const brand = o.brand ?? "bridge";
1250
+ const theme = o.theme ?? "light";
1251
+ if (!existsSync4(join4(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
1252
+ const index = JSON.parse(await readFile4(join4(o.storybook, "index.json"), "utf8"));
1253
+ if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
1254
+ const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
1255
+ const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
1256
+ const manifest = {
1257
+ version: 1,
1258
+ tool: "@bridge-ds/visual",
1259
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1260
+ sha: o.sha,
1261
+ scale,
1262
+ mode: { brand, theme, density: "comfortable" },
1263
+ storyId: FIGMA_REFERENCE_STORY,
1264
+ components: {}
1265
+ };
1266
+ for (const j of jobs) manifest.components[j.component] ??= { specHash: j.specHash, variants: {} };
1267
+ let captured = 0;
1268
+ let skipped = 0;
1269
+ const served = await serveDir(o.storybook);
1270
+ const browser = await launch();
1271
+ try {
1272
+ const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
1273
+ const page = await newPage(ctx);
1274
+ await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
1275
+ await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
1276
+ await settle(page, { busy: false });
1277
+ log(`Capturing ${jobs.length} variants at ${scale}x (${brand} ${theme})`);
1278
+ for (const j of jobs) {
1279
+ const variants = manifest.components[j.component].variants;
1280
+ if (!j.plan.ok) {
1281
+ variants[j.key] = { skipped: j.plan.reason };
1282
+ skipped++;
1283
+ continue;
1284
+ }
1285
+ const plan = JSON.stringify(j.plan.render);
1286
+ await page.evaluate(
1287
+ ({ id, plan: plan2 }) => new Promise((resolve2) => {
1288
+ const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
1289
+ const done = () => {
1290
+ clearTimeout(timer);
1291
+ ch.off("storyRendered", done);
1292
+ resolve2();
1293
+ };
1294
+ const timer = setTimeout(done, 1e4);
1295
+ ch.on("storyRendered", done);
1296
+ ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
1297
+ }),
1298
+ { id: FIGMA_REFERENCE_STORY, plan }
1299
+ );
1300
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
1301
+ const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
1302
+ let target = page.locator("[data-fidelity-root] > *").first();
1303
+ if (!error && !await target.count()) {
1304
+ const marked = page.locator("[data-fidelity-target]").first();
1305
+ await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
1306
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
1307
+ target = marked;
1308
+ }
1309
+ if (error || !await target.count()) {
1310
+ variants[j.key] = { skipped: error ?? "the code example did not render" };
1311
+ skipped++;
1312
+ continue;
1313
+ }
1314
+ const file = referenceImagePath(j.component, j.key);
1315
+ const path = join4(o.out, file);
1316
+ await mkdir4(dirname3(path), { recursive: true });
1317
+ await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
1318
+ const box = await target.boundingBox();
1319
+ variants[j.key] = { file, width: Math.round(box?.width ?? 0), height: Math.round(box?.height ?? 0) };
1320
+ captured++;
1321
+ }
1322
+ await ctx.close();
1323
+ } finally {
1324
+ await browser.close().catch(() => void 0);
1325
+ await served.close();
1326
+ }
1327
+ await mkdir4(o.out, { recursive: true });
1328
+ const manifestPath = join4(o.out, "manifest.json");
1329
+ await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
1330
+ `);
1331
+ log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
1332
+ return { manifest, manifestPath, captured, skipped };
1333
+ }
1099
1334
  export {
1100
1335
  COMMENT_MARKER,
1101
1336
  DEFAULT_ROUTES,
1102
1337
  DEFAULT_TENANTS,
1103
1338
  boxesFromCells,
1104
1339
  boxesFromDiff,
1340
+ captureFigmaReferences,
1105
1341
  changedTokens,
1106
1342
  decodePng,
1107
1343
  diffPngs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "Bridge MCP server: lets Claude (Claude Code, Claude Desktop or any MCP client) read, check and propose changes to a DTCG design system repo through pull requests.",
6
6
  "license": "MIT",
@@ -54,8 +54,8 @@
54
54
  "build:package": "node ../../scripts/build-packages.mjs mcp"
55
55
  },
56
56
  "dependencies": {
57
- "@bridge-ds/core": "^0.4.0",
58
- "@bridge-ds/visual": "^0.2.2",
57
+ "@bridge-ds/core": "^0.6.0",
58
+ "@bridge-ds/visual": "^0.3.0",
59
59
  "@modelcontextprotocol/sdk": "1.31.0",
60
60
  "typescript": "5.9.3",
61
61
  "zod": "3.25.76"