@bridge-ds/mcp 0.3.1 → 0.3.2

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
@@ -69,7 +69,7 @@ Working on the server itself: `npm run mcp:build` in a clone of this repo builds
69
69
  | `bridge_get_token_history` | Every change of one token, before and after per mode |
70
70
  | `bridge_plan_revert` | Dry run of a revert (or a one-token restore) against the current base, conflicts shown |
71
71
  | `bridge_propose_revert` | The revert as a local bridge/* branch or a draft PR labelled revert |
72
- | `bridge_audit` | Adoption in a product folder or file (local): token and component adoption, hardcoded values with the closest token, foreign components with the DS one to use, top files and fixes |
72
+ | `bridge_audit` | Adoption in a product folder or file (local): CSS, SCSS, Less, TS/JS, TSX/JSX, Astro, Vue and Svelte. Token and component adoption, hardcoded values with the closest token, foreign and native components with the DS one to use, top files and fixes; `allowlist` and `bridge-audit-ignore-next-line: reason` comments leave known exceptions out, listed under `ignored` |
73
73
  | `bridge_list_requests` | Component change requests (issues labeled `component-change`) with status and linked pull requests |
74
74
  | `bridge_get_request` | One request with the component's files and a brief to implement it in a pull request that says "Closes #n" |
75
75
  | `bridge_create_request` | File a request as an issue labeled `component-change` (token required; `dryRun` previews; `exploration` targets an exploration branch) |
@@ -774,6 +774,12 @@ function classifyAuth(status, detail, opts = {}) {
774
774
  }
775
775
  return {};
776
776
  }
777
+ function rateLimitMessage(x) {
778
+ const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
779
+ if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
780
+ const hint = x.tokenHint ?? "add a GitHub token";
781
+ return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
782
+ }
777
783
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
778
784
  var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
779
785
  var API = "https://api.github.com";
@@ -877,12 +883,8 @@ var GitHubClient = class _GitHubClient {
877
883
  const remaining = res.headers?.get("x-ratelimit-remaining");
878
884
  const reset = res.headers?.get("x-ratelimit-reset");
879
885
  if ((res.status === 403 || res.status === 429) && remaining === "0") {
880
- const when = reset ? new Date(Number(reset) * 1e3) : void 0;
881
- throw new GitHubError(
882
- 429,
883
- `GitHub's hourly request limit for this token is used up.${when ? ` It resets at ${when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : ""} Try again after that.`,
884
- detail
885
- );
886
+ const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
887
+ throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
886
888
  }
887
889
  const info = classifyAuth(res.status, detail, {
888
890
  path,
@@ -1853,10 +1855,10 @@ function planRestore(input) {
1853
1855
  const from = input.label ? ` from ${input.label}` : "";
1854
1856
  const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
1855
1857
  const title = `Restore ${path} (${label})${from}`;
1856
- const item = input.item;
1858
+ const item2 = input.item;
1857
1859
  const summary2 = [
1858
1860
  `Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
1859
- ...item ? ["", `That value came from ${item.pr ? item.pr.url ? `[#${item.pr.number}](${item.pr.url})` : `#${item.pr.number}` : `commit ${item.sha.slice(0, 7)}`} ("${item.title}"), ${item.date.slice(0, 10)}, by ${item.author}.`] : []
1861
+ ...item2 ? ["", `That value came from ${item2.pr ? item2.pr.url ? `[#${item2.pr.number}](${item2.pr.url})` : `#${item2.pr.number}` : `commit ${item2.sha.slice(0, 7)}`} ("${item2.title}"), ${item2.date.slice(0, 10)}, by ${item2.author}.`] : []
1860
1862
  ].join("\n");
1861
1863
  return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary: summary2, planned, conflicts: [], skipped: [] });
1862
1864
  }
@@ -3298,29 +3300,29 @@ function sanitizeDraftText(raw) {
3298
3300
  const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
3299
3301
  return { title, summary: summary2 };
3300
3302
  }
3301
- function sanitizeDocsResult(item, r) {
3302
- const task = item.docs.task;
3303
+ function sanitizeDocsResult(item2, r) {
3304
+ const task = item2.docs.task;
3303
3305
  if (task.kind === "selection") {
3304
3306
  const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
3305
3307
  const replacement = sanitizeDocsReplacement(raw);
3306
- if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
3308
+ if (!replacement) throw new Error(`Item "${item2.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item2.id}", "replacement": "..."}.`);
3307
3309
  return { replacement };
3308
3310
  }
3309
3311
  try {
3310
3312
  const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
3311
3313
  return { docs: sections2, checked };
3312
3314
  } catch {
3313
- throw new Error(`Item "${item.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item.id}", "docs": {${task.keys.map((k) => `"${k}": ...`).join(", ")}}} in the shapes of its prompt.`);
3315
+ throw new Error(`Item "${item2.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item2.id}", "docs": {${task.keys.map((k) => `"${k}": ...`).join(", ")}}} in the shapes of its prompt.`);
3314
3316
  }
3315
3317
  }
3316
- function sanitizeResultLines(item, raw) {
3318
+ function sanitizeResultLines(item2, raw) {
3317
3319
  if (raw === void 0 || raw === null) return [];
3318
- if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
3319
- const known = new Set((item.lines ?? []).map((l) => l.id));
3320
+ if (!Array.isArray(raw)) throw new Error(`lines for "${item2.id}" must be a list of {"id", "line", "title"}.`);
3321
+ const known = new Set((item2.lines ?? []).map((l) => l.id));
3320
3322
  const out = /* @__PURE__ */ new Map();
