@bridge-ds/mcp 0.9.0 → 0.11.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.
- package/LICENSE +110 -0
- package/README.md +9 -7
- package/dist/branches.d.ts +5 -1
- package/dist/{chunk-TP33MW7E.js → chunk-LI4FNPYK.js} +5064 -3726
- package/dist/{chunk-ZVORAO5H.js → chunk-WSNJYGA5.js} +212 -164
- package/dist/context.d.ts +19 -0
- package/dist/explorations.d.ts +175 -14
- package/dist/figma-fixes.d.ts +40 -12
- package/dist/figma.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +20 -2
- package/dist/lib.js +16 -2
- package/dist/ops.d.ts +1 -0
- package/dist/pieces.d.ts +11 -4
- package/dist/product-link.d.ts +42 -0
- package/dist/product-repos.d.ts +25 -0
- package/dist/server.d.ts +28 -1
- package/dist/{src-RUS6SPYZ.js → src-P6G5JWLN.js} +1 -1
- package/dist/studio.d.ts +1 -0
- package/dist/suggestions.d.ts +18 -1
- package/package.json +4 -4
|
@@ -119,7 +119,7 @@ var STATE_OF = {
|
|
|
119
119
|
deployed: "ready",
|
|
120
120
|
ready: "ready",
|
|
121
121
|
ignored: "ready",
|
|
122
|
-
// Vercel skipped the build
|
|
122
|
+
// Vercel skipped the build: nothing to wait for. BranchPreview.skipped says the branch has no build of its own.
|
|
123
123
|
pending: "building",
|
|
124
124
|
queued: "building",
|
|
125
125
|
in_progress: "building",
|
|
@@ -131,13 +131,15 @@ var STATE_OF = {
|
|
|
131
131
|
canceled: "failed"
|
|
132
132
|
};
|
|
133
133
|
var stateOf = (s) => STATE_OF[(s ?? "").toLowerCase()] ?? "unknown";
|
|
134
|
+
var IGNORED_BUILD = /ignored build step/i;
|
|
134
135
|
var isInspectorUrl = (u) => /^https:\/\/(?:www\.)?vercel\.com\//i.test(u);
|
|
135
136
|
function resolveBranchPreviews(apps, ctx, sources = {}) {
|
|
136
137
|
const seen = /* @__PURE__ */ new Map();
|
|
137
138
|
const at = (app) => seen.get(app) ?? (seen.set(app, {}), seen.get(app));
|
|
138
|
-
const offer = (app, url, urlSource, state, stateRank) => {
|
|
139
|
+
const offer = (app, url, urlSource, state, stateRank, skipped) => {
|
|
139
140
|
if (!app) return;
|
|
140
141
|
const s = at(app);
|
|
142
|
+
if (skipped !== void 0 && (s.skipRank === void 0 || stateRank < s.skipRank)) Object.assign(s, { skipped, skipRank: stateRank });
|
|
141
143
|
if (url && !s.url) Object.assign(s, { url, urlSource });
|
|
142
144
|
if (state !== "unknown" && (s.stateRank === void 0 || stateRank < s.stateRank)) Object.assign(s, { state, stateRank });
|
|
143
145
|
};
|
|
@@ -148,7 +150,7 @@ function resolveBranchPreviews(apps, ctx, sources = {}) {
|
|
|
148
150
|
const project = projectOfLabel(st.context);
|
|
149
151
|
if (!project) continue;
|
|
150
152
|
const url = httpsUrl(st.target_url);
|
|
151
|
-
offer(appForProject(project, apps), url && !isInspectorUrl(url) ? url : null, "status", stateOf(st.state), 0);
|
|
153
|
+
offer(appForProject(project, apps), url && !isInspectorUrl(url) ? url : null, "status", stateOf(st.state), 0, IGNORED_BUILD.test(st.description ?? ""));
|
|
152
154
|
}
|
|
153
155
|
for (const d of sources.deployments ?? []) {
|
|
154
156
|
const project = projectOfLabel(d.environment);
|
|
@@ -160,17 +162,18 @@ function resolveBranchPreviews(apps, ctx, sources = {}) {
|
|
|
160
162
|
const state = stateOf(d.state);
|
|
161
163
|
if (state !== "unknown" && (s.stateRank === void 0 || 1 < s.stateRank)) Object.assign(s, { state, stateRank: 1 });
|
|
162
164
|
}
|
|
163
|
-
for (const p of parseVercelComment(findVercelComment(sources.comments ?? [])?.body) ?? []) offer(appForProject(p.name, apps), p.previewUrl, "vercel-comment", stateOf(p.status), 2);
|
|
165
|
+
for (const p of parseVercelComment(findVercelComment(sources.comments ?? [])?.body) ?? []) offer(appForProject(p.name, apps), p.previewUrl, "vercel-comment", stateOf(p.status), 2, p.status ? p.status.toUpperCase() === "IGNORED" : void 0);
|
|
164
166
|
const out = [];
|
|
165
167
|
for (const app of Object.keys(PREVIEW_APP_LABELS)) {
|
|
166
168
|
if (!apps[app]) continue;
|
|
167
169
|
const s = seen.get(app) ?? {};
|
|
168
170
|
const url = s.url ?? previewUrl(apps[app], ctx);
|
|
169
171
|
const source = s.url ? s.urlSource : url ? "template" : null;
|
|
170
|
-
out.push({ app, label: PREVIEW_APP_LABELS[app], url, state: s.state ?? "unknown", source });
|
|
172
|
+
out.push({ app, label: PREVIEW_APP_LABELS[app], url, state: s.state ?? "unknown", source, ...s.skipped ? { skipped: true } : {} });
|
|
171
173
|
}
|
|
172
174
|
return out;
|
|
173
175
|
}
|
|
176
|
+
var previewLinksOf = (previews) => previews.filter((p) => !!p.url && !p.skipped).map((p) => ({ app: p.app, label: p.label, url: p.url }));
|
|
174
177
|
var previewsSettled = (previews) => previews.every((p) => p.state === "failed" || !!p.url && p.state === "ready");
|
|
175
178
|
|
|
176
179
|
// ../core/src/teams.ts
|
|
@@ -533,6 +536,27 @@ function productForPlace(products, place) {
|
|
|
533
536
|
}
|
|
534
537
|
return { product: defaultProduct(products), how: "default" };
|
|
535
538
|
}
|
|
539
|
+
function productRepoOf(p, designSystemRepo) {
|
|
540
|
+
const repo = p?.repo?.trim();
|
|
541
|
+
if (!repo || !REPO.test(repo)) return null;
|
|
542
|
+
return repo.toLowerCase() === (designSystemRepo ?? "").trim().toLowerCase() ? null : repo;
|
|
543
|
+
}
|
|
544
|
+
function productRepos(products, designSystemRepo) {
|
|
545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
546
|
+
const out = [];
|
|
547
|
+
for (const p of products) {
|
|
548
|
+
const repo = productRepoOf(p, designSystemRepo);
|
|
549
|
+
if (!repo || seen.has(repo.toLowerCase())) continue;
|
|
550
|
+
seen.add(repo.toLowerCase());
|
|
551
|
+
out.push({ product: p, repo });
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
function productOfRepo(products, repo, designSystemRepo) {
|
|
556
|
+
const r = (repo ?? "").trim().toLowerCase();
|
|
557
|
+
if (!r) return void 0;
|
|
558
|
+
return productRepos(products, designSystemRepo).find((x) => x.repo.toLowerCase() === r)?.product;
|
|
559
|
+
}
|
|
536
560
|
var KEY = /^[a-z0-9][a-z0-9-]*$/;
|
|
537
561
|
var REPO = /^[\w.-]+\/[\w.-]+$/;
|
|
538
562
|
var FIELDS = ["key", "name", "repo", "path", "preview", "framework", "default"];
|
|
@@ -722,7 +746,8 @@ function defineConfig(input) {
|
|
|
722
746
|
}
|
|
723
747
|
} : {},
|
|
724
748
|
...g.rights ? { rights: normalizeRights(g.rights) } : {},
|
|
725
|
-
...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {}
|
|
749
|
+
...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {},
|
|
750
|
+
...g.leadsPublishTier1 === false ? { leadsPublishTier1: false } : {}
|
|
726
751
|
},
|
|
727
752
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
728
753
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
@@ -850,6 +875,7 @@ function loadConfig(source) {
|
|
|
850
875
|
}
|
|
851
876
|
problems.push(...rightsProblems(gov.rights));
|
|
852
877
|
if (gov.soloOwnerCanApproveBoth !== void 0 && typeof gov.soloOwnerCanApproveBoth !== "boolean") problems.push('"governance.soloOwnerCanApproveBoth" must be true or false');
|
|
878
|
+
if (gov.leadsPublishTier1 !== void 0 && typeof gov.leadsPublishTier1 !== "boolean") problems.push('"governance.leadsPublishTier1" must be true or false');
|
|
853
879
|
}
|
|
854
880
|
}
|
|
855
881
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
@@ -1071,164 +1097,6 @@ function parseContrastChecks(text) {
|
|
|
1071
1097
|
return out;
|
|
1072
1098
|
}
|
|
1073
1099
|
|
|
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
|
-
|
|
1232
1100
|
// ../core/src/layered.ts
|
|
1233
1101
|
var DTCG_TYPES = /* @__PURE__ */ new Set(["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "number", "strokeStyle", "border", "transition", "shadow", "gradient", "typography", "fontStyle", "string", "boolean"]);
|
|
1234
1102
|
var aliasOf = (v) => isAlias(v) ? aliasTarget(v) : null;
|
|
@@ -1596,6 +1464,164 @@ function layeredParityIssues(set) {
|
|
|
1596
1464
|
return out;
|
|
1597
1465
|
}
|
|
1598
1466
|
|
|
1467
|
+
// ../core/src/scopes.ts
|
|
1468
|
+
var has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
1469
|
+
function defaultScopes(path, type, tier) {
|
|
1470
|
+
const s = path.split(".");
|
|
1471
|
+
switch (type) {
|
|
1472
|
+
case "color":
|
|
1473
|
+
if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
|
|
1474
|
+
if (has(s, "text")) return ["TEXT_FILL"];
|
|
1475
|
+
if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
|
|
1476
|
+
if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
|
|
1477
|
+
if (has(s, "shadow")) return ["EFFECT_COLOR"];
|
|
1478
|
+
return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
|
|
1479
|
+
case "dimension":
|
|
1480
|
+
if (has(s, "radius")) return ["CORNER_RADIUS"];
|
|
1481
|
+
if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
|
|
1482
|
+
if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
|
|
1483
|
+
if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
|
|
1484
|
+
if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
|
|
1485
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
1486
|
+
case "fontWeight":
|
|
1487
|
+
return ["FONT_WEIGHT"];
|
|
1488
|
+
case "fontFamily":
|
|
1489
|
+
return ["FONT_FAMILY"];
|
|
1490
|
+
case "number":
|
|
1491
|
+
if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
|
|
1492
|
+
if (has(s, "opacity")) return ["OPACITY"];
|
|
1493
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
1494
|
+
case "string":
|
|
1495
|
+
return ["TEXT_CONTENT"];
|
|
1496
|
+
default:
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
var sameScopes = (a, b) => {
|
|
1501
|
+
const sa = [...new Set(a)].sort();
|
|
1502
|
+
const sb = [...new Set(b)].sort();
|
|
1503
|
+
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
1504
|
+
};
|
|
1505
|
+
var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
1506
|
+
|
|
1507
|
+
// ../core/src/diff-engine.ts
|
|
1508
|
+
var normDesc = (d) => (d ?? "").trim();
|
|
1509
|
+
function fieldChanges(a, b) {
|
|
1510
|
+
const out = [];
|
|
1511
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
1512
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
1513
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
1514
|
+
for (const mode of modes) {
|
|
1515
|
+
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
1516
|
+
}
|
|
1517
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
1518
|
+
const sa = effectiveScopes(a);
|
|
1519
|
+
const sb = effectiveScopes(b);
|
|
1520
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
1521
|
+
return out;
|
|
1522
|
+
}
|
|
1523
|
+
function sameToken(a, b) {
|
|
1524
|
+
if (!a || !b) return a === b;
|
|
1525
|
+
return fieldChanges(a, b).length === 0;
|
|
1526
|
+
}
|
|
1527
|
+
function sideChange(from, to) {
|
|
1528
|
+
if (!from && !to) return void 0;
|
|
1529
|
+
if (!from) return { kind: "added", fields: [] };
|
|
1530
|
+
if (!to) return { kind: "removed", fields: [] };
|
|
1531
|
+
const fields = fieldChanges(from, to);
|
|
1532
|
+
if (!fields.length) return void 0;
|
|
1533
|
+
return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
|
|
1534
|
+
}
|
|
1535
|
+
function threeWayDiff(base, figma, head) {
|
|
1536
|
+
const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
|
|
1537
|
+
const result = { local: [], remote: [], conflicts: [], unchanged: 0 };
|
|
1538
|
+
for (const id of [...ids].sort()) {
|
|
1539
|
+
const b = base?.get(id);
|
|
1540
|
+
const f = figma.get(id);
|
|
1541
|
+
const h = head.get(id);
|
|
1542
|
+
const local = sideChange(b, f);
|
|
1543
|
+
const remote = sideChange(b, h);
|
|
1544
|
+
if (!local && !remote || f && h && sameToken(f, h) || !f && !h) {
|
|
1545
|
+
result.unchanged++;
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const entry = { id, path: (f ?? h ?? b).path, base: b, figma: f, head: h, local, remote, conflict: false };
|
|
1549
|
+
if (local && !remote) {
|
|
1550
|
+
entry.path = (f ?? b).path;
|
|
1551
|
+
result.local.push(entry);
|
|
1552
|
+
} else if (remote && !local) {
|
|
1553
|
+
entry.path = (h ?? b).path;
|
|
1554
|
+
result.remote.push(entry);
|
|
1555
|
+
} else {
|
|
1556
|
+
entry.conflict = true;
|
|
1557
|
+
result.conflicts.push(entry);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const byPath = (a, b) => a.path.localeCompare(b.path);
|
|
1561
|
+
result.local.sort(byPath);
|
|
1562
|
+
result.remote.sort(byPath);
|
|
1563
|
+
result.conflicts.sort(byPath);
|
|
1564
|
+
return result;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// ../core/src/risk.ts
|
|
1568
|
+
var RISK_LABELS = {
|
|
1569
|
+
1: "Tier 1: low risk, one code owner",
|
|
1570
|
+
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
1571
|
+
3: "Tier 3: token change, needs design + tech approval"
|
|
1572
|
+
};
|
|
1573
|
+
function figmaPushTierLabel(tier, changes) {
|
|
1574
|
+
const waits = changes.some((c) => c.kind === "renamed" || c.kind === "removed");
|
|
1575
|
+
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"}`;
|
|
1576
|
+
}
|
|
1577
|
+
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.";
|
|
1578
|
+
var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
1579
|
+
var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
1580
|
+
var isSummary = (c) => "kind" in c;
|
|
1581
|
+
function summarizeTokenChange(c) {
|
|
1582
|
+
if (isSummary(c)) return c;
|
|
1583
|
+
const path = (c.target ?? c.current).path;
|
|
1584
|
+
if (!c.target) return { kind: "removed", path };
|
|
1585
|
+
if (!c.current) return { kind: "added", path };
|
|
1586
|
+
if (c.current.path !== c.target.path) return { kind: "renamed", path };
|
|
1587
|
+
const fields = fieldChanges(c.current, c.target);
|
|
1588
|
+
return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
|
|
1589
|
+
}
|
|
1590
|
+
function tokenBump(changes) {
|
|
1591
|
+
let bump;
|
|
1592
|
+
for (const c of changes.map(summarizeTokenChange)) {
|
|
1593
|
+
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
1594
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
1595
|
+
}
|
|
1596
|
+
return bump;
|
|
1597
|
+
}
|
|
1598
|
+
function isGuardrail(path, config) {
|
|
1599
|
+
const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
|
|
1600
|
+
return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
|
|
1601
|
+
}
|
|
1602
|
+
function classifyRisk(input) {
|
|
1603
|
+
const reasons = [];
|
|
1604
|
+
const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
|
|
1605
|
+
for (const c of tokenChanges) {
|
|
1606
|
+
if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
|
|
1607
|
+
else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
|
|
1608
|
+
}
|
|
1609
|
+
const bump = tokenBump(tokenChanges);
|
|
1610
|
+
const componentsDir = input.config?.components?.dir;
|
|
1611
|
+
for (const f of input.files ?? []) {
|
|
1612
|
+
const p = f.path.replace(/^\/+/, "");
|
|
1613
|
+
if (p.endsWith(".tokens.json")) {
|
|
1614
|
+
if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
|
|
1615
|
+
} else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
|
|
1616
|
+
else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
|
|
1617
|
+
else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
|
|
1618
|
+
else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
|
|
1619
|
+
}
|
|
1620
|
+
reasons.sort((a, b) => b.tier - a.tier);
|
|
1621
|
+
const tier = reasons[0]?.tier ?? 1;
|
|
1622
|
+
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1599
1625
|
// ../core/src/layered-review.ts
|
|
1600
1626
|
function checkLayeredPair(set, pair, combo) {
|
|
1601
1627
|
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
@@ -1830,6 +1856,23 @@ function hashSpec(spec) {
|
|
|
1830
1856
|
return specHash(JSON.stringify(rest));
|
|
1831
1857
|
}
|
|
1832
1858
|
var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
1859
|
+
function matchVariant(spec, props) {
|
|
1860
|
+
if (!spec) return void 0;
|
|
1861
|
+
const key = variantKeyOf(props);
|
|
1862
|
+
const exact = spec.variants.find((v) => variantKeyOf(v.props) === key);
|
|
1863
|
+
if (exact) return exact;
|
|
1864
|
+
const asked = Object.entries(props);
|
|
1865
|
+
const fits = spec.variants.filter((v) => Object.keys(v.props).length > asked.length && asked.every(([k, val]) => v.props[k] === val));
|
|
1866
|
+
if (fits.length <= 1) return fits[0];
|
|
1867
|
+
const atDefault = (name, value) => {
|
|
1868
|
+
const axis = spec.axes?.find((a) => a.name === name);
|
|
1869
|
+
if (axis) return value === axis.default;
|
|
1870
|
+
const v = value.toLowerCase();
|
|
1871
|
+
return v === "false" || v === "default";
|
|
1872
|
+
};
|
|
1873
|
+
const best = fits.filter((v) => Object.entries(v.props).every(([k, val]) => k in props || atDefault(k, val)));
|
|
1874
|
+
return best.length === 1 ? best[0] : void 0;
|
|
1875
|
+
}
|
|
1833
1876
|
function parseComponentSpecs(text) {
|
|
1834
1877
|
let data;
|
|
1835
1878
|
try {
|
|
@@ -2122,6 +2165,7 @@ export {
|
|
|
2122
2165
|
readPreviewComment,
|
|
2123
2166
|
findPreviewComment,
|
|
2124
2167
|
resolveBranchPreviews,
|
|
2168
|
+
previewLinksOf,
|
|
2125
2169
|
previewsSettled,
|
|
2126
2170
|
expandOwners,
|
|
2127
2171
|
entryLabel,
|
|
@@ -2143,6 +2187,9 @@ export {
|
|
|
2143
2187
|
findProduct,
|
|
2144
2188
|
productOfBranch,
|
|
2145
2189
|
productForPlace,
|
|
2190
|
+
productRepoOf,
|
|
2191
|
+
productRepos,
|
|
2192
|
+
productOfRepo,
|
|
2146
2193
|
CONFIG_FILE,
|
|
2147
2194
|
DEFAULT_OVERRIDE,
|
|
2148
2195
|
DEFAULT_TOKEN_PATHS,
|
|
@@ -2188,6 +2235,7 @@ export {
|
|
|
2188
2235
|
coverageSummary,
|
|
2189
2236
|
hashSpec,
|
|
2190
2237
|
variantKeyOf,
|
|
2238
|
+
matchVariant,
|
|
2191
2239
|
parseComponentSpecs,
|
|
2192
2240
|
figmaCell,
|
|
2193
2241
|
FIGMA_REFERENCES_BRANCH,
|
package/dist/context.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface ContextOptions {
|
|
|
23
23
|
surface?: 'mcp' | 'cli';
|
|
24
24
|
/** How a person adds a token where Bridge runs, for the rate-limit message. Default: the MCP server's environment. */
|
|
25
25
|
tokenHint?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Remote mode run inside a connected product repository (`.bridge/product.json`, product-link.ts): that checkout,
|
|
28
|
+
* which continuing one of its explorations switches to the branch when safe.
|
|
29
|
+
*/
|
|
30
|
+
workTree?: string;
|
|
31
|
+
/** The product key of that repository: new work and a bare reference (#12) default to it. */
|
|
32
|
+
product?: string;
|
|
26
33
|
}
|
|
27
34
|
/** Whether a request only reads: GET and HEAD, and GraphQL queries (POST /graphql without a mutation). */
|
|
28
35
|
export declare function isReadRequest(url: string, init?: {
|
|
@@ -52,6 +59,18 @@ export declare class BridgeContext {
|
|
|
52
59
|
* branch are refused, new branches need the agent prefix. Local mode uses config.repo or the origin remote.
|
|
53
60
|
*/
|
|
54
61
|
github(): Promise<GitHubClient | null>;
|
|
62
|
+
/**
|
|
63
|
+
* A client for another repository (a product's own one) with the same token, fetch and hosts. Its default branch
|
|
64
|
+
* and the protected ones are refused, new branches need `branchPrefix`.
|
|
65
|
+
*/
|
|
66
|
+
githubFor(repo: string, rules: {
|
|
67
|
+
protectedBranches: string[];
|
|
68
|
+
branchPrefix: string;
|
|
69
|
+
}): GitHubClient;
|
|
70
|
+
/** The local checkout Bridge may switch branches in: the repo root in local mode, the connected product checkout in remote mode. */
|
|
71
|
+
get checkout(): string | undefined;
|
|
72
|
+
/** The product of the connected product repository Bridge runs in (`.bridge/product.json`), if any. */
|
|
73
|
+
get product(): string | undefined;
|
|
55
74
|
get canWriteRemote(): boolean;
|
|
56
75
|
/** Who reads the messages: 'mcp' (tool names and arguments, the default) or 'cli' (commands and flags). */
|
|
57
76
|
get surface(): 'mcp' | 'cli';
|