@bridge-ds/mcp 0.8.4 → 0.9.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.
@@ -171,7 +171,7 @@ function resolveBranchPreviews(apps, ctx, sources = {}) {
171
171
  }
172
172
  return out;
173
173
  }
174
- var previewsSettled = (previews) => previews.every((p) => !!p.url && (p.state === "ready" || p.state === "failed"));
174
+ var previewsSettled = (previews) => previews.every((p) => p.state === "failed" || !!p.url && p.state === "ready");
175
175
 
176
176
  // ../core/src/teams.ts
177
177
  var LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
@@ -578,6 +578,12 @@ var CONFIG_FILE = "bridge.config.json";
578
578
  var CHANGES_MODES = ["suggest", "direct"];
579
579
  var DEFAULT_CHANGES_MODE = "suggest";
580
580
  var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
581
+ var TEAM_NAME_MAX = 60;
582
+ function teamName(value) {
583
+ if (typeof value !== "string") return void 0;
584
+ const v = value.trim().replace(/\s+/g, " ");
585
+ return v && v.length <= TEAM_NAME_MAX ? v : void 0;
586
+ }
581
587
  var DEFAULT_TOKEN_PATHS = {
582
588
  root: "tokens",
583
589
  primitives: "primitives",
@@ -683,6 +689,7 @@ function defineConfig(input) {
683
689
  const g = input.governance ?? {};
684
690
  const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
685
691
  return {
692
+ ...teamName(input.name) ? { name: teamName(input.name) } : {},
686
693
  repo: parseRepoInput(input.repo, input.branch),
687
694
  tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
688
695
  axes,
@@ -721,6 +728,7 @@ function defineConfig(input) {
721
728
  changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
722
729
  ...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
723
730
  ...input.library?.public === true ? { library: { public: true } } : {},
731
+ ...input.demo?.viewer?.trim() ? { demo: { viewer: input.demo.viewer.trim(), ...input.demo.role?.trim() ? { role: input.demo.role.trim() } : {} } } : {},
724
732
  ...input.audit ? { audit: { ...input.audit } } : {},
725
733
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
726
734
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -845,6 +853,7 @@ function loadConfig(source) {
845
853
  }
846
854
  }
847
855
  problems.push(...visualImpactProblems(d.visualImpact));
856
+ if (d.name !== void 0 && teamName(d.name) === void 0) problems.push(`"name" must be the team's name, 1 to ${TEAM_NAME_MAX} characters`);
848
857
  if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
849
858
  const sb = d.storybook;
850
859
  if (sb !== void 0) {
@@ -860,6 +869,14 @@ function loadConfig(source) {
860
869
  for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
861
870
  }
862
871
  }
872
+ const demo = d.demo;
873
+ if (demo !== void 0) {
874
+ if (!isObj(demo) || !isStr(demo.viewer) || !demo.viewer.trim()) problems.push('"demo" must be { "viewer" }, for example { "viewer": "Dana", "role": "DS lead" }');
875
+ else {
876
+ if (demo.role !== void 0 && !isStr(demo.role)) problems.push('"demo.role" must be a string, for example "DS lead"');
877
+ for (const k of Object.keys(demo)) if (k !== "viewer" && k !== "role") problems.push(`"demo.${k}" is not a known key`);
878
+ }
879
+ }
863
880
  problems.push(...productProblems(d.products));
864
881
  const audit = d.audit;
865
882
  if (audit !== void 0) {
@@ -897,6 +914,7 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
897
914
  return {
898
915
  $schema: schema,
899
916
  version: 1,
917
+ ...config.name ? { name: config.name } : {},
900
918
  ...config.repo ? { repo: config.repo } : {},
901
919
  tokens: { root: config.tokens.root, include: config.tokens.include, ...config.tokens.exclude.length ? { exclude: config.tokens.exclude } : {}, tiers: config.tokens.tiers, layers: config.tokens.layers.map((l) => l.optional ? l : { files: l.files }), ...config.tokens.usage ? { usage: config.tokens.usage } : {} },
902
920
  modes: Object.fromEntries(Object.entries(config.axes).map(([k, a]) => [k, a.parity ? a : { values: a.values, default: a.default }])),
@@ -912,6 +930,7 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
912
930
  changes: changesMode(config),
913
931
  ...config.storybook ? { storybook: config.storybook } : {},
914
932
  ...config.library ? { library: config.library } : {},
933
+ ...config.demo ? { demo: config.demo } : {},
915
934
  ...config.products && !sameProducts(config.products, DEFAULT_PRODUCTS) ? { products: config.products } : {}
916
935
  };
917
936
  }
@@ -1052,6 +1071,164 @@ function parseContrastChecks(text) {
1052
1071
  return out;
1053
1072
  }
1054
1073
 
1074
+ // ../core/src/scopes.ts
1075
+ var has = (segments, ...names) => names.some((n) => segments.includes(n));
1076
+ function defaultScopes(path, type, tier) {
1077
+ const s = path.split(".");
1078
+ switch (type) {
1079
+ case "color":
1080
+ if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
1081
+ if (has(s, "text")) return ["TEXT_FILL"];
1082
+ if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
1083
+ if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
1084
+ if (has(s, "shadow")) return ["EFFECT_COLOR"];
1085
+ return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
1086
+ case "dimension":
1087
+ if (has(s, "radius")) return ["CORNER_RADIUS"];
1088
+ if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
1089
+ if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
1090
+ if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
1091
+ if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
1092
+ return ["WIDTH_HEIGHT", "GAP"];
1093
+ case "fontWeight":
1094
+ return ["FONT_WEIGHT"];
1095
+ case "fontFamily":
1096
+ return ["FONT_FAMILY"];
1097
+ case "number":
1098
+ if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
1099
+ if (has(s, "opacity")) return ["OPACITY"];
1100
+ return ["WIDTH_HEIGHT", "GAP"];
1101
+ case "string":
1102
+ return ["TEXT_CONTENT"];
1103
+ default:
1104
+ return [];
1105
+ }
1106
+ }
1107
+ var sameScopes = (a, b) => {
1108
+ const sa = [...new Set(a)].sort();
1109
+ const sb = [...new Set(b)].sort();
1110
+ return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
1111
+ };
1112
+ var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
1113
+
1114
+ // ../core/src/diff-engine.ts
1115
+ var normDesc = (d) => (d ?? "").trim();
1116
+ function fieldChanges(a, b) {
1117
+ const out = [];
1118
+ if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
1119
+ if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
1120
+ const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
1121
+ for (const mode of modes) {
1122
+ if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
1123
+ }
1124
+ if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
1125
+ const sa = effectiveScopes(a);
1126
+ const sb = effectiveScopes(b);
1127
+ if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
1128
+ return out;
1129
+ }
1130
+ function sameToken(a, b) {
1131
+ if (!a || !b) return a === b;
1132
+ return fieldChanges(a, b).length === 0;
1133
+ }
1134
+ function sideChange(from, to) {
1135
+ if (!from && !to) return void 0;
1136
+ if (!from) return { kind: "added", fields: [] };
1137
+ if (!to) return { kind: "removed", fields: [] };
1138
+ const fields = fieldChanges(from, to);
1139
+ if (!fields.length) return void 0;
1140
+ return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
1141
+ }
1142
+ function threeWayDiff(base, figma, head) {
1143
+ const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
1144
+ const result = { local: [], remote: [], conflicts: [], unchanged: 0 };
1145
+ for (const id of [...ids].sort()) {
1146
+ const b = base?.get(id);
1147
+ const f = figma.get(id);
1148
+ const h = head.get(id);
1149
+ const local = sideChange(b, f);
1150
+ const remote = sideChange(b, h);
1151
+ if (!local && !remote || f && h && sameToken(f, h) || !f && !h) {
1152
+ result.unchanged++;
1153
+ continue;
1154
+ }
1155
+ const entry = { id, path: (f ?? h ?? b).path, base: b, figma: f, head: h, local, remote, conflict: false };
1156
+ if (local && !remote) {
1157
+ entry.path = (f ?? b).path;
1158
+ result.local.push(entry);
1159
+ } else if (remote && !local) {
1160
+ entry.path = (h ?? b).path;
1161
+ result.remote.push(entry);
1162
+ } else {
1163
+ entry.conflict = true;
1164
+ result.conflicts.push(entry);
1165
+ }
1166
+ }
1167
+ const byPath = (a, b) => a.path.localeCompare(b.path);
1168
+ result.local.sort(byPath);
1169
+ result.remote.sort(byPath);
1170
+ result.conflicts.sort(byPath);
1171
+ return result;
1172
+ }
1173
+
1174
+ // ../core/src/risk.ts
1175
+ var RISK_LABELS = {
1176
+ 1: "Tier 1: low risk, one code owner",
1177
+ 2: "Tier 2: medium risk, design review then design + tech approval",
1178
+ 3: "Tier 3: token change, needs design + tech approval"
1179
+ };
1180
+ function figmaPushTierLabel(tier, changes) {
1181
+ const waits = changes.some((c) => c.kind === "renamed" || c.kind === "removed");
1182
+ return `Tier ${tier}: token change. From Figma: design-approved, ${waits ? "waits for a developer OK (a token renamed or removed can break product code)" : "ships once the checks pass"}`;
1183
+ }
1184
+ var figmaPushNextLine = (changes) => changes.some((c) => c.kind === "renamed" || c.kind === "removed") ? "Pushed from Figma, so design-approved. A developer OKs the rename or removal before it merges." : "Pushed from Figma, so design-approved. Bridge merges it once the automatic checks pass; Undo is in History in Bridge Studio.";
1185
+ var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
1186
+ var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
1187
+ var isSummary = (c) => "kind" in c;
1188
+ function summarizeTokenChange(c) {
1189
+ if (isSummary(c)) return c;
1190
+ const path = (c.target ?? c.current).path;
1191
+ if (!c.target) return { kind: "removed", path };
1192
+ if (!c.current) return { kind: "added", path };
1193
+ if (c.current.path !== c.target.path) return { kind: "renamed", path };
1194
+ const fields = fieldChanges(c.current, c.target);
1195
+ return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
1196
+ }
1197
+ function tokenBump(changes) {
1198
+ let bump;
1199
+ for (const c of changes.map(summarizeTokenChange)) {
1200
+ const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
1201
+ if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
1202
+ }
1203
+ return bump;
1204
+ }
1205
+ function isGuardrail(path, config) {
1206
+ const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
1207
+ return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
1208
+ }
1209
+ function classifyRisk(input) {
1210
+ const reasons = [];
1211
+ const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
1212
+ for (const c of tokenChanges) {
1213
+ if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
1214
+ else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
1215
+ }
1216
+ const bump = tokenBump(tokenChanges);
1217
+ const componentsDir = input.config?.components?.dir;
1218
+ for (const f of input.files ?? []) {
1219
+ const p = f.path.replace(/^\/+/, "");
1220
+ if (p.endsWith(".tokens.json")) {
1221
+ if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
1222
+ } else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
1223
+ else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
1224
+ else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
1225
+ else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
1226
+ }
1227
+ reasons.sort((a, b) => b.tier - a.tier);
1228
+ const tier = reasons[0]?.tier ?? 1;
1229
+ return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
1230
+ }
1231
+
1055
1232
  // ../core/src/layered.ts
1056
1233
  var DTCG_TYPES = /* @__PURE__ */ new Set(["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "number", "strokeStyle", "border", "transition", "shadow", "gradient", "typography", "fontStyle", "string", "boolean"]);
1057
1234
  var aliasOf = (v) => isAlias(v) ? aliasTarget(v) : null;
@@ -1419,159 +1596,6 @@ function layeredParityIssues(set) {
1419
1596
  return out;
1420
1597
  }
1421
1598
 
1422
- // ../core/src/scopes.ts
1423
- var has = (segments, ...names) => names.some((n) => segments.includes(n));
1424
- function defaultScopes(path, type, tier) {
1425
- const s = path.split(".");
1426
- switch (type) {
1427
- case "color":
1428
- if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
1429
- if (has(s, "text")) return ["TEXT_FILL"];
1430
- if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
1431
- if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
1432
- if (has(s, "shadow")) return ["EFFECT_COLOR"];
1433
- return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
1434
- case "dimension":
1435
- if (has(s, "radius")) return ["CORNER_RADIUS"];
1436
- if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
1437
- if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
1438
- if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
1439
- if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
1440
- return ["WIDTH_HEIGHT", "GAP"];
1441
- case "fontWeight":
1442
- return ["FONT_WEIGHT"];
1443
- case "fontFamily":
1444
- return ["FONT_FAMILY"];
1445
- case "number":
1446
- if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
1447
- if (has(s, "opacity")) return ["OPACITY"];
1448
- return ["WIDTH_HEIGHT", "GAP"];
1449
- case "string":
1450
- return ["TEXT_CONTENT"];
1451
- default:
1452
- return [];
1453
- }
1454
- }
1455
- var sameScopes = (a, b) => {
1456
- const sa = [...new Set(a)].sort();
1457
- const sb = [...new Set(b)].sort();
1458
- return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
1459
- };
1460
- var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
1461
-
1462
- // ../core/src/diff-engine.ts
1463
- var normDesc = (d) => (d ?? "").trim();
1464
- function fieldChanges(a, b) {
1465
- const out = [];
1466
- if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
1467
- if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
1468
- const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
1469
- for (const mode of modes) {
1470
- if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
1471
- }
1472
- if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
1473
- const sa = effectiveScopes(a);
1474
- const sb = effectiveScopes(b);
1475
- if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
1476
- return out;
1477
- }
1478
- function sameToken(a, b) {
1479
- if (!a || !b) return a === b;
1480
- return fieldChanges(a, b).length === 0;
1481
- }
1482
- function sideChange(from, to) {
1483
- if (!from && !to) return void 0;
1484
- if (!from) return { kind: "added", fields: [] };
1485
- if (!to) return { kind: "removed", fields: [] };
1486
- const fields = fieldChanges(from, to);
1487
- if (!fields.length) return void 0;
1488
- return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
1489
- }
1490
- function threeWayDiff(base, figma, head) {
1491
- const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
1492
- const result = { local: [], remote: [], conflicts: [], unchanged: 0 };
1493
- for (const id of [...ids].sort()) {
1494
- const b = base?.get(id);
1495
- const f = figma.get(id);
1496
- const h = head.get(id);
1497
- const local = sideChange(b, f);
1498
- const remote = sideChange(b, h);
1499
- if (!local && !remote || f && h && sameToken(f, h) || !f && !h) {
1500
- result.unchanged++;
1501
- continue;
1502
- }
1503
- const entry = { id, path: (f ?? h ?? b).path, base: b, figma: f, head: h, local, remote, conflict: false };
1504
- if (local && !remote) {
1505
- entry.path = (f ?? b).path;
1506
- result.local.push(entry);
1507
- } else if (remote && !local) {
1508
- entry.path = (h ?? b).path;
1509
- result.remote.push(entry);
1510
- } else {
1511
- entry.conflict = true;
1512
- result.conflicts.push(entry);
1513
- }
1514
- }
1515
- const byPath = (a, b) => a.path.localeCompare(b.path);
1516
- result.local.sort(byPath);
1517
- result.remote.sort(byPath);
1518
- result.conflicts.sort(byPath);
1519
- return result;
1520
- }
1521
-
1522
- // ../core/src/risk.ts
1523
- var RISK_LABELS = {
1524
- 1: "Tier 1: low risk, one code owner",
1525
- 2: "Tier 2: medium risk, design review then design + tech approval",
1526
- 3: "Tier 3: token change, needs design + tech approval"
1527
- };
1528
- var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
1529
- var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
1530
- var isSummary = (c) => "kind" in c;
1531
- function summarizeTokenChange(c) {
1532
- if (isSummary(c)) return c;
1533
- const path = (c.target ?? c.current).path;
1534
- if (!c.target) return { kind: "removed", path };
1535
- if (!c.current) return { kind: "added", path };
1536
- if (c.current.path !== c.target.path) return { kind: "renamed", path };
1537
- const fields = fieldChanges(c.current, c.target);
1538
- return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
1539
- }
1540
- function tokenBump(changes) {
1541
- let bump;
1542
- for (const c of changes.map(summarizeTokenChange)) {
1543
- const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
1544
- if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
1545
- }
1546
- return bump;
1547
- }
1548
- function isGuardrail(path, config) {
1549
- const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
1550
- return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
1551
- }
1552
- function classifyRisk(input) {
1553
- const reasons = [];
1554
- const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
1555
- for (const c of tokenChanges) {
1556
- if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
1557
- else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
1558
- }
1559
- const bump = tokenBump(tokenChanges);
1560
- const componentsDir = input.config?.components?.dir;
1561
- for (const f of input.files ?? []) {
1562
- const p = f.path.replace(/^\/+/, "");
1563
- if (p.endsWith(".tokens.json")) {
1564
- if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
1565
- } else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
1566
- else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
1567
- else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
1568
- else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
1569
- }
1570
- reasons.sort((a, b) => b.tier - a.tier);
1571
- const tier = reasons[0]?.tier ?? 1;
1572
- return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
1573
- }
1574
-
1575
1599
  // ../core/src/layered-review.ts
1576
1600
  function checkLayeredPair(set, pair, combo) {
1577
1601
  const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
@@ -1703,14 +1727,15 @@ _${deltas.length - limit} more pair(s) not shown._` : "";
1703
1727
  }
1704
1728
  function composeProposalBody(args) {
1705
1729
  const t = args.tier.tier;
1706
- const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
1730
+ const figma = Boolean(args.fromFigma);
1731
+ const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${figma && n === t ? figmaPushTierLabel(n, args.changes) : RISK_LABELS[n]}`;
1707
1732
  const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
1708
1733
  return [
1709
1734
  "## What changed and why",
1710
1735
  "",
1711
1736
  args.summary.trim(),
1712
1737
  "",
1713
- `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
1738
+ figma ? `Pushed from Figma through ${args.tool ?? "the Bridge Figma plugin"}${args.author ? ` by ${args.author}` : ""}. ${figmaPushNextLine(args.changes)}` : `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
1714
1739
  "",
1715
1740
  "## Risk tier",
1716
1741
  "",
@@ -1740,8 +1765,8 @@ function composeProposalBody(args) {
1740
1765
  "",
1741
1766
  "## Authored by",
1742
1767
  "",
1743
- "- [ ] Human",
1744
- "- [x] AI agent (Claude), reviewed by a human before merge",
1768
+ `- [${figma ? "x" : " "}] Human`,
1769
+ `- [${figma ? " " : "x"}] AI agent (Claude), reviewed by a human before merge`,
1745
1770
  "",
1746
1771
  "## Design review checklist",
1747
1772
  "",
@@ -1755,6 +1780,36 @@ function composeProposalBody(args) {
1755
1780
  // ../core/src/component-spec.ts
1756
1781
  var COMPONENT_SPECS_VERSION = 1;
1757
1782
  var DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
1783
+ var SPEC_FIELDS = [
1784
+ "fill",
1785
+ "stroke",
1786
+ "strokeWeight",
1787
+ "strokeTopWeight",
1788
+ "strokeRightWeight",
1789
+ "strokeBottomWeight",
1790
+ "strokeLeftWeight",
1791
+ "cornerRadius",
1792
+ "paddingTop",
1793
+ "paddingRight",
1794
+ "paddingBottom",
1795
+ "paddingLeft",
1796
+ "itemSpacing",
1797
+ "counterAxisSpacing",
1798
+ "width",
1799
+ "height",
1800
+ "minWidth",
1801
+ "minHeight",
1802
+ "opacity",
1803
+ "shadow",
1804
+ "textFill",
1805
+ "fontFamily",
1806
+ "fontSize",
1807
+ "fontWeight",
1808
+ "lineHeight",
1809
+ "letterSpacing",
1810
+ "textDecoration"
1811
+ ];
1812
+ var COLOR_FIELDS = /* @__PURE__ */ new Set(["fill", "stroke", "textFill"]);
1758
1813
  function coverageSummary(file) {
1759
1814
  const list2 = file.coverage ?? file.components.map((c) => ({ name: c.name, status: "generated" }));
1760
1815
  const generated = list2.filter((c) => c.status === "generated").length;
@@ -1762,6 +1817,18 @@ function coverageSummary(file) {
1762
1817
  const layout = list2.filter((c) => c.status === "layout").length;
1763
1818
  return { generated, drafted, inFigma: generated + drafted, visual: list2.length - layout, layout, total: list2.length, missing: list2.filter((c) => c.status === "unsupported") };
1764
1819
  }
1820
+ function specHash(text) {
1821
+ let h = 2166136261;
1822
+ for (let i = 0; i < text.length; i++) {
1823
+ h ^= text.charCodeAt(i);
1824
+ h = Math.imul(h, 16777619);
1825
+ }
1826
+ return (h >>> 0).toString(16).padStart(8, "0");
1827
+ }
1828
+ function hashSpec(spec) {
1829
+ const { hash: _ignored, example: _example, ...rest } = spec;
1830
+ return specHash(JSON.stringify(rest));
1831
+ }
1765
1832
  var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
1766
1833
  function parseComponentSpecs(text) {
1767
1834
  let data;
@@ -1805,7 +1872,7 @@ var STATE_PSEUDO = {
1805
1872
  var iconName = (component) => component.replace(/^Icon\//, "");
1806
1873
  var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
1807
1874
  function planReference(spec, variant, file) {
1808
- if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
1875
+ if (spec.view === "popup") return planPopup(spec, variant);
1809
1876
  const props = { ...spec.fixed ?? {} };
1810
1877
  let pseudo;
1811
1878
  for (const axis of spec.axes) {
@@ -1834,6 +1901,7 @@ function planReference(spec, variant, file) {
1834
1901
  if (part.swap && swapTo) {
1835
1902
  const slot = file?.slots?.find((s) => s.name === swapTo);
1836
1903
  if (!slot) return { reason: `${swapTo} is not in the specs` };
1904
+ if (slot.kind === "media") return { reason: `Figma shows a media placeholder (${swapTo}); code shows the image it is given, nothing to compare` };
1837
1905
  if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
1838
1906
  return slot.text.default;
1839
1907
  }
@@ -1885,10 +1953,13 @@ function planReference(spec, variant, file) {
1885
1953
  }
1886
1954
  const example = spec.example;
1887
1955
  const exampleChildren = example?.children?.length ? example.children : void 0;
1956
+ const component = exportName(example?.c ?? (spec.name.includes("/") ? spec.component ?? spec.name : spec.name));
1957
+ const parent = spec.component ? exportName(spec.component) : void 0;
1888
1958
  return {
1889
1959
  ok: true,
1890
1960
  render: {
1891
- component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
1961
+ component,
1962
+ ...parent && parent !== component ? { within: parent } : {},
1892
1963
  props: { ...example?.p ?? {}, ...props },
1893
1964
  ...exampleChildren ? { children: exampleChildren } : {},
1894
1965
  ...text !== void 0 && !exampleChildren ? { text } : {},
@@ -1899,6 +1970,28 @@ function planReference(spec, variant, file) {
1899
1970
  }
1900
1971
  };
1901
1972
  }
1973
+ function planPopup(spec, variant) {
1974
+ const root = spec.parts.find((p) => !p.parent);
1975
+ const own = {};
1976
+ for (const axis of spec.axes) {
1977
+ const value = variant.props[axis.name];
1978
+ if (!axis.prop || value === void 0) return { ok: false, reason: `${axis.name} has no matching option in code` };
1979
+ own[axis.prop] = value === "true" ? true : value === "false" ? false : value;
1980
+ }
1981
+ const className = /\.module\.css \.([\w-]+)/.exec(root?.source ?? "")?.[1];
1982
+ const component = spec.component ? exportName(spec.component) : void 0;
1983
+ const example = spec.example;
1984
+ if (!component || !example) return { ok: false, reason: "drawn open from no code example: nothing to open in code" };
1985
+ if (!className) return { ok: false, reason: "the popup has no class to find it by in code" };
1986
+ const popup = { className };
1987
+ if (exportName(example.c) === component) {
1988
+ return { ok: true, render: { component, props: { ...example.p ?? {}, ...own, defaultOpen: true }, ...example.children?.length ? { children: example.children } : {}, popup } };
1989
+ }
1990
+ return {
1991
+ ok: true,
1992
+ render: { component, props: { defaultOpen: true }, children: [{ c: `${component}Trigger`, children: ["Open"] }, { ...example, p: { ...example.p ?? {}, ...own } }], popup }
1993
+ };
1994
+ }
1902
1995
  function referenceImagePath(component, key) {
1903
1996
  const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1904
1997
  const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
@@ -2053,6 +2146,7 @@ export {
2053
2146
  CONFIG_FILE,
2054
2147
  DEFAULT_OVERRIDE,
2055
2148
  DEFAULT_TOKEN_PATHS,
2149
+ DEFAULT_GOVERNANCE,
2056
2150
  loadConfig,
2057
2151
  tokensPath,
2058
2152
  protectedBranchesOf,
@@ -2067,6 +2161,7 @@ export {
2067
2161
  parseContrastChecks,
2068
2162
  threeWayDiff,
2069
2163
  RISK_LABELS,
2164
+ isGuardrail,
2070
2165
  classifyRisk,
2071
2166
  aliasOf,
2072
2167
  globToRegExp,
@@ -2088,7 +2183,10 @@ export {
2088
2183
  ProtectedBranchError,
2089
2184
  assertSafeBranch,
2090
2185
  DEFAULT_COMPONENT_SPECS_FILE,
2186
+ SPEC_FIELDS,
2187
+ COLOR_FIELDS,
2091
2188
  coverageSummary,
2189
+ hashSpec,
2092
2190
  variantKeyOf,
2093
2191
  parseComponentSpecs,
2094
2192
  figmaCell,
@@ -25,7 +25,9 @@ export declare function parseProps(src: string, name: string): {
25
25
  props: PropInfo[];
26
26
  } | null;
27
27
  export declare function cssVarsUsed(css: string): string[];
28
- export declare function getComponent(source: RepoSource, config: BridgeConfig, set: TokenSet | null, name: string): Promise<{
28
+ export declare function getComponent(source: RepoSource, config: BridgeConfig, set: TokenSet | null, name: string, ctx?: {
29
+ surface: 'mcp' | 'cli';
30
+ }): Promise<{
29
31
  tokens: {
30
32
  notTokens?: string[] | undefined;
31
33
  used: string[];
package/dist/context.d.ts CHANGED
@@ -13,7 +13,22 @@ export interface ContextOptions {
13
13
  fetch?: typeof fetch;
14
14
  apiUrl?: string;
15
15
  rawUrl?: string;
16
+ /**
17
+ * Read only: every request that would change something on GitHub (anything but GET and HEAD, and GraphQL
18
+ * mutations) is refused before it leaves, with this sentence. The CLI sets it when the token is one the person did
19
+ * not pass for this command (their gh sign-in, used to read).
20
+ */
21
+ readOnly?: string;
22
+ /** Who reads the messages: the MCP server's client (tool names and arguments) or the bridge CLI (commands and flags). */
23
+ surface?: 'mcp' | 'cli';
24
+ /** How a person adds a token where Bridge runs, for the rate-limit message. Default: the MCP server's environment. */
25
+ tokenHint?: string;
16
26
  }
27
+ /** Whether a request only reads: GET and HEAD, and GraphQL queries (POST /graphql without a mutation). */
28
+ export declare function isReadRequest(url: string, init?: {
29
+ method?: string;
30
+ body?: unknown;
31
+ }): boolean;
17
32
  export declare class BridgeContext {
18
33
  readonly mode: 'local' | 'remote';
19
34
  readonly source: RepoSource;
@@ -38,5 +53,12 @@ export declare class BridgeContext {
38
53
  */
39
54
  github(): Promise<GitHubClient | null>;
40
55
  get canWriteRemote(): boolean;
56
+ /** Who reads the messages: 'mcp' (tool names and arguments, the default) or 'cli' (commands and flags). */
57
+ get surface(): 'mcp' | 'cli';
41
58
  describe(): string;
59
+ /**
60
+ * What Bridge may write here, in one sentence: the same words in bridge_get_config, the server instructions, the
61
+ * server's start line and (for the CLI) `bridge --help`. Bridge never writes to the default branch.
62
+ */
63
+ access(): string;
42
64
  }