3321
3323
  for (const l of raw) {
3322
3324
  const id = typeof l?.id === "string" ? l.id : "";
3323
- if (!known.has(id)) throw new Error(`Item "${item.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
3325
+ if (!known.has(id)) throw new Error(`Item "${item2.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
3324
3326
  const line = sanitizeDraftLine(l.line);
3325
3327
  const title = sanitizeDraftTitle(l.title);
3326
3328
  if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
@@ -3337,13 +3339,13 @@ function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
3337
3339
  const next = new Map((p.results ?? []).map((r) => [r.id, r]));
3338
3340
  for (const r of results) {
3339
3341
  if (!ids.has(r.id)) throw new Error(`Session ${p.sessionId} has no item "${r.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
3340
- const item = p.items.find((i) => i.id === r.id);
3341
- if (item.scope === "docs") {
3342
- next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item, r), by: "claude", at: at.toISOString() });
3342
+ const item2 = p.items.find((i) => i.id === r.id);
3343
+ if (item2.scope === "docs") {
3344
+ next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item2, r), by: "claude", at: at.toISOString() });
3343
3345
  continue;
3344
3346
  }
3345
3347
  const clean = sanitizeDraftText(r);
3346
- const lines = sanitizeResultLines(item, r.lines);
3348
+ const lines = sanitizeResultLines(item2, r.lines);
3347
3349
  if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
3348
3350
  next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
3349
3351
  }
@@ -3731,6 +3733,10 @@ var EQUIVALENTS = [
3731
3733
  [/^Badge$/, ["Badge"]],
3732
3734
  [/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
3733
3735
  [/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
3736
+ [/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
3737
+ [/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
3738
+ [/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
3739
+ [/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
3734
3740
  [/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
3735
3741
  [/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
3736
3742
  [/^(LinearProgress|Progress)$/, ["Progress"]],
@@ -4277,6 +4283,26 @@ var THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|ex
4277
4283
  var CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
4278
4284
  var MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
4279
4285
  var THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
4286
+ var DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
4287
+ function domRendererComponent(fn, components) {
4288
+ const m = DOM_RENDERER_RE.exec(fn);
4289
+ if (!m) return void 0;
4290
+ const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
4291
+ if (components.has(pascal2)) return pascal2;
4292
+ return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
4293
+ }
4294
+ var LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
4295
+ var LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
4296
+ var HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
4297
+ var HTML_ATTR_RE = /\s(data-tip)\s*=/g;
4298
+ function lookalikesInClassList(list, components) {
4299
+ const out = [];
4300
+ for (const token of list.split(/\s+/)) {
4301
+ const component = LOOKALIKE_CLASSES[token];
4302
+ if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
4303
+ }
4304
+ return out;
4305
+ }
4280
4306
  function kindFor(path) {
4281
4307
  return path.endsWith(".tsx") ? ts.ScriptKind.TSX : path.endsWith(".jsx") ? ts.ScriptKind.JSX : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
4282
4308
  }
@@ -4296,6 +4322,78 @@ function contextForDeclaration(name, typeText) {
4296
4322
  if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
4297
4323
  return null;
4298
4324
  }
4325
+ function importsOf(path, text) {
4326
+ const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
4327
+ const imports = /* @__PURE__ */ new Map();
4328
+ for (const node of sf.statements) {
4329
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
4330
+ const mod = node.moduleSpecifier.text;
4331
+ const c = node.importClause;
4332
+ if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
4333
+ if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
4334
+ if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) imports.set(el.name.text, { module: mod, imported: (el.propertyName ?? el.name).text });
4335
+ }
4336
+ return imports;
4337
+ }
4338
+ function classifyTag(t, imports, ds) {
4339
+ const { name } = t;
4340
+ const at = { line: t.line, column: t.column };
4341
+ if (/^[a-z][\w-]*$/.test(name)) {
4342
+ if (!ds.nativeElements.includes(name)) return {};
4343
+ if (name === "input" && t.type === "hidden") return {};
4344
+ if (name === "a" && !t.hasHref) return {};
4345
+ if (t.inRenderProp) return {};
4346
+ const suggestion = nativeEquivalent(name, t.type, ds.components);
4347
+ return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
4348
+ }
4349
+ const root = name.split(".")[0];
4350
+ const imp = imports.get(root);
4351
+ const ns = imp?.imported === "*";
4352
+ const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
4353
+ const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
4354
+ if (imp) {
4355
+ const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
4356
+ if (isNonVisual(leaf.split(".").pop())) return {};
4357
+ if (kind === "ds") return { ds: leaf };
4358
+ if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
4359
+ if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
4360
+ if (t.inRenderProp) return {};
4361
+ const suggestion = ds.components.has("Link") ? "Link" : void 0;
4362
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
4363
+ }
4364
+ if (kind === "foreign" || kind === "shadcn") {
4365
+ const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
4366
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
4367
+ }
4368
+ if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
4369
+ return {};
4370
+ }
4371
+ if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
4372
+ return {};
4373
+ }
4374
+ function tailwindFindings(hits, offset, pos, ds) {
4375
+ const findings = [];
4376
+ let themeRefs = 0;
4377
+ for (const h of hits) {
4378
+ if (h.kind === "scale") {
4379
+ themeRefs++;
4380
+ continue;
4381
+ }
4382
+ if (h.kind === "token") continue;
4383
+ let cat = h.category;
4384
+ const v = h.value;
4385
+ const isColor = Boolean(parseCssColor(v));
4386
+ if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
4387
+ else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
4388
+ else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
4389
+ else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
4390
+ if (cat === "color" && !isColor) continue;
4391
+ const suggestion = ds.suggest(cat, v);
4392
+ const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
4393
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
4394
+ }
4395
+ return { findings, themeRefs };
4396
+ }
4299
4397
  function scanCode(path, text, ds) {
4300
4398
  const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
4301
4399
  const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
@@ -4310,24 +4408,9 @@ function scanCode(path, text, ds) {
4310
4408
  out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
4311
4409
  };
4312
4410
  const addTailwind = (hits, offset) => {
4313
- for (const h of hits) {
4314
- if (h.kind === "scale") {
4315
- out.themeRefs++;
4316
- continue;
4317
- }
4318
- if (h.kind === "token") continue;
4319
- let cat = h.category;
4320
- const v = h.value;
4321
- const isColor = Boolean(parseCssColor(v));
4322
- if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
4323
- else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
4324
- else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
4325
- else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
4326
- if (cat === "color" && !isColor) continue;
4327
- const suggestion = ds.suggest(cat, v);
4328
- const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
4329
- out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
4330
- }
4411
+ const r = tailwindFindings(hits, offset, pos, ds);
4412
+ out.themeRefs += r.themeRefs;
4413
+ out.findings.push(...r.findings);
4331
4414
  };
4332
4415
  const styleValue = (key, node, ctx) => {
4333
4416
  const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
@@ -4388,13 +4471,18 @@ function scanCode(path, text, ds) {
4388
4471
  }
4389
4472
  for (const a of scan.applies) addTailwind(a.hits, start + a.start);
4390
4473
  };
4474
+ const lookalike = (list, offset) => {
4475
+ for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
4476
+ };
4391
4477
  const classLiteral = (node) => {
4392
4478
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
4393
4479
  handled.add(node);
4480
+ lookalike(node.text, node.getStart(sf) + 1);
4394
4481
  addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
4395
4482
  } else if (ts.isTemplateExpression(node)) {
4396
4483
  for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
4397
4484
  handled.add(p);
4485
+ lookalike(p.text, p.getStart(sf) + 1);
4398
4486
  addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
4399
4487
  }
4400
4488
  node.templateSpans.forEach((s) => visit(s.expression, null, true));
@@ -4403,49 +4491,13 @@ function scanCode(path, text, ds) {
4403
4491
  const jsxName = (n) => n.getText(sf);
4404
4492
  const element = (node) => {
4405
4493
  const name = jsxName(node.tagName);
4406
- const at = pos(node.getStart(sf));
4407
- if (/^[a-z][\w-]*$/.test(name)) {
4408
- if (!ds.nativeElements.includes(name)) return;
4409
- const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
4410
- const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
4411
- if (name === "input" && type === "hidden") return;
4412
- if (name === "a" && !node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href")) return;
4413
- if (isRenderElement(node)) return;
4414
- const suggestion = nativeEquivalent(name, type, ds.components);
4415
- out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
4416
- return;
4417
- }
4418
- const root = name.split(".")[0];
4419
- const imp = imports.get(root);
4420
- const ns = imp?.imported === "*";
4421
- const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
4422
- const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
4423
- if (imp) {
4424
- const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
4425
- if (isNonVisual(leaf.split(".").pop())) return;
4426
- if (kind === "ds") {
4427
- out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
4428
- return;
4429
- }
4430
- if (kind === "charts") {
4431
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
4432
- return;
4433
- }
4434
- if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
4435
- if (isRenderElement(node)) return;
4436
- const suggestion = ds.components.has("Link") ? "Link" : void 0;
4437
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
4438
- return;
4439
- }
4440
- if (kind === "foreign" || kind === "shadcn") {
4441
- const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
4442
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
4443
- return;
4444
- }
4445
- if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
4446
- return;
4447
- }
4448
- if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
4494
+ const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
4495
+ const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
4496
+ const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
4497
+ const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
4498
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
4499
+ if (r.component) out.components.push(r.component);
4500
+ if (r.localCandidate) pendingLocal.push(r.localCandidate);
4449
4501
  };
4450
4502
  const pendingLocal = [];
4451
4503
  function visit(node, ctx, classCtx) {
@@ -4505,6 +4557,17 @@ function scanCode(path, text, ds) {
4505
4557
  cssTemplate(node.template);
4506
4558
  return;
4507
4559
  }
4560
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
4561
+ const imp = imports.get(node.expression.text);
4562
+ if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
4563
+ const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
4564
+ if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
4565
+ }
4566
+ }
4567
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
4568
+ visit(node.left, ctx, classCtx);
4569
+ return classLiteral(node.right);
4570
+ }
4508
4571
  if (ts.isCallExpression(node)) {
4509
4572
  const callee = node.expression.getText(sf);
4510
4573
  if (CLASS_FNS.test(callee)) {
@@ -4549,6 +4612,14 @@ function scanCode(path, text, ds) {
4549
4612
  const literal = (node) => {
4550
4613
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
4551
4614
  const t = node.text;
4615
+ if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
4616
+ const base = node.getStart(sf) + 1;
4617
+ for (const m of t.matchAll(HTML_CLASS_RE)) for (const l of lookalikesInClassList(m[1], ds.components)) out.components.push({ ...pos(base + m.index), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
4618
+ for (const m of t.matchAll(HTML_ATTR_RE)) {
4619
+ const component = LOOKALIKE_ATTRS[m[1]];
4620
+ if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
4621
+ }
4622
+ }
4552
4623
  for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
4553
4624
  if (ds.isDsVar(m[1])) out.tokenUsages++;
4554
4625
  else out.themeRefs++;
@@ -4568,29 +4639,85 @@ function scanCode(path, text, ds) {
4568
4639
  return out;
4569
4640
  }
4570
4641
 
4571
- // ../core/src/audit/types.ts
4572
- var AUDIT_SCHEMA_VERSION = 1;
4573
- var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width", "shadow", "duration", "easing", "z-index", "font-family", "font-size", "font-weight", "line-height"];
4574
-
4575
- // ../core/src/audit/report.ts
4576
- var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts"];
4577
- var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
4578
- var IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
4579
- function isAuditable(path) {
4580
- if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
4581
- if (IGNORE_FILE.test(path)) return false;
4582
- return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
4642
+ // ../core/src/audit/ignore.ts
4643
+ var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
4644
+ function ignoreDirectives(text) {
4645
+ const at = lineCol(text);
4646
+ const out = [];
4647
+ for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
4648
+ const start = m.index;
4649
+ const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
4650
+ const open = opener?.[1];
4651
+ if (!open || open === "//" && opener[2].includes("\n")) continue;
4652
+ const from = start + m[0].length;
4653
+ let end;
4654
+ if (open === "//") {
4655
+ end = text.indexOf("\n", from);
4656
+ if (end < 0) end = text.length;
4657
+ } else {
4658
+ end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
4659
+ if (end < 0) continue;
4660
+ }
4661
+ const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
4662
+ out.push({ line: at(start).line, target: at(end).line + 1, reason });
4663
+ }
4664
+ return out;
4583
4665
  }
4584
- var langOf = (path) => {
4585
- const ext = path.slice(path.lastIndexOf(".") + 1);
4586
- if (ext === "css") return "css";
4587
- if (ext === "scss" || ext === "sass") return "scss";
4588
- if (ext === "less") return "less";
4589
- if (ext === "tsx") return "tsx";
4590
- if (ext === "jsx") return "jsx";
4591
- if (/^[cm]?ts$/.test(ext)) return "ts";
4592
- return "js";
4593
- };
4666
+ var item = (x, reason, via) => "category" in x ? { line: x.line, column: x.column, kind: "value", what: x.value, reason, via } : { line: x.line, column: x.column, kind: "component", what: x.name, reason, via };
4667
+ function applyIgnoreComments(text, findings, components) {
4668
+ const errors = [];
4669
+ const reasons = /* @__PURE__ */ new Map();
4670
+ for (const d of ignoreDirectives(text)) {
4671
+ if (!d.reason) {
4672
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
4673
+ continue;
4674
+ }
4675
+ if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
4676
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
4677
+ continue;
4678
+ }
4679
+ reasons.set(d.target, d.reason);
4680
+ }
4681
+ const ignored = [];
4682
+ const keep = (xs) => xs.filter((x) => {
4683
+ const r = reasons.get(x.line);
4684
+ if (r === void 0) return true;
4685
+ ignored.push(item(x, r, "comment"));
4686
+ return false;
4687
+ });
4688
+ return { findings: keep(findings), components: keep(components), ignored, errors };
4689
+ }
4690
+ function applyAllowlist(entries, findings, components) {
4691
+ const used = /* @__PURE__ */ new Set();
4692
+ const ignored = [];
4693
+ const keep = (xs) => xs.filter((x) => {
4694
+ const what = "category" in x ? x.value : x.name;
4695
+ const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
4696
+ let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
4697
+ if (i < 0) i = entries.findIndex(fits);
4698
+ if (i < 0) return true;
4699
+ used.add(i);
4700
+ ignored.push(item(x, entries[i].reason, "allowlist"));
4701
+ return false;
4702
+ });
4703
+ return { findings: keep(findings), components: keep(components), ignored, used };
4704
+ }
4705
+ function parseAllowlist(json) {
4706
+ const fail2 = (msg) => {
4707
+ throw new Error(`allowlist: ${msg}`);
4708
+ };
4709
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail2('expected { "entries": [...] }');
4710
+ return json.entries.map((e, i) => {
4711
+ const x = e;
4712
+ if (typeof x.file !== "string" || !x.file) fail2(`entry ${i} needs "file"`);
4713
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail2(`entry ${i} (${x.file}) needs a "line" number`);
4714
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail2(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
4715
+ if (x.what !== void 0 && typeof x.what !== "string") fail2(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
4716
+ return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
4717
+ });
4718
+ }
4719
+
4720
+ // ../core/src/audit/style.ts
4594
4721
  function isSafeFix(h, s) {
4595
4722
  if (!s || s.level !== "exact") return false;
4596
4723
  switch (h.category) {
@@ -4615,49 +4742,346 @@ function isSafeFix(h, s) {
4615
4742
  return false;
4616
4743
  }
4617
4744
  }
4618
- function auditFile(path, text, ds) {
4619
- const lang = langOf(path);
4620
- if (lang === "css" || lang === "scss" || lang === "less") {
4621
- const at = lineCol(text);
4622
- const scan = scanCssText(text, lang);
4623
- const findings = [];
4624
- let tokenUsages = 0;
4625
- let themeRefs = 0;
4626
- for (const d of scan.decls) {
4627
- for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
4628
- if (d.scan.themeRef) themeRefs++;
4629
- else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
4630
- const property = d.property.toLowerCase();
4631
- for (const h of d.scan.hits) {
4632
- const suggestion = ds.suggest(h.category, h.text, property);
4633
- const p = at(d.valueStart + h.index);
4634
- findings.push({ ...p, category: h.category, property, value: h.text, source: "css", ...suggestion ? { suggestion } : {}, ...isSafeFix(h, suggestion) ? { autofix: true } : {} });
4745
+ function auditCssText(text, lang, ds, opts) {
4746
+ const base = opts.offset ?? 0;
4747
+ const source = opts.source ?? "css";
4748
+ const at = (o) => opts.pos(base + o);
4749
+ const scan = scanCssText(text, lang);
4750
+ const findings = [];
4751
+ let tokenUsages = 0;
4752
+ let themeRefs = 0;
4753
+ for (const d of scan.decls) {
4754
+ for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
4755
+ if (d.scan.themeRef) themeRefs++;
4756
+ else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
4757
+ const property = d.property.toLowerCase();
4758
+ for (const h of d.scan.hits) {
4759
+ const suggestion = ds.suggest(h.category, h.text, property);
4760
+ const autofix = source === "css" && isSafeFix(h, suggestion);
4761
+ findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
4762
+ }
4763
+ }
4764
+ for (const a of scan.applies) {
4765
+ for (const h of a.hits) {
4766
+ if (h.kind === "scale") themeRefs++;
4767
+ else if (h.kind !== "token" && h.value) {
4768
+ const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
4769
+ const suggestion = ds.suggest(cat, h.value);
4770
+ findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
4771
+ } else if (h.kind === "token" && h.value) {
4772
+ const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
4773
+ if (v && ds.isDsVar(v)) tokenUsages++;
4774
+ else themeRefs++;
4635
4775
  }
4636
4776
  }
4637
- for (const a of scan.applies) {
4638
- for (const h of a.hits) {
4639
- if (h.kind === "scale") themeRefs++;
4640
- else if (h.kind !== "token" && h.value) {
4641
- const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
4642
- const suggestion = ds.suggest(cat, h.value);
4643
- findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
4644
- } else if (h.kind === "token" && h.value) {
4645
- const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
4646
- if (v && ds.isDsVar(v)) tokenUsages++;
4647
- else themeRefs++;
4648
- }
4777
+ }
4778
+ findings.sort((x, y) => x.line - y.line || x.column - y.column);
4779
+ return { findings, tokenUsages, themeRefs };
4780
+ }
4781
+
4782
+ // ../core/src/audit/markup.ts
4783
+ var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
4784
+ function mask2(text, ranges, keepInside) {
4785
+ const inside = new Uint8Array(text.length);
4786
+ for (const r of ranges) inside.fill(1, r.start, r.end);
4787
+ let out = "";
4788
+ for (let i = 0; i < text.length; i++) {
4789
+ const c = text[i];
4790
+ out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
4791
+ }
4792
+ return out;
4793
+ }
4794
+ var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
4795
+ var isCodeScript = (attrs) => {
4796
+ const type = attrOf(attrs, "type")?.toLowerCase();
4797
+ return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
4798
+ };
4799
+ function markupBlocks(text, lang) {
4800
+ const nonTemplate = [];
4801
+ let frontmatter;
4802
+ let from = 0;
4803
+ if (lang === "astro") {
4804
+ const open = /^\s*---[^\S\n]*\n/.exec(text);
4805
+ if (open) {
4806
+ const close = /\n---[^\S\n]*(\n|$)/g;
4807
+ close.lastIndex = open[0].length - 1;
4808
+ const m = close.exec(text);
4809
+ if (m) {
4810
+ frontmatter = { start: open[0].length, end: m.index + 1 };
4811
+ from = m.index + m[0].length;
4812
+ nonTemplate.push({ start: 0, end: from });
4813
+ }
4814
+ }
4815
+ }
4816
+ const scripts = [];
4817
+ const styles = [];
4818
+ const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
4819
+ re.lastIndex = from;
4820
+ for (let m; m = re.exec(text); ) {
4821
+ const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
4822
+ const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
4823
+ (m[1].toLowerCase() === "script" ? scripts : styles).push(block);
4824
+ nonTemplate.push({ start: m.index, end: m.index + m[0].length });
4825
+ }
4826
+ for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
4827
+ return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
4828
+ }
4829
+ function closeBrace(s, i) {
4830
+ let depth = 0;
4831
+ for (let k = i; k < s.length; k++) {
4832
+ const c = s[k];
4833
+ if (c === "{") depth++;
4834
+ else if (c === "}") {
4835
+ if (--depth === 0) return k + 1;
4836
+ } else if (c === '"' || c === "'") {
4837
+ const e = s.indexOf(c, k + 1);
4838
+ if (e < 0) return s.length;
4839
+ k = e;
4840
+ } else if (c === "`") {
4841
+ for (k++; k < s.length && s[k] !== "`"; k++) {
4842
+ if (s[k] === "\\") k++;
4843
+ else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
4844
+ }
4845
+ }
4846
+ }
4847
+ return s.length;
4848
+ }
4849
+ function readTag(s, i, lang) {
4850
+ const attrs = [];
4851
+ let k = i;
4852
+ while (k < s.length) {
4853
+ while (k < s.length && /\s/.test(s[k])) k++;
4854
+ const c = s[k];
4855
+ if (c === void 0) break;
4856
+ if (c === ">") return { attrs, end: k + 1, selfClosing: false };
4857
+ if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
4858
+ if (c === "{") {
4859
+ const e = closeBrace(s, k);
4860
+ const inner = s.slice(k + 1, e - 1).trim();
4861
+ attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
4862
+ k = e;
4863
+ continue;
4864
+ }
4865
+ const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
4866
+ if (!nm) {
4867
+ k++;
4868
+ continue;
4869
+ }
4870
+ const name = nm[0];
4871
+ k += name.length;
4872
+ let j = k;
4873
+ while (j < s.length && /\s/.test(s[j])) j++;
4874
+ if (s[j] !== "=") {
4875
+ attrs.push({ name, kind: "bare", value: "", valueStart: k });
4876
+ continue;
4877
+ }
4878
+ j++;
4879
+ while (j < s.length && /\s/.test(s[j])) j++;
4880
+ const q = s[j];
4881
+ const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
4882
+ if (q === '"' || q === "'") {
4883
+ const e = s.indexOf(q, j + 1);
4884
+ const end = e < 0 ? s.length : e;
4885
+ attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
4886
+ k = end + 1;
4887
+ } else if (q === "{") {
4888
+ const e = closeBrace(s, j);
4889
+ attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
4890
+ k = e;
4891
+ } else {
4892
+ const m = /^[^\s>]+/.exec(s.slice(j));
4893
+ const v = m?.[0].replace(/\/$/, "") ?? "";
4894
+ attrs.push({ name, kind: "static", value: v, valueStart: j });
4895
+ k = j + v.length;
4896
+ }
4897
+ }
4898
+ return { attrs, end: s.length, selfClosing: false };
4899
+ }
4900
+ var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
4901
+ var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
4902
+ var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
4903
+ function scanMarkup(path, text, lang, ds) {
4904
+ const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
4905
+ const pos = lineCol(text);
4906
+ const blocks = markupBlocks(text, lang);
4907
+ const imports = /* @__PURE__ */ new Map();
4908
+ const merge = (c) => {
4909
+ out.findings.push(...c.findings);
4910
+ out.tokenUsages += c.tokenUsages;
4911
+ out.themeRefs += c.themeRefs;
4912
+ for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
4913
+ out.components.push(...c.components);
4914
+ out.definitions.push(...c.definitions);
4915
+ };
4916
+ const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
4917
+ for (const r of codeRanges) {
4918
+ const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
4919
+ const code2 = mask2(text, [r], true);
4920
+ const file = `${path}.${jsx ? "tsx" : "ts"}`;
4921
+ merge(scanCode(file, code2, ds));
4922
+ for (const [k, v] of importsOf(file, code2)) imports.set(k, v);
4923
+ }
4924
+ for (const b of blocks.styles) {
4925
+ const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
4926
+ const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
4927
+ out.findings.push(...r.findings);
4928
+ out.tokenUsages += r.tokenUsages;
4929
+ out.themeRefs += r.themeRefs;
4930
+ }
4931
+ const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
4932
+ const { line, column } = pos(offset);
4933
+ const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
4934
+ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
4935
+ const c = scanCode(`${path}.tsx`, src, ds);
4936
+ merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
4937
+ };
4938
+ const styleAttr = (a) => {
4939
+ if (a.kind === "static") {
4940
+ const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
4941
+ const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
4942
+ out.findings.push(...r.findings);
4943
+ out.tokenUsages += r.tokenUsages;
4944
+ out.themeRefs += r.themeRefs;
4945
+ return;
4946
+ }
4947
+ const str2 = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
4948
+ if (str2) {
4949
+ const body = str2[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
4950
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str2[1].length + 1, source: "style" });
4951
+ out.findings.push(...r.findings);
4952
+ out.tokenUsages += r.tokenUsages;
4953
+ out.themeRefs += r.themeRefs;
4954
+ return;
4955
+ }
4956
+ scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
4957
+ };
4958
+ const tpl = mask2(text, blocks.nonTemplate, false);
4959
+ const pending = [];
4960
+ let noscript = 0;
4961
+ const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
4962
+ for (let m; m = TAG.exec(tpl); ) {
4963
+ const raw = m[2];
4964
+ if (m[1]) {
4965
+ if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
4966
+ continue;
4967
+ }
4968
+ const tag = readTag(tpl, m.index + m[0].length, lang);
4969
+ TAG.lastIndex = tag.end;
4970
+ const lower = raw.toLowerCase();
4971
+ if (lower === "noscript" && !tag.selfClosing) {
4972
+ noscript++;
4973
+ continue;
4974
+ }
4975
+ const attrs = tag.attrs;
4976
+ const get = (n) => attrs.find((a) => attrName(a.name) === n);
4977
+ for (const a of attrs) {
4978
+ const n = attrName(a.name);
4979
+ if (n === "style") styleAttr(a);
4980
+ else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
4981
+ const prop = a.name.slice(6).split("|")[0];
4982
+ const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
4983
+ out.findings.push(...r2.findings);
4984
+ out.tokenUsages += r2.tokenUsages;
4985
+ out.themeRefs += r2.themeRefs;
4986
+ } else if (/^(class|className|class:list)$/.test(n)) {
4987
+ if (a.kind === "static") {
4988
+ for (const l of lookalikesInClassList(a.value, ds.components)) out.components.push({ ...pos(a.valueStart), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
4989
+ const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
4990
+ out.findings.push(...r2.findings);
4991
+ out.themeRefs += r2.themeRefs;
4992
+ } else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
4993
+ } else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
4994
+ out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
4995
+ } else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
4996
+ const suggestion = ds.suggest("color", a.value.trim(), n);
4997
+ out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
4649
4998
  }
4650
4999
  }
