@bridge-ds/mcp 0.3.2 → 0.4.0

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.
@@ -391,6 +391,7 @@ var TeamDirectory = class {
391
391
  var OWNER_GROUPS = ["design", "tech"];
392
392
  var RIGHTS = ["approveDesign", "approveTech", "ship"];
393
393
  var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
394
+ var norm2 = (x) => x.trim().toLowerCase().replace(/^@/, "");
394
395
  var uniq = (xs) => [...new Set(xs)];
395
396
  function normalizeRights(input) {
396
397
  const pick = (r) => {
@@ -426,6 +427,16 @@ function shipPolicy(owners, rights, teams) {
426
427
  const logins = expandEntries(entries, teams);
427
428
  return { logins, groups: [...rights.ship], entries, ...teams ? { teams } : {} };
428
429
  }
430
+ function unreadableShipTeams(policy) {
431
+ if (!policy?.teams) return [];
432
+ const keys = new Set((policy.entries ?? []).map((e) => e.trim().toLowerCase().replace(/^@?/, "@")));
433
+ return unreadableTeams(policy.teams).filter((t) => keys.has(t.key));
434
+ }
435
+ function canShip(login, policy) {
436
+ if (!policy) return true;
437
+ if (!login || login.includes("/")) return false;
438
+ return policy.logins.map(norm2).includes(norm2(login));
439
+ }
429
440
  function groupsPhrase(groups) {
430
441
  const g = OWNER_GROUPS.filter((x) => groups.includes(x));
431
442
  if (g.length === 1) return `the ${g[0]} owner`;
@@ -433,7 +444,7 @@ function groupsPhrase(groups) {
433
444
  }
434
445
  function whoCanShipSentence(policy) {
435
446
  const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
436
- return `Only ${groupsPhrase(policy.groups)} can ship this${names ? ` (${names})` : ""}.`;
447
+ return `Only ${groupsPhrase(policy.groups)} can merge this${names ? ` (${names})` : ""}.`;
437
448
  }
438
449
  function visualImpactProblems(raw) {
439
450
  if (raw === void 0) return [];
@@ -446,6 +457,101 @@ function visualImpactProblems(raw) {
446
457
  return out;
447
458
  }
448
459
 
460
+ // ../core/src/products.ts
461
+ var DEFAULT_PRODUCTS = [
462
+ { key: "admin", name: "Admin demo", path: "apps/admin", framework: "react" },
463
+ { key: "library", name: "Library", default: true }
464
+ ];
465
+ var PRODUCT_LABEL_PREFIX = "product: ";
466
+ var productLabel = (key) => `${PRODUCT_LABEL_PREFIX}${key}`;
467
+ function productsOf(config) {
468
+ const list2 = config?.products;
469
+ return Array.isArray(list2) && list2.length ? list2 : DEFAULT_PRODUCTS;
470
+ }
471
+ function defaultProduct(products) {
472
+ const list2 = products.length ? products : DEFAULT_PRODUCTS;
473
+ return list2.find((p) => p.default) ?? list2[list2.length - 1];
474
+ }
475
+ function findProduct(products, keyOrName) {
476
+ const k = (keyOrName ?? "").trim().toLowerCase();
477
+ if (!k) return void 0;
478
+ return products.find((p) => p.key.toLowerCase() === k) ?? products.find((p) => p.name.toLowerCase() === k);
479
+ }
480
+ var cleanPath = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
481
+ function productOfBranch(products, branch) {
482
+ const fallback = defaultProduct(products);
483
+ for (const l of branch.labels ?? []) {
484
+ const name = typeof l === "string" ? l : l?.name;
485
+ if (typeof name !== "string" || !name.toLowerCase().startsWith(PRODUCT_LABEL_PREFIX)) continue;
486
+ const hit = findProduct(products, name.slice(PRODUCT_LABEL_PREFIX.length));
487
+ if (hit) return hit;
488
+ }
489
+ const files = branch.files ?? [];
490
+ for (const p of products) {
491
+ if (p === fallback || !p.path) continue;
492
+ const prefix = `${cleanPath(p.path)}/`;
493
+ if (prefix !== "/" && files.some((f) => f.startsWith(prefix))) return p;
494
+ }
495
+ return fallback;
496
+ }
497
+ function productForPlace(products, place) {
498
+ const repo = place.repo?.trim().toLowerCase();
499
+ if (repo && repo !== place.designSystemRepo?.trim().toLowerCase()) {
500
+ const hit = products.find((p) => p.repo?.toLowerCase() === repo);
501
+ if (hit) return { product: hit, how: "repo" };
502
+ }
503
+ const rel = cleanPath(place.relPath ?? "");
504
+ if (rel && rel !== ".") {
505
+ let best;
506
+ for (const p of products) {
507
+ if (!p.path) continue;
508
+ const dir = cleanPath(p.path);
509
+ if (dir && (rel === dir || rel.startsWith(`${dir}/`)) && (!best || dir.length > cleanPath(best.path).length)) best = p;
510
+ }
511
+ if (best) return { product: best, how: "path" };
512
+ }
513
+ return { product: defaultProduct(products), how: "default" };
514
+ }
515
+ var KEY = /^[a-z0-9][a-z0-9-]*$/;
516
+ var REPO = /^[\w.-]+\/[\w.-]+$/;
517
+ var FIELDS = ["key", "name", "repo", "path", "preview", "framework", "default"];
518
+ function productProblems(raw) {
519
+ if (raw === void 0) return [];
520
+ if (!Array.isArray(raw)) return ['"products" must be a list of { "key", "name" } objects'];
521
+ if (!raw.length) return ['"products" must list at least one product, or be left out to use the defaults'];
522
+ const problems = [];
523
+ const seen = /* @__PURE__ */ new Set();
524
+ let defaults = 0;
525
+ raw.forEach((p, i) => {
526
+ const at = `"products[${i}]`;
527
+ if (!p || typeof p !== "object" || Array.isArray(p)) {
528
+ problems.push(`${at}" must be an object with a "key" and a "name"`);
529
+ return;
530
+ }
531
+ const o = p;
532
+ for (const k of Object.keys(o)) if (!FIELDS.includes(k)) problems.push(`${at}.${k}" is not a known field (${FIELDS.join(", ")})`);
533
+ if (typeof o.key !== "string" || !KEY.test(o.key)) problems.push(`${at}.key" must be lowercase letters, digits and dashes, such as "admin"`);
534
+ else if (seen.has(o.key)) problems.push(`${at}.key" ("${o.key}") is used twice: give each product its own key`);
535
+ else seen.add(o.key);
536
+ if (typeof o.name !== "string" || !o.name.trim()) problems.push(`${at}.name" must be the name people read, such as "Admin demo"`);
537
+ if (o.repo !== void 0 && (typeof o.repo !== "string" || !REPO.test(o.repo))) problems.push(`${at}.repo" must be "owner/name"`);
538
+ if (o.path !== void 0) {
539
+ const bad = typeof o.path !== "string" || !o.path.trim() || o.path.startsWith("/") || o.path.split("/").includes("..");
540
+ if (bad) problems.push(`${at}.path" must be a folder relative to the repository root, without a leading "/" or "..", such as "apps/admin"`);
541
+ }
542
+ if (o.preview !== void 0 && (typeof o.preview !== "string" || !/^https?:\/\//.test(o.preview))) problems.push(`${at}.preview" must be an http(s) URL template, such as "https://app-git-{branch}.vercel.app"`);
543
+ if (o.framework !== void 0 && (typeof o.framework !== "string" || !o.framework.trim())) problems.push(`${at}.framework" must be a name such as "react", "vue" or "other"`);
544
+ if (o.default !== void 0 && typeof o.default !== "boolean") problems.push(`${at}.default" must be true or false`);
545
+ if (o.default === true) defaults++;
546
+ });
547
+ if (defaults > 1) problems.push('"products" marks more than one product as default: keep "default": true on the library only');
548
+ return problems;
549
+ }
550
+ function sameProducts(a, b) {
551
+ const norm3 = (l) => JSON.stringify(l.map((p) => FIELDS.map((f) => p[f] ?? null)));
552
+ return norm3(a) === norm3(b);
553
+ }
554
+
449
555
  // ../core/src/config.ts
450
556
  var CONFIG_FILE = "bridge.config.json";
451
557
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
@@ -586,6 +692,7 @@ function defineConfig(input) {
586
692
  } : {},
587
693
  ...g.rights ? { rights: normalizeRights(g.rights) } : {}
588
694
  },
695
+ products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
589
696
  ...input.audit ? { audit: { ...input.audit } } : {},
590
697
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
591
698
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -708,6 +815,7 @@ function loadConfig(source) {
708
815
  }
709
816
  }
710
817
  problems.push(...visualImpactProblems(d.visualImpact));
818
+ problems.push(...productProblems(d.products));
711
819
  const audit = d.audit;
712
820
  if (audit !== void 0) {
713
821
  if (!isObj(audit)) problems.push('"audit" must be an object');
@@ -754,7 +862,8 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
754
862
  governance: config.governance,
755
863
  ...config.audit ? { audit: config.audit } : {},
756
864
  ...config.previews ? { previews: config.previews } : {},
757
- ...config.visualImpact ? { visualImpact: config.visualImpact } : {}
865
+ ...config.visualImpact ? { visualImpact: config.visualImpact } : {},
866
+ ...config.products && !sameProducts(config.products, DEFAULT_PRODUCTS) ? { products: config.products } : {}
758
867
  };
759
868
  }
760
869
 
@@ -1317,6 +1426,49 @@ function fieldChanges(a, b) {
1317
1426
  if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
1318
1427
  return out;
1319
1428
  }
1429
+ function sameToken(a, b) {
1430
+ if (!a || !b) return a === b;
1431
+ return fieldChanges(a, b).length === 0;
1432
+ }
1433
+ function sideChange(from, to) {
1434
+ if (!from && !to) return void 0;
1435
+ if (!from) return { kind: "added", fields: [] };
1436
+ if (!to) return { kind: "removed", fields: [] };
1437
+ const fields = fieldChanges(from, to);
1438
+ if (!fields.length) return void 0;
1439
+ return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
1440
+ }
1441
+ function threeWayDiff(base, figma, head) {
1442
+ const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
1443
+ const result = { local: [], remote: [], conflicts: [], unchanged: 0 };
1444
+ for (const id of [...ids].sort()) {
1445
+ const b = base?.get(id);
1446
+ const f = figma.get(id);
1447
+ const h = head.get(id);
1448
+ const local = sideChange(b, f);
1449
+ const remote = sideChange(b, h);
1450
+ if (!local && !remote || f && h && sameToken(f, h) || !f && !h) {
1451
+ result.unchanged++;
1452
+ continue;
1453
+ }
1454
+ const entry = { id, path: (f ?? h ?? b).path, base: b, figma: f, head: h, local, remote, conflict: false };
1455
+ if (local && !remote) {
1456
+ entry.path = (f ?? b).path;
1457
+ result.local.push(entry);
1458
+ } else if (remote && !local) {
1459
+ entry.path = (h ?? b).path;
1460
+ result.remote.push(entry);
1461
+ } else {
1462
+ entry.conflict = true;
1463
+ result.conflicts.push(entry);
1464
+ }
1465
+ }
1466
+ const byPath = (a, b) => a.path.localeCompare(b.path);
1467
+ result.local.sort(byPath);
1468
+ result.remote.sort(byPath);
1469
+ result.conflicts.sort(byPath);
1470
+ return result;
1471
+ }
1320
1472
 
1321
1473
  // ../core/src/risk.ts
1322
1474
  var RISK_LABELS = {
@@ -1551,6 +1703,164 @@ function composeProposalBody(args) {
1551
1703
  ].join("\n");
1552
1704
  }
1553
1705
 
1706
+ // ../core/src/component-spec.ts
1707
+ var COMPONENT_SPECS_VERSION = 1;
1708
+ var DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
1709
+ function coverageSummary(file) {
1710
+ const list2 = file.coverage ?? file.components.map((c) => ({ name: c.name, status: "generated" }));
1711
+ const generated = list2.filter((c) => c.status === "generated").length;
1712
+ const drafted = list2.filter((c) => c.status === "drafted").length;
1713
+ const layout = list2.filter((c) => c.status === "layout").length;
1714
+ return { generated, drafted, inFigma: generated + drafted, visual: list2.length - layout, layout, total: list2.length, missing: list2.filter((c) => c.status === "unsupported") };
1715
+ }
1716
+ var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
1717
+ function parseComponentSpecs(text) {
1718
+ let data;
1719
+ try {
1720
+ data = JSON.parse(text);
1721
+ } catch {
1722
+ throw new Error("component-specs.json is not valid JSON.");
1723
+ }
1724
+ const f = data;
1725
+ if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
1726
+ if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
1727
+ return f;
1728
+ }
1729
+ function figmaCell(entry) {
1730
+ if (!entry) return void 0;
1731
+ if (entry.status === "generated") return { label: "Yes" };
1732
+ if (entry.status === "drafted") return { label: "Drafted", title: "Drafted with Claude where the code cannot be read on its own: in Figma, check it against the code." };
1733
+ if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
1734
+ return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
1735
+ }
1736
+
1737
+ // ../core/src/fidelity-references.ts
1738
+ var FIGMA_REFERENCES_BRANCH = "bridge-visual";
1739
+ var FIGMA_REFERENCES_DIR = "figma-references";
1740
+ var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
1741
+ var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
1742
+ var FIGMA_FILL_WIDTH_TOKEN = "size.80";
1743
+ var STATE_PSEUDO = {
1744
+ default: "rest",
1745
+ rest: "rest",
1746
+ enabled: "rest",
1747
+ hover: "hover",
1748
+ hovered: "hover",
1749
+ pressed: "active",
1750
+ active: "active",
1751
+ focus: "focus-visible",
1752
+ focused: "focus-visible",
1753
+ "focus visible": "focus-visible",
1754
+ disabled: "disabled"
1755
+ };
1756
+ var iconName = (component) => component.replace(/^Icon\//, "");
1757
+ var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
1758
+ function planReference(spec, variant, file) {
1759
+ if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
1760
+ const props = { ...spec.fixed ?? {} };
1761
+ let pseudo;
1762
+ for (const axis of spec.axes) {
1763
+ const value = variant.props[axis.name];
1764
+ if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
1765
+ if (axis.prop) {
1766
+ props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
1767
+ continue;
1768
+ }
1769
+ if (axis.name === "Checked") {
1770
+ props.checked = value === "true";
1771
+ if (value === "mixed") props.indeterminate = true;
1772
+ continue;
1773
+ }
1774
+ if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
1775
+ const s = STATE_PSEUDO[value.toLowerCase()];
1776
+ if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
1777
+ if (s === "disabled") props.disabled = true;
1778
+ else if (s !== "rest") pseudo = s;
1779
+ }
1780
+ const root = spec.parts.find((p) => !p.parent);
1781
+ if (!root) return { ok: false, reason: "the spec has no root layer" };
1782
+ const children = (id) => spec.parts.filter((p) => p.parent === id);
1783
+ const textOf = (part) => {
1784
+ const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
1785
+ if (part.swap && swapTo) {
1786
+ const slot = file?.slots?.find((s) => s.name === swapTo);
1787
+ if (!slot) return { reason: `${swapTo} is not in the specs` };
1788
+ if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
1789
+ return slot.text.default;
1790
+ }
1791
+ const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
1792
+ return typeof prop?.default === "string" ? prop.default : part.name;
1793
+ };
1794
+ let text;
1795
+ const icons = {};
1796
+ for (const prop of spec.properties) {
1797
+ if (prop.type === "TEXT") {
1798
+ const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
1799
+ if (hidden && prop.prop) continue;
1800
+ if (prop.prop === "children") text = String(prop.default);
1801
+ else if (prop.prop) props[prop.prop] = String(prop.default);
1802
+ continue;
1803
+ }
1804
+ const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
1805
+ if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
1806
+ }
1807
+ for (const part of spec.parts) {
1808
+ if (part.kind !== "icon" || !part.swap) continue;
1809
+ const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
1810
+ if (!shown) continue;
1811
+ if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
1812
+ icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
1813
+ }
1814
+ let parts;
1815
+ const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
1816
+ const frames = children(root.id).filter((p) => p.kind === "frame");
1817
+ 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)));
1818
+ if (compound) {
1819
+ parts = [];
1820
+ for (const f of frames) {
1821
+ const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
1822
+ const value = textOf(t);
1823
+ if (typeof value !== "string") return { ok: false, reason: value.reason };
1824
+ parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
1825
+ }
1826
+ }
1827
+ for (const p of loose) {
1828
+ if (p.name === "Value") continue;
1829
+ const part = spec.parts.find((x) => x.text === p.name);
1830
+ if (compound && part && part.parent !== root.id) continue;
1831
+ if (part && /placeholder$/.test(part.id)) {
1832
+ props.placeholder = String(p.default);
1833
+ continue;
1834
+ }
1835
+ return { ok: false, reason: `the ${p.name} text has no matching option in code` };
1836
+ }
1837
+ const example = spec.example;
1838
+ const exampleChildren = example?.children?.length ? example.children : void 0;
1839
+ return {
1840
+ ok: true,
1841
+ render: {
1842
+ component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
1843
+ props: { ...example?.p ?? {}, ...props },
1844
+ ...exampleChildren ? { children: exampleChildren } : {},
1845
+ ...text !== void 0 && !exampleChildren ? { text } : {},
1846
+ ...parts && !exampleChildren ? { parts } : {},
1847
+ ...Object.keys(icons).length ? { icons } : {},
1848
+ ...pseudo ? { pseudo } : {},
1849
+ ...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
1850
+ }
1851
+ };
1852
+ }
1853
+ function referenceImagePath(component, key) {
1854
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1855
+ const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
1856
+ return `${slug(component)}/${file || "default"}.png`;
1857
+ }
1858
+ function planReferences(file) {
1859
+ return file.components.flatMap(
1860
+ (spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
1861
+ );
1862
+ }
1863
+
1554
1864
  // ../core/src/node/git-local.ts
1555
1865
  import { execFile } from "node:child_process";
1556
1866
  import { mkdtemp, rm } from "node:fs/promises";
@@ -1661,6 +1971,7 @@ async function identityEnv(root) {
1661
1971
 
1662
1972
  export {
1663
1973
  __export,
1974
+ previewUrl,
1664
1975
  previewLinks,
1665
1976
  readPreviewComment,
1666
1977
  findPreviewComment,
@@ -1673,8 +1984,18 @@ export {
1673
1984
  DEFAULT_RIGHTS,
1674
1985
  approverEntries,
1675
1986
  shipPolicy,
1987
+ unreadableShipTeams,
1988
+ canShip,
1676
1989
  whoCanShipSentence,
1990
+ productLabel,
1991
+ productsOf,
1992
+ defaultProduct,
1993
+ findProduct,
1994
+ productOfBranch,
1995
+ productForPlace,
1677
1996
  CONFIG_FILE,
1997
+ DEFAULT_OVERRIDE,
1998
+ DEFAULT_TOKEN_PATHS,
1678
1999
  loadConfig,
1679
2000
  tokensPath,
1680
2001
  protectedBranchesOf,
@@ -1686,6 +2007,7 @@ export {
1686
2007
  renderedColor,
1687
2008
  contrastPair,
1688
2009
  parseContrastChecks,
2010
+ threeWayDiff,
1689
2011
  RISK_LABELS,
1690
2012
  classifyRisk,
1691
2013
  aliasOf,
@@ -1707,6 +2029,15 @@ export {
1707
2029
  DEFAULT_PROTECTED_BRANCHES,
1708
2030
  ProtectedBranchError,
1709
2031
  assertSafeBranch,
2032
+ DEFAULT_COMPONENT_SPECS_FILE,
2033
+ coverageSummary,
2034
+ parseComponentSpecs,
2035
+ figmaCell,
2036
+ FIGMA_REFERENCES_BRANCH,
2037
+ FIGMA_REFERENCES_MANIFEST,
2038
+ FIGMA_REFERENCE_STORY,
2039
+ referenceImagePath,
2040
+ planReferences,
1710
2041
  git,
1711
2042
  isGitRepo,
1712
2043
  currentBranch,