@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.
- package/README.md +92 -49
- package/dist/branches.d.ts +85 -10
- package/dist/build-requests.d.ts +96 -0
- package/dist/{chunk-HZWFI7A2.js → chunk-TP33MW7E.js} +3705 -836
- package/dist/{chunk-PMZGEZGX.js → chunk-ZVORAO5H.js} +258 -160
- package/dist/components.d.ts +3 -1
- package/dist/context.d.ts +22 -0
- package/dist/figma-fixes.d.ts +206 -0
- package/dist/figma.d.ts +2 -1
- package/dist/history.d.ts +149 -6
- package/dist/index.js +8 -4
- package/dist/lib.d.ts +15 -2
- package/dist/lib.js +18 -4
- package/dist/ops.d.ts +41 -0
- package/dist/say.d.ts +22 -0
- package/dist/{src-DRJFSLRV.js → src-RUS6SPYZ.js} +75 -20
- package/dist/studio.d.ts +10 -0
- package/dist/suggestions.d.ts +3 -1
- package/package.json +4 -3
|
@@ -171,7 +171,7 @@ function resolveBranchPreviews(apps, ctx, sources = {}) {
|
|
|
171
171
|
}
|
|
172
172
|
return out;
|
|
173
173
|
}
|
|
174
|
-
var previewsSettled = (previews) => previews.every((p) =>
|
|
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
|
|
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
|
-
|
|
1744
|
-
"
|
|
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
|
|
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
|
|
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,
|
package/dist/components.d.ts
CHANGED
|
@@ -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
|
|
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
|
}
|