4651
- findings.sort((x, y) => x.line - y.line || x.column - y.column);
4652
- return { path, lang, tokenUsages, themeRefs, hardcoded: findings.length, dsComponents: 0, otherComponents: 0, findings, components: [], definitions: [], dsByName: {} };
5000
+ let name = raw;
5001
+ if (raw.includes(":")) continue;
5002
+ if (raw.includes("-")) {
5003
+ if (!imports.has(pascal(raw))) continue;
5004
+ name = pascal(raw);
5005
+ }
5006
+ const isNative = /^[a-z]/.test(name) && !name.includes(".");
5007
+ if (isNative && noscript > 0) continue;
5008
+ const type = get("type");
5009
+ const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
5010
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
5011
+ if (r.component) out.components.push(r.component);
5012
+ if (r.localCandidate) pending.push(r.localCandidate);
5013
+ }
5014
+ const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
5015
+ if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
5016
+ const defined = new Set(out.definitions.map((d) => d.name));
5017
+ out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
5018
+ out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
5019
+ out.components.sort((a, b) => a.line - b.line || a.column - b.column);
5020
+ return out;
5021
+ }
5022
+
5023
+ // ../core/src/audit/types.ts
5024
+ var AUDIT_SCHEMA_VERSION = 1;
5025
+ var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width", "shadow", "duration", "easing", "z-index", "font-family", "font-size", "font-weight", "line-height"];
5026
+
5027
+ // ../core/src/audit/report.ts
5028
+ var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
5029
+ var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
5030
+ var IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
5031
+ function isAuditable(path) {
5032
+ if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
5033
+ if (IGNORE_FILE.test(path)) return false;
5034
+ return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
5035
+ }
5036
+ var langOf = (path) => {
5037
+ const ext = path.slice(path.lastIndexOf(".") + 1);
5038
+ if (ext === "css") return "css";
5039
+ if (ext === "scss" || ext === "sass") return "scss";
5040
+ if (ext === "less") return "less";
5041
+ if (ext === "tsx") return "tsx";
5042
+ if (ext === "jsx") return "jsx";
5043
+ if (/^[cm]?ts$/.test(ext)) return "ts";
5044
+ if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
5045
+ return "js";
5046
+ };
5047
+ function auditFile(path, text, ds) {
5048
+ const lang = langOf(path);
5049
+ if (lang === "css" || lang === "scss" || lang === "less") {
5050
+ const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
5051
+ return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
4653
5052
  }
4654
- const c = scanCode(path, text, ds);
5053
+ const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
4655
5054
  const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
4656
- return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: c.findings.length, dsComponents: dsCount, otherComponents: c.components.length, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds };
5055
+ return withIgnores(text, { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: 0, dsComponents: dsCount, otherComponents: 0, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds });
5056
+ }
5057
+ function withIgnores(text, f) {
5058
+ const r = applyIgnoreComments(text, f.findings, f.components);
5059
+ return {
5060
+ ...f,
5061
+ findings: r.findings,
5062
+ components: r.components,
5063
+ hardcoded: r.findings.length,
5064
+ otherComponents: r.components.length,
5065
+ ...r.ignored.length ? { ignored: r.ignored } : {},
5066
+ ...r.errors.length ? { ignoreErrors: r.errors } : {}
5067
+ };
4657
5068
  }
4658
5069
  var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
4659
5070
  function buildAuditReport(input, ds) {
4660
- const reports = input.files.map((f) => auditFile(f.path, f.text, ds));
5071
+ const allow = input.allowlist ?? [];
5072
+ const usedAllow = /* @__PURE__ */ new Set();
5073
+ const reports = input.files.map((f) => {
5074
+ const r = auditFile(f.path, f.text, ds);
5075
+ const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
5076
+ if (!idx.length) return r;
5077
+ const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
5078
+ a.used.forEach((k) => usedAllow.add(idx[k]));
5079
+ if (!a.ignored.length) return r;
5080
+ const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
5081
+ return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
5082
+ });
5083
+ const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
5084
+ const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
4661
5085
  const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
4662
5086
  const byCategory = {};
4663
5087
  const fixes = /* @__PURE__ */ new Map();
@@ -4717,6 +5141,15 @@ function buildAuditReport(input, ds) {
4717
5141
  topFiles,
4718
5142
  topFixes,
4719
5143
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
5144
+ ...ignoredItems.length || staleAllowlist.length ? {
5145
+ ignored: {
5146
+ total: ignoredItems.length,
5147
+ comment: ignoredItems.filter((x) => x.via === "comment").length,
5148
+ allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
5149
+ items: ignoredItems,
5150
+ staleAllowlist
5151
+ }
5152
+ } : {},
4720
5153
  history: []
4721
5154
  };
4722
5155
  }
@@ -5030,6 +5463,9 @@ function collectAuditFiles(target, exclude = []) {
5030
5463
  }
5031
5464
  return { root: abs, files, skipped };
5032
5465
  }
5466
+ function loadAllowlist(path) {
5467
+ return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
5468
+ }
5033
5469
 
5034
5470
  // src/config.ts
5035
5471
  var SCHEMA_URL = "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json";
@@ -5287,7 +5723,8 @@ function client(repo, o, rules) {
5287
5723
  ...o.apiUrl ? { apiUrl: o.apiUrl } : {},
5288
5724
  ...o.rawUrl ? { rawUrl: o.rawUrl } : {},
5289
5725
  protectedBranches: rules.protectedBranches,
5290
- branchPrefix: rules.branchPrefix
5726
+ branchPrefix: rules.branchPrefix,
5727
+ tokenHint: "set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN) in the environment of the Bridge MCP server, read access to this repository is enough"
5291
5728
  });
5292
5729
  }
5293
5730
  var BridgeContext = class _BridgeContext {
@@ -6656,7 +7093,8 @@ async function auditOp(ctx, a) {
6656
7093
  if (!existsSync2(target)) throw new Error(`No such file or folder: ${target}`);
6657
7094
  const ds = await designSystemIndex(ctx);
6658
7095
  const { files, skipped } = collectAuditFiles(target, config.audit?.exclude ?? []);
6659
- let report = buildAuditReport({ files, skipped, target: a.path, ...a.label ? { label: a.label } : {} }, ds);
7096
+ const allowlist = a.allowlist ? loadAllowlist(isAbsolute(a.allowlist) ? a.allowlist : resolve3(ctx.root, a.allowlist)) : void 0;
7097
+ let report = buildAuditReport({ files, skipped, target: a.path, ...a.label ? { label: a.label } : {}, ...allowlist ? { allowlist } : {} }, ds);
6660
7098
  if (a.baseline) report = compareWithBaseline(report, JSON.parse(readFileSync2(isAbsolute(a.baseline) ? a.baseline : resolve3(ctx.root, a.baseline), "utf8")));
6661
7099
  const limit = a.limit ?? 10;
6662
7100
  const single = files.length === 1;
@@ -6673,6 +7111,8 @@ async function auditOp(ctx, a) {
6673
7111
  native: report.components.native,
6674
7112
  recreated: report.components.recreated
6675
7113
  },
7114
+ ...report.ignored ? { ignored: { total: report.ignored.total, comment: report.ignored.comment, allowlist: report.ignored.allowlist, items: report.ignored.items.slice(0, limit * 5), staleAllowlist: report.ignored.staleAllowlist } } : {},
7115
+ ...report.files.some((f) => f.ignoreErrors?.length) ? { ignoreErrors: report.files.flatMap((f) => (f.ignoreErrors ?? []).map((e) => ({ file: f.path, ...e }))) } : {},
6676
7116
  topFiles: report.topFiles.slice(0, limit),
6677
7117
  topFixes: report.topFixes.slice(0, limit).map((f) => ({ value: f.value, category: f.category, count: f.count, ...f.suggestion ? { use: `var(${f.suggestion.cssVar})`, token: f.suggestion.token, tokenValue: f.suggestion.value, ...f.suggestion.mode ? { mode: f.suggestion.mode } : {}, confidence: f.suggestion.level } : { use: null } })),
6678
7118
  ...withFindings ? {
@@ -6807,19 +7247,19 @@ async function buildPlan(ctx, a, tool) {
6807
7247
  file = v[v.length - 1].file;
6808
7248
  }
6809
7249
  let value = r.value;
6810
- let item;
7250
+ let item2;
6811
7251
  if (value === void 0) {
6812
7252
  if (!r.entry) throw new Error("restore needs value, or entry (the history entry whose value to put back).");
6813
7253
  const e = findEntry(report.entries, r.entry);
6814
7254
  const items = tokenHistory([e], path, config).filter((i) => !file || i.file === file);
6815
7255
  if (!items.length) throw new Error(`${e.id} did not change ${path}${file ? ` in ${file}` : ""}. bridge_get_token_history lists the changes of a token.`);
6816
7256
  if (items.length > 1 && !file) throw new Error(`${e.id} changed ${path} in ${items.map((i) => i.file).join(", ")}. Pass mode (for example {"theme":"dark"}) or file.`);
6817
- item = items[0];
6818
- file ??= item.file;
6819
- value = r.side === "before" ? item.before : item.after;
6820
- if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${item.kind}).`);
7257
+ item2 = items[0];
7258
+ file ??= item2.file;
7259
+ value = r.side === "before" ? item2.before : item2.after;
7260
+ if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${item2.kind}).`);
6821
7261
  }
6822
- return planRestore({ current, path, ...file ? { file } : {}, value, ...item ? { item, label: item.pr ? `#${item.pr.number}` : item.entryId } : {}, pairs, tool });
7262
+ return planRestore({ current, path, ...file ? { file } : {}, value, ...item2 ? { item: item2, label: item2.pr ? `#${item2.pr.number}` : item2.entryId } : {}, pairs, tool });
6823
7263
  }
6824
7264
  const entry = findEntry(report.entries, a.target);
6825
7265
  const later = report.entries.filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
@@ -7176,11 +7616,11 @@ async function pendingDrafts(ctx, a) {
7176
7616
  const images = [];
7177
7617
  if (a.images !== false) {
7178
7618
  for (const s of sessions) {
7179
- for (const item of s.items) {
7180
- for (const img of item.images ?? []) {
7619
+ for (const item2 of s.items) {
7620
+ for (const img of item2.images ?? []) {
7181
7621
  if (images.length >= IMAGES_PER_CALL || !img.path) continue;
7182
7622
  const f = await gh.getFileBase64(img.path, REQUESTS_BRANCH).catch(() => void 0);
7183
- if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item.id, label: img.label, ...img.line ? { line: img.line } : {}, base64: f.base64 });
7623
+ if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item2.id, label: img.label, ...img.line ? { line: img.line } : {}, base64: f.base64 });
7184
7624
  }
7185
7625
  }
7186
7626
  }
@@ -7212,7 +7652,7 @@ async function writeDrafts(ctx, a) {
7212
7652
  }
7213
7653
 
7214
7654
  // src/server.ts
7215
- var VERSION = true ? "0.3.1" : "dev";
7655
+ var VERSION = true ? "0.3.2" : "dev";
7216
7656
  var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
7217
7657
  var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
7218
7658
  var run = (fn) => async (a) => {
@@ -7570,13 +8010,14 @@ function createServer(ctx) {
7570
8010
  "bridge_audit",
7571
8011
  {
7572
8012
  title: "Audit design system adoption",
7573
- description: "Measure how a product codebase uses this design system (local only). Scans CSS, SCSS, style/sx/styled objects, CSS-in-JS and Tailwind classes for hardcoded colors, spacing, radii, shadows, durations, z-index and fonts, each with the closest token (colors by deltaE across every mode) and a confidence; and JSX for DS components versus foreign UI libraries (MUI, react-admin, Radix, shadcn/ui...), native elements and re-created DS components. Returns token and component adoption, counts by category, top files and top fixes; for a single file (or findings: true) the findings with line numbers.",
8013
+ description: "Measure how a product codebase uses this design system (local only). Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte files (template markup, frontmatter and <script> blocks, <style> blocks). Finds hardcoded colors, spacing, radii, shadows, durations, z-index and fonts in style sheets, style attributes, style/sx/styled objects, CSS-in-JS and Tailwind classes, each with the closest token (colors by deltaE across every mode) and a confidence; and in JSX and template markup, DS components versus foreign UI libraries (MUI, react-admin, Radix, shadcn/ui...), native elements a DS component should render, and re-created DS components. Hidden inputs and elements inside <noscript> do not count. A `bridge-audit-ignore-next-line: reason` comment on the line above leaves one exception out; an allowlist file lists known file:line exceptions; both need a reason and stay listed under `ignored`. Returns token and component adoption, counts by category, top files and top fixes; for a single file (or findings: true) the findings with line numbers.",
7574
8014
  inputSchema: {
7575
8015
  path: z.string().min(1).describe("Product folder or file, absolute or relative to the design system repo root."),
7576
8016
  baseline: z.string().optional().describe("Earlier report from `bridge audit --json` to compare with (progress per metric and file)."),
7577
8017
  label: z.string().optional(),
7578
8018
  limit: z.number().int().min(1).max(50).optional().describe("Top files and fixes to return. Default 10."),
7579
- findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file.")
8019
+ findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file."),
8020
+ allowlist: z.string().optional().describe('Allowlist file of known exceptions: { "entries": [{ "file", "line", "what"?, "reason" }] }, paths relative to the audited folder.')
7580
8021
  },
7581
8022
  annotations: RO
7582
8023
  },
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  VERSION,
4
4
  optionsFromArgv,
5
5
  serveStdio
6
- } from "./chunk-I3TL76GK.js";
6
+ } from "./chunk-MTQCDV65.js";
7
7
  import "./chunk-X455XY4E.js";
8
8
 
9
9
  // src/index.ts
package/dist/lib.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  parseDocsReply,
24
24
  requests_exports,
25
25
  serveStdio
26
- } from "./chunk-I3TL76GK.js";
26
+ } from "./chunk-MTQCDV65.js";
27
27
  import {
28
28
  CONFIG_FILE,
29
29
  loadConfig,
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;
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.2",
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.5.0",
58
+ "@bridge-ds/visual": "^0.2.3",
59
59
  "@modelcontextprotocol/sdk": "1.31.0",
60
60
  "typescript": "5.9.3",
61
61
  "zod": "3.25.76"