@bridge-ds/mcp 0.9.0 → 0.10.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 +1 -1
- package/dist/branches.d.ts +2 -0
- package/dist/{chunk-ZVORAO5H.js → chunk-BZEPCY42.js} +161 -159
- package/dist/{chunk-TP33MW7E.js → chunk-E5V42VCS.js} +349 -84
- package/dist/figma-fixes.d.ts +40 -12
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +19 -2
- package/dist/lib.js +6 -2
- package/dist/ops.d.ts +1 -0
- package/dist/server.d.ts +28 -1
- package/dist/{src-RUS6SPYZ.js → src-ATD4DJF3.js} +1 -1
- package/dist/studio.d.ts +1 -0
- package/dist/suggestions.d.ts +18 -1
- package/package.json +4 -4
|
@@ -84,11 +84,12 @@ import {
|
|
|
84
84
|
validateLayered,
|
|
85
85
|
variantKeyOf,
|
|
86
86
|
whoCanShipSentence
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-BZEPCY42.js";
|
|
88
88
|
|
|
89
89
|
// src/lib.ts
|
|
90
90
|
import { execFileSync } from "node:child_process";
|
|
91
91
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
92
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
92
93
|
|
|
93
94
|
// src/context.ts
|
|
94
95
|
import { resolve as resolve2 } from "node:path";
|
|
@@ -1831,6 +1832,7 @@ var GitHubClient = class _GitHubClient {
|
|
|
1831
1832
|
const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
1832
1833
|
const r = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`);
|
|
1833
1834
|
if (r.type !== "file") return void 0;
|
|
1835
|
+
if (r.encoding === "none" || !r.content && (r.size ?? 0) > 0) return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`, void 0, true, { accept: ACCEPT_RAW, text: true });
|
|
1834
1836
|
if (r.encoding === "base64" && r.content !== void 0) return base64ToUtf8(r.content);
|
|
1835
1837
|
return r.content;
|
|
1836
1838
|
} catch (err) {
|
|
@@ -4586,11 +4588,19 @@ function pairParts(staticParts, measured, o = {}) {
|
|
|
4586
4588
|
};
|
|
4587
4589
|
const ancestors = (id) => {
|
|
4588
4590
|
const out = [];
|
|
4589
|
-
|
|
4591
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
4592
|
+
for (let p = mParts.get(id)?.parent; p && !seen.has(p); p = mParts.get(p)?.parent) {
|
|
4593
|
+
seen.add(p);
|
|
4594
|
+
out.push(p);
|
|
4595
|
+
}
|
|
4590
4596
|
return out;
|
|
4591
4597
|
};
|
|
4592
4598
|
const pairedAncestor = (p) => {
|
|
4593
|
-
|
|
4599
|
+
const seen = /* @__PURE__ */ new Set([p.id]);
|
|
4600
|
+
for (let a = p.parent; a && !seen.has(a); a = sParts.get(a)?.parent) {
|
|
4601
|
+
if (pairs.has(a)) return pairs.get(a);
|
|
4602
|
+
seen.add(a);
|
|
4603
|
+
}
|
|
4594
4604
|
return void 0;
|
|
4595
4605
|
};
|
|
4596
4606
|
const best = (p, cands) => {
|
|
@@ -4759,6 +4769,16 @@ function parseMeasuredSpecsFile(input) {
|
|
|
4759
4769
|
return data;
|
|
4760
4770
|
}
|
|
4761
4771
|
|
|
4772
|
+
// ../core/src/fidelity.ts
|
|
4773
|
+
var FIDELITY_DEFAULTS = { scale: 2, threshold: 0.1, maxPercent: 3, sizeTolerance: 1 };
|
|
4774
|
+
function sizeGap(s, tolerance = FIDELITY_DEFAULTS.sizeTolerance) {
|
|
4775
|
+
const r = (n) => Math.round(n);
|
|
4776
|
+
const parts = [];
|
|
4777
|
+
if (Math.abs(s.figma.width - s.code.width) > tolerance) parts.push(`width ${r(s.figma.width)} in Figma vs ${r(s.code.width)} in code`);
|
|
4778
|
+
if (Math.abs(s.figma.height - s.code.height) > tolerance) parts.push(`height ${r(s.figma.height)} in Figma vs ${r(s.code.height)} in code`);
|
|
4779
|
+
return parts.length ? parts.join(", ") : void 0;
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4762
4782
|
// ../core/src/gap-case.ts
|
|
4763
4783
|
var GAP_CASE_VERSION = 1;
|
|
4764
4784
|
var lc = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -4792,8 +4812,10 @@ function storyForVariant(spec, props, stories, shot) {
|
|
|
4792
4812
|
}
|
|
4793
4813
|
return order(ok2).find(fits);
|
|
4794
4814
|
}
|
|
4815
|
+
var ICON_LIBRARY = /^(lucide|heroicons?|phosphor|tabler|feather|material-?icons?|fa|fontawesome|svg)(?:$|[-_])/i;
|
|
4795
4816
|
function partWords(part, id = part?.id ?? "") {
|
|
4796
4817
|
if (id === "root") return "component";
|
|
4818
|
+
if (ICON_LIBRARY.test(part?.id ?? id)) return "icon";
|
|
4797
4819
|
const base = (part?.id ?? id).replace(/^[a-z]{1,6}--/i, "").replace(/[-_]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().toLowerCase();
|
|
4798
4820
|
return base || (part?.name ?? id).toLowerCase();
|
|
4799
4821
|
}
|
|
@@ -5057,6 +5079,8 @@ function gapSummary(component, variants, checked, fidelity) {
|
|
|
5057
5079
|
if (!variants.length) return checked ? `${component} matches the code on ${checked} variant${checked === 1 ? "" : "s"}.` : `${component} has no variant to compare.`;
|
|
5058
5080
|
const first2 = variants.flatMap((v) => v.differences)[0];
|
|
5059
5081
|
const head = `${component} differs from the code on ${variants.length} of ${checked} variant${checked === 1 ? "" : "s"}`;
|
|
5082
|
+
const sized = variants.map((v) => v.size ? sizeGap(v.size) : void 0).find(Boolean);
|
|
5083
|
+
if (!first2 && sized) return `${head}: ${sized}.`;
|
|
5060
5084
|
if (!first2) return variants.some((v) => (v.percent ?? 0) > UNMEASURED_PERCENT) ? `${head}: Bridge doesn't measure what differs here yet (the measured values agree, the images do not).` : `${head}: the measure agrees, compare the images.`;
|
|
5061
5085
|
const more = variants.reduce((n, v) => n + v.differences.length, 0) - 1;
|
|
5062
5086
|
return `${head}. ${first2.text}${more > 0 ? `, and ${more} more` : ""}.`;
|
|
@@ -5784,10 +5808,13 @@ function drawnAxes(axes, variants) {
|
|
|
5784
5808
|
}
|
|
5785
5809
|
function specsFromMeasure(file, opts = {}) {
|
|
5786
5810
|
const known = opts.known ?? (() => true);
|
|
5811
|
+
const key = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
5812
|
+
const only = opts.only === void 0 ? void 0 : key(opts.only);
|
|
5787
5813
|
const groups = /* @__PURE__ */ new Map();
|
|
5788
5814
|
for (const s of file.stories) {
|
|
5789
5815
|
if (s.error || !s.spec) continue;
|
|
5790
5816
|
const name = setNameOf3(s.title);
|
|
5817
|
+
if (only !== void 0 && key(name) !== only) continue;
|
|
5791
5818
|
groups.set(name, [...groups.get(name) ?? [], s]);
|
|
5792
5819
|
}
|
|
5793
5820
|
const argTypesOf = (name) => Object.assign({}, ...(file.components ?? []).filter((c) => setNameOf3(c.title) === name).map((c) => c.argTypes));
|
|
@@ -5881,7 +5908,7 @@ function figmaGaps(measured, run2) {
|
|
|
5881
5908
|
}
|
|
5882
5909
|
function gapCaseFor(input) {
|
|
5883
5910
|
const fixed = input.fixes ? applyFixes(input.measured, input.fixes).file : input.measured;
|
|
5884
|
-
const { specs, shots } = specsFromMeasure(fixed);
|
|
5911
|
+
const { specs, shots } = specsFromMeasure(fixed, { only: input.component });
|
|
5885
5912
|
const spec = specs.components.find((c) => lc3(c.name) === lc3(input.component));
|
|
5886
5913
|
const fidelity = input.run?.report.components.find((c) => lc3(c.component) === lc3(input.component));
|
|
5887
5914
|
const setShots = byName(shots, input.component) ?? {};
|
|
@@ -5939,10 +5966,10 @@ function tryFigmaFixes(input) {
|
|
|
5939
5966
|
const before = gapCaseFor({ component, measured: input.measured, fixes: input.fixes, run: input.run });
|
|
5940
5967
|
const afterRun = applyFixes(input.measured, file);
|
|
5941
5968
|
const applied = afterRun.applied.filter((a) => proposedIds.has(a.id));
|
|
5942
|
-
const afterBuilt = specsFromMeasure(afterRun.file);
|
|
5969
|
+
const afterBuilt = specsFromMeasure(afterRun.file, { only: component });
|
|
5943
5970
|
const afterSpec = afterBuilt.specs.components.find((c) => lc3(c.name) === lc3(component));
|
|
5944
5971
|
const hintsA = afterSpec ? afterBuilt.hints[afterSpec.name] : void 0;
|
|
5945
|
-
const hintsB = before.spec ? specsFromMeasure(before.measured).hints[before.spec.name] : void 0;
|
|
5972
|
+
const hintsB = before.spec ? specsFromMeasure(before.measured, { only: before.spec.name }).hints[before.spec.name] : void 0;
|
|
5946
5973
|
const resolve4 = measuredResolver(before.measured);
|
|
5947
5974
|
const built = byName(input.run?.built, component);
|
|
5948
5975
|
const stories = storiesOfComponent(before.measured, component);
|
|
@@ -5979,7 +6006,7 @@ function tryFigmaFixes(input) {
|
|
|
5979
6006
|
verdict,
|
|
5980
6007
|
referee: "values",
|
|
5981
6008
|
variants,
|
|
5982
|
-
summary: trySummary(component, before.gap, all, regressions, verdict, notFound, proposed),
|
|
6009
|
+
summary: trySummary(component, before.gap, all, regressions, verdict, notFound, proposed, variants.filter((v) => v.verdict === "gone").length),
|
|
5983
6010
|
detail: tryDetail(variants, applied),
|
|
5984
6011
|
file
|
|
5985
6012
|
};
|
|
@@ -6113,17 +6140,22 @@ function sizeShift(field, specB, vb, specA, va, built, resolve4) {
|
|
|
6113
6140
|
walk2(root);
|
|
6114
6141
|
return unknown ? void 0 : round2(shift);
|
|
6115
6142
|
}
|
|
6116
|
-
function trySummary(component, gap, diffs, regressions, verdict, notFound, proposed) {
|
|
6143
|
+
function trySummary(component, gap, diffs, regressions, verdict, notFound, proposed, gone) {
|
|
6117
6144
|
if (!gap.variants.length) return `${component} has no differing variant in the gap case: nothing for these fixes to move (the values referee needs a Check fidelity report or a difference in the measure).`;
|
|
6145
|
+
const absent = notFound.length ? ` ${notFound.map((a) => a.id).join(", ")} found nothing in the measure.` : "";
|
|
6146
|
+
if (!diffs.length && gone) {
|
|
6147
|
+
const n = gap.variants.length;
|
|
6148
|
+
const which = gone < n ? `${gone} of the ${n} differing variants are not` : n === 1 ? "the differing variant is not" : `none of the ${n} differing variants is`;
|
|
6149
|
+
return `The values referee compared nothing: ${which} in the rebuilt set.${absent} Pull this branch in the DS Bridge Figma plugin and run Check fidelity to see what the fixes do.`;
|
|
6150
|
+
}
|
|
6118
6151
|
const resolved = diffs.filter((d) => d.result === "resolved").length;
|
|
6119
6152
|
const closer = diffs.filter((d) => d.result === "closer").length;
|
|
6120
6153
|
const worse = diffs.filter((d) => d.result === "worse").length;
|
|
6121
|
-
const bits = [`${resolved} of ${diffs.length} difference${diffs.length === 1 ? "" : "s"} resolved`, ...closer ? [`${closer} closer`] : [], ...worse ? [`${worse} worse`] : [], ...regressions ? [`${regressions} new`] : []];
|
|
6154
|
+
const bits = [`${resolved} of ${diffs.length} difference${diffs.length === 1 ? "" : "s"} resolved`, ...closer ? [`${closer} closer`] : [], ...worse ? [`${worse} worse`] : [], ...regressions ? [`${regressions} new`] : [], ...gone ? [`${gone} not in the rebuilt set`] : []];
|
|
6122
6155
|
const word = verdict === "closer" ? "Closer to the code" : verdict === "worse" ? "Further from the code" : "No closer to the code";
|
|
6123
6156
|
const statusOnly = proposed.length > 0 && proposed.every((f) => f.kind === "status" || f.kind === "axis" && f.action === "map");
|
|
6124
|
-
const missing = notFound.length ? ` ${notFound.map((a) => a.id).join(", ")} found nothing in the measure.` : "";
|
|
6125
6157
|
const note = statusOnly && verdict === "same" ? " These fixes change how the set is named or labelled, not what it draws." : "";
|
|
6126
|
-
return `${word} on the measured values: ${bits.join(", ")}.${
|
|
6158
|
+
return `${word} on the measured values: ${bits.join(", ")}.${absent}${note} The pixel check runs in the plugin after the next pull.`;
|
|
6127
6159
|
}
|
|
6128
6160
|
function tryDetail(variants, applied) {
|
|
6129
6161
|
const fmt = (v) => v === null ? "none" : v === void 0 ? "unknown" : typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -6148,6 +6180,32 @@ function fixWords(f) {
|
|
|
6148
6180
|
function touchesLayout(f) {
|
|
6149
6181
|
return f.kind === "part" && (f.width !== void 0 || f.height !== void 0 || f.direction !== void 0 || f.align !== void 0 || f.overlay !== void 0);
|
|
6150
6182
|
}
|
|
6183
|
+
var VARIANTS_INLINE = 8;
|
|
6184
|
+
var valueWords = (v) => v === null ? "none" : v === void 0 ? "unknown" : typeof v === "string" ? v : JSON.stringify(v);
|
|
6185
|
+
function triedHighlights(variants) {
|
|
6186
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6187
|
+
for (const v of variants) {
|
|
6188
|
+
for (const d of v.differences) {
|
|
6189
|
+
const key = [d.part, d.property, valueWords(d.before), valueWords(d.expected), valueWords(d.after), d.result].join("|");
|
|
6190
|
+
const seen = groups.get(key);
|
|
6191
|
+
if (seen) seen.n += 1;
|
|
6192
|
+
else groups.set(key, { n: 1, d });
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
return [...groups.values()].map(
|
|
6196
|
+
({ n, d }) => `- ${d.part} ${d.property}, ${n === 1 ? "1 variant" : `${n} variants`}: ${valueWords(d.before)} in Figma against ${valueWords(d.expected)} in code, now ${valueWords(d.after)}: ${d.result}`
|
|
6197
|
+
);
|
|
6198
|
+
}
|
|
6199
|
+
function triedEvidence(variants) {
|
|
6200
|
+
const lines2 = variants.flatMap((v) => [
|
|
6201
|
+
`- ${v.key}: ${v.verdict === "gone" ? "not in the rebuilt set" : v.verdict}`,
|
|
6202
|
+
...v.differences.map((d) => ` - ${d.text}: ${d.result}${d.deltaBefore !== void 0 ? ` (${d.deltaBefore} px, now ${d.deltaAfter ?? "?"} px${d.estimated ? ", estimated" : ""})` : ""}`),
|
|
6203
|
+
...v.regressions.map((r) => ` - new difference: ${r.text}`)
|
|
6204
|
+
]);
|
|
6205
|
+
if (variants.length <= VARIANTS_INLINE) return [...lines2, ""];
|
|
6206
|
+
const highlights = triedHighlights(variants);
|
|
6207
|
+
return [...highlights.length ? [...highlights, ""] : [], "<details>", `<summary>Variant by variant (${variants.length})</summary>`, "", ...lines2, "", "</details>", ""];
|
|
6208
|
+
}
|
|
6151
6209
|
function figmaFixesPrBody(input) {
|
|
6152
6210
|
const { tried } = input;
|
|
6153
6211
|
const where2 = (f) => {
|
|
@@ -6174,13 +6232,9 @@ function figmaFixesPrBody(input) {
|
|
|
6174
6232
|
"",
|
|
6175
6233
|
tried.summary,
|
|
6176
6234
|
"",
|
|
6177
|
-
|
|
6178
|
-
`- ${v.key}: ${v.verdict}`,
|
|
6179
|
-
...v.differences.map((d) => ` - ${d.text}: ${d.result}${d.deltaBefore !== void 0 ? ` (${d.deltaBefore} px, now ${d.deltaAfter ?? "?"} px${d.estimated ? ", estimated" : ""})` : ""}`),
|
|
6180
|
-
...v.regressions.map((r) => ` - new difference: ${r.text}`)
|
|
6181
|
-
]),
|
|
6182
|
-
"",
|
|
6235
|
+
// The caveat is the reader's key to the section, so it comes before the evidence, not after it.
|
|
6183
6236
|
...tried.verdict === "same" && input.fixes.some(touchesLayout) ? ["These fixes size, place or lay out parts. The values referee can't judge layout, so it says same: Check fidelity decides.", ""] : [],
|
|
6237
|
+
...triedEvidence(tried.variants),
|
|
6184
6238
|
"The values referee compares the rebuilt set with what the code renders. The story shot is the final referee: pull this branch in the DS Bridge Figma plugin and run Check fidelity before approving. A fix stays only when the variant moved closer to its shot.",
|
|
6185
6239
|
"",
|
|
6186
6240
|
`Drafted by Claude through ${input.tool}. figma-fixes.json only: no component, token or plugin change.`
|
|
@@ -6415,6 +6469,82 @@ function figmaPushStatus(x) {
|
|
|
6415
6469
|
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
6416
6470
|
}
|
|
6417
6471
|
|
|
6472
|
+
// ../core/src/figma-fix-run.ts
|
|
6473
|
+
var DEFAULT_MEASURED_SPECS_FILE = "measured-specs.json";
|
|
6474
|
+
var FIGMA_FIXES_LABEL = "figma-fixes";
|
|
6475
|
+
var slug = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "component";
|
|
6476
|
+
var figmaFixesBranchStem = (config, component) => `${config.governance.branchPrefix}figma-fixes-${slug(component)}`;
|
|
6477
|
+
function figmaFixesProposal(input) {
|
|
6478
|
+
const { config } = input;
|
|
6479
|
+
const tried = tryFigmaFixes(input);
|
|
6480
|
+
if (tried.status === "invalid") throw new Error(`Not proposed: ${tried.problems.join("; ")}.`);
|
|
6481
|
+
const summary2 = (input.summary ?? "").trim();
|
|
6482
|
+
if (summary2.length < 8) throw new Error("Not proposed: summary is one or two sentences a reviewer reads first (what the fixes do for the designer).");
|
|
6483
|
+
if (/—/.test(summary2)) throw new Error("Not proposed: the summary uses an em dash: use a colon or a comma.");
|
|
6484
|
+
const ids = new Set(input.proposed.map((f) => f.id));
|
|
6485
|
+
const proposed = tried.file.fixes.filter((f) => ids.has(f.id));
|
|
6486
|
+
const { gap } = gapCaseFor(input);
|
|
6487
|
+
const drawing = proposed.some((f) => f.kind === "part" || f.kind === "story" || f.kind === "axis" && f.action !== "map");
|
|
6488
|
+
const measurable = gap.variants.some((v) => v.differences.length);
|
|
6489
|
+
if (tried.verdict === "worse" || drawing && measurable && tried.verdict === "same" && !proposed.some(touchesLayout)) {
|
|
6490
|
+
return {
|
|
6491
|
+
status: "blocked",
|
|
6492
|
+
verdict: tried.verdict,
|
|
6493
|
+
why: `${tried.summary} Bridge does not open fixes that move ${gap.component} ${tried.verdict === "worse" ? "further from" : "no closer to"} the code.`,
|
|
6494
|
+
detail: tried.detail,
|
|
6495
|
+
component: gap.component
|
|
6496
|
+
};
|
|
6497
|
+
}
|
|
6498
|
+
const content = `${JSON.stringify(tried.file, null, 2)}
|
|
6499
|
+
`;
|
|
6500
|
+
const fileContents = [{ path: input.fixesPath, content }];
|
|
6501
|
+
const risk = classifyRisk({ files: [{ path: input.fixesPath, status: input.fixes ? "modified" : "added" }], config });
|
|
6502
|
+
const prBody = `${figmaFixesPrBody({ component: gap.component, summary: summary2, gap, tried, fixes: proposed, tool: input.tool })}
|
|
6503
|
+
|
|
6504
|
+
Risk: ${risk.label}.`;
|
|
6505
|
+
const title = `fix(figma): ${gap.component} drawn closer to its story`;
|
|
6506
|
+
const message = `${title}
|
|
6507
|
+
|
|
6508
|
+
${summary2}
|
|
6509
|
+
|
|
6510
|
+
Fixes: ${proposed.map((f) => f.id).join(", ")}. Drafted by Claude through ${input.tool}.`;
|
|
6511
|
+
const labels = [.../* @__PURE__ */ new Set([...config.governance.labels, `tier: ${risk.tier}`, `source: ${input.source}`, FIGMA_FIXES_LABEL, DRAFTED_BY_CLAUDE_LABEL])];
|
|
6512
|
+
const report = { component: gap.component, verdict: tried.verdict, summary: tried.summary, variants: tried.variants, fixes: proposed.map((f) => f.id), tier: risk.tier, prTitle: title };
|
|
6513
|
+
return { status: "ready", report, fixes: proposed, labels, prBody, fileContents, title, message, draft: risk.tier >= 2, base: baseBranchOf(config), branchStem: figmaFixesBranchStem(config, gap.component) };
|
|
6514
|
+
}
|
|
6515
|
+
|
|
6516
|
+
// ../core/src/measure-location.ts
|
|
6517
|
+
var MEASURE_BRANCH = REQUESTS_BRANCH;
|
|
6518
|
+
var MEASURE_WORKFLOW_FILE = ".github/workflows/bridge-measure.yml";
|
|
6519
|
+
var MEASURE_SETUP_ACTION = "Set up automatic measure";
|
|
6520
|
+
var MEASURE_MISSING_TEXT = `Bridge hasn't measured your components yet. A developer turns it on once: ${MEASURE_SETUP_ACTION}.`;
|
|
6521
|
+
function measureSetupSteps(repo) {
|
|
6522
|
+
return [
|
|
6523
|
+
`${MEASURE_SETUP_ACTION}${repo ? ` for ${repo}` : ""}, once:`,
|
|
6524
|
+
`- In Bridge Studio: Settings, For developers, ${MEASURE_SETUP_ACTION}. It opens a pull request that adds ${MEASURE_WORKFLOW_FILE}.`,
|
|
6525
|
+
`- Or in the repository: npx @bridge-ds/cli measure setup, then commit ${MEASURE_WORKFLOW_FILE}.`,
|
|
6526
|
+
`Once it is merged, the workflow measures the Storybook each time it changes and publishes ${DEFAULT_MEASURED_SPECS_FILE} to the ${MEASURE_BRANCH} branch.`
|
|
6527
|
+
];
|
|
6528
|
+
}
|
|
6529
|
+
async function findMeasure(files, path = DEFAULT_MEASURED_SPECS_FILE) {
|
|
6530
|
+
const local = await files.repo(path);
|
|
6531
|
+
if (local) return { text: local, path };
|
|
6532
|
+
if (!files.branch) return void 0;
|
|
6533
|
+
const remote = await files.branch(path, MEASURE_BRANCH).catch(() => void 0);
|
|
6534
|
+
return remote ? { text: remote, path, branch: MEASURE_BRANCH } : void 0;
|
|
6535
|
+
}
|
|
6536
|
+
var measureFromWords = (m) => m.branch ? `${m.path} on ${m.branch}` : m.path;
|
|
6537
|
+
var MeasureMissingError = class extends Error {
|
|
6538
|
+
code = "measure-missing";
|
|
6539
|
+
steps;
|
|
6540
|
+
constructor(repo, extra = []) {
|
|
6541
|
+
const steps = [...measureSetupSteps(repo), ...extra];
|
|
6542
|
+
super([MEASURE_MISSING_TEXT, "", ...steps].join("\n"));
|
|
6543
|
+
this.name = "MeasureMissingError";
|
|
6544
|
+
this.steps = steps;
|
|
6545
|
+
}
|
|
6546
|
+
};
|
|
6547
|
+
|
|
6418
6548
|
// ../core/src/review-gate.ts
|
|
6419
6549
|
function parseTier(labels, body) {
|
|
6420
6550
|
const found = [];
|
|
@@ -6589,7 +6719,8 @@ function branchVerdict(input) {
|
|
|
6589
6719
|
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
6590
6720
|
const gate = input.ci && approvals ? mergeGate({ tier: input.tier, ci: input.ci, approvals, draft, mergeable: input.mergeable, state, merged_at: input.merged_at, viewer: input.viewer, override, exploration: input.exploration, ship: input.ship }) : null;
|
|
6591
6721
|
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
6592
|
-
const
|
|
6722
|
+
const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
|
|
6723
|
+
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
|
|
6593
6724
|
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
6594
6725
|
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
6595
6726
|
}
|
|
@@ -6730,6 +6861,28 @@ function reviewersToAsk(x) {
|
|
|
6730
6861
|
return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
|
|
6731
6862
|
}
|
|
6732
6863
|
|
|
6864
|
+
// ../core/src/tier1-publish.ts
|
|
6865
|
+
var leadsPublishTier1On = (gov) => gov?.leadsPublishTier1 !== false;
|
|
6866
|
+
var TIER1_PUBLISH_HEADING = "**Bridge: published without review (Tier 1)**";
|
|
6867
|
+
var publishedWithoutReview = (name) => `Published by ${name.trim() || "a lead"} without review (Tier 1).`;
|
|
6868
|
+
function tier1PublishedBy(body) {
|
|
6869
|
+
if (!body || !body.trimStart().startsWith(TIER1_PUBLISH_HEADING)) return null;
|
|
6870
|
+
const m = /Published by (.+?) without review \(Tier 1\)\./.exec(body);
|
|
6871
|
+
return m ? m[1] : null;
|
|
6872
|
+
}
|
|
6873
|
+
function tier1PublishOf(reviews, headSha) {
|
|
6874
|
+
for (const r of [...reviews ?? []].reverse()) {
|
|
6875
|
+
if (r.state !== "APPROVED") continue;
|
|
6876
|
+
if (headSha && r.commit_id && r.commit_id !== headSha) continue;
|
|
6877
|
+
const by = tier1PublishedBy(r.body);
|
|
6878
|
+
if (by) return by;
|
|
6879
|
+
}
|
|
6880
|
+
return null;
|
|
6881
|
+
}
|
|
6882
|
+
function tier1PublishSentence(enabled) {
|
|
6883
|
+
return enabled ? "Tier 1 (docs, guidance, token descriptions, story and test fixes): team admins and the reviewers of that part publish it without review; Bridge merges it once the checks pass." : "Tier 1 needs one owner approval: leads publishing small changes without review is off.";
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6733
6886
|
// ../core/src/library-update.ts
|
|
6734
6887
|
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
6735
6888
|
var names = (files) => files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
@@ -7018,6 +7171,46 @@ function parseSuggestionAppliedComment(body) {
|
|
|
7018
7171
|
return null;
|
|
7019
7172
|
}
|
|
7020
7173
|
}
|
|
7174
|
+
var SUGGESTION_OVERRIDE_MARK = "bridge-suggestion-override";
|
|
7175
|
+
function composeSuggestionOverrideComment(r) {
|
|
7176
|
+
const audit = overrideComment(r.login, r.reason, r.missing).replace(/ Merged from Bridge\.$/, " Approved as owner from Bridge.");
|
|
7177
|
+
const record = { payloadSha: r.payloadSha, login: r.login.replace(/^@/, "").toLowerCase(), missing: r.missing };
|
|
7178
|
+
return [audit, "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_OVERRIDE_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
7179
|
+
}
|
|
7180
|
+
function parseSuggestionOverrideComment(body) {
|
|
7181
|
+
const text = (body ?? "").trim();
|
|
7182
|
+
if (!text.startsWith(OVERRIDE_HEADING)) return null;
|
|
7183
|
+
const m = new RegExp(`<!-- ${SUGGESTION_OVERRIDE_MARK} (\\{[^\\n]*?\\}) -->`).exec(text);
|
|
7184
|
+
if (!m) return null;
|
|
7185
|
+
try {
|
|
7186
|
+
const r = JSON.parse(m[1]);
|
|
7187
|
+
if (typeof r.payloadSha !== "string" || typeof r.login !== "string") return null;
|
|
7188
|
+
const why = /Owner override by @[\w-]+: ([\s\S]*?)\. Missing:/.exec(text);
|
|
7189
|
+
return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
|
|
7190
|
+
} catch {
|
|
7191
|
+
return null;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
function suggestionOverrideProblem(x) {
|
|
7195
|
+
const lists = approverLists(x.gov.owners, x.gov.rights ?? null, x.gov.teams);
|
|
7196
|
+
const holders = overrideHolders(x.gov.override, lists).map(norm2);
|
|
7197
|
+
if (!x.viewer || !holders.includes(norm2(x.viewer))) return "Only an owner the review rules allow to use the owner override can approve as owner.";
|
|
7198
|
+
if (norm2(x.viewer) !== norm2(x.author)) return "The owner override is for your own suggestion: approve this one the usual way.";
|
|
7199
|
+
const others = eligibleReviewers("approval", { owners: lists, author: x.author });
|
|
7200
|
+
if (others.open || others.logins.length) return "Someone else can review it: ask them instead.";
|
|
7201
|
+
return null;
|
|
7202
|
+
}
|
|
7203
|
+
function suggestionOverrideOf(comments, gov, opts) {
|
|
7204
|
+
if (!gov.override || !opts.author) return null;
|
|
7205
|
+
for (const c of [...comments].reverse()) {
|
|
7206
|
+
const r = parseSuggestionOverrideComment(c.body);
|
|
7207
|
+
if (!r || r.payloadSha !== opts.payloadSha || !c.author || norm2(c.author) !== norm2(r.login)) continue;
|
|
7208
|
+
if (suggestionOverrideProblem({ viewer: c.author, author: opts.author, gov })) continue;
|
|
7209
|
+
if (overrideReasonProblem(r.reason, gov.override)) continue;
|
|
7210
|
+
return r;
|
|
7211
|
+
}
|
|
7212
|
+
return null;
|
|
7213
|
+
}
|
|
7021
7214
|
var norm2 = (l) => l.trim().replace(/^@/, "").toLowerCase();
|
|
7022
7215
|
var COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
7023
7216
|
function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
@@ -7049,7 +7242,9 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
|
7049
7242
|
});
|
|
7050
7243
|
const summary2 = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
|
|
7051
7244
|
const stale = [...latestOld.keys()].filter((l) => !summary2.approvers.includes(l) && !summary2.changesRequested.includes(l));
|
|
7052
|
-
|
|
7245
|
+
const out = { ...summary2, stale, ignored: [...ignored].filter((l) => !summary2.approvers.includes(l)) };
|
|
7246
|
+
const override = summary2.changesRequested.length ? null : suggestionOverrideOf(comments, gov, { payloadSha: opts.payloadSha, author: opts.author });
|
|
7247
|
+
return override ? { ...out, design: true, tech: true, owner: true, override } : out;
|
|
7053
7248
|
}
|
|
7054
7249
|
function suggestionReviewState(tier, approvals) {
|
|
7055
7250
|
if (approvals.changesRequested.length) return "changes-requested";
|
|
@@ -8111,7 +8306,7 @@ async function planSuggestionApply(input) {
|
|
|
8111
8306
|
for (const p of new Set(targets)) if (await ws.isSymlink(p)) links.push(`"${p}" is a symbolic link (or inside one): Bridge never writes through links.`);
|
|
8112
8307
|
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
8113
8308
|
}
|
|
8114
|
-
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
8309
|
+
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth, override: config.governance.override ?? null };
|
|
8115
8310
|
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
|
|
8116
8311
|
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
8117
8312
|
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
@@ -8411,6 +8606,27 @@ async function reviewSuggestionOnGitHub(gh, n, input, gov) {
|
|
|
8411
8606
|
if (labelWarning) warnings.push(labelWarning);
|
|
8412
8607
|
return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
|
|
8413
8608
|
}
|
|
8609
|
+
async function overrideSuggestionOnGitHub(gh, n, input, gov) {
|
|
8610
|
+
if (!gov.override) throw new GitHubError(422, "The review rules allow no owner override.");
|
|
8611
|
+
const before = await getSuggestionOnGitHub(gh, n, gov);
|
|
8612
|
+
if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be approved: ${before.parsed.message}`);
|
|
8613
|
+
if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed.`);
|
|
8614
|
+
const sha = before.parsed.sha;
|
|
8615
|
+
if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and approve the new version.`);
|
|
8616
|
+
const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
|
|
8617
|
+
const problem = suggestionOverrideProblem({ viewer: by, author: before.issue.author, gov }) ?? overrideReasonProblem(input.reason, gov.override);
|
|
8618
|
+
if (problem) throw new GitHubError(422, problem);
|
|
8619
|
+
if (before.approvals?.changesRequested.length) throw new GitHubError(422, `Changes were asked for on #${n}: the owner override covers missing approvals only.`);
|
|
8620
|
+
const missing = pendingApprovals(before.parsed.payload.tier.tier, before.approvals ?? { approvers: [], changesRequested: [], design: false, tech: false }).map((r) => r === "design" ? "design approval" : r === "tech" ? "tech approval" : "approval");
|
|
8621
|
+
const posted = await gh.createIssueComment(n, composeSuggestionOverrideComment({ login: by, reason: input.reason, missing, payloadSha: sha }));
|
|
8622
|
+
const after = await getSuggestionOnGitHub(gh, n, gov);
|
|
8623
|
+
const counts = Boolean(after.approvals?.override);
|
|
8624
|
+
const warnings = [];
|
|
8625
|
+
if (!counts) warnings.push("The owner override is posted but does not count: check the owners and the override in bridge.config.json.");
|
|
8626
|
+
const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, after.status);
|
|
8627
|
+
if (labelWarning) warnings.push(labelWarning);
|
|
8628
|
+
return { commentUrl: posted.url, payloadSha: sha, status: after.status, counts, warnings };
|
|
8629
|
+
}
|
|
8414
8630
|
async function linkSuggestionPrOnGitHub(gh, n, link) {
|
|
8415
8631
|
if (!link.pr && !link.branch && !link.commit) throw new GitHubError(422, "Say where it is applied: a pull request number, a branch or a commit.");
|
|
8416
8632
|
const issue = await gh.getIssue(n);
|
|
@@ -12690,7 +12906,7 @@ function auditSnippet(set, code2, max = 3) {
|
|
|
12690
12906
|
}
|
|
12691
12907
|
|
|
12692
12908
|
// src/markdown.ts
|
|
12693
|
-
var
|
|
12909
|
+
var slug2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
12694
12910
|
function sections(md) {
|
|
12695
12911
|
const lines2 = md.split("\n");
|
|
12696
12912
|
const heads = [];
|
|
@@ -12702,7 +12918,7 @@ function sections(md) {
|
|
|
12702
12918
|
});
|
|
12703
12919
|
return heads.map((h, i) => {
|
|
12704
12920
|
const next = heads.slice(i + 1).find((x) => x.level <= h.level);
|
|
12705
|
-
return { level: h.level, title: h.title, slug:
|
|
12921
|
+
return { level: h.level, title: h.title, slug: slug2(h.title), body: lines2.slice(h.line, next ? next.line : lines2.length).join("\n").trim() };
|
|
12706
12922
|
});
|
|
12707
12923
|
}
|
|
12708
12924
|
function toc(md) {
|
|
@@ -12713,9 +12929,9 @@ function findSection(md, query) {
|
|
|
12713
12929
|
const all = sections(md);
|
|
12714
12930
|
const num2 = /^\d+$/.test(q) ? all.filter((s) => s.title.startsWith(`${q}.`)) : [];
|
|
12715
12931
|
if (num2.length) return num2;
|
|
12716
|
-
const exact = all.filter((s) => s.slug ===
|
|
12932
|
+
const exact = all.filter((s) => s.slug === slug2(q) || s.title.toLowerCase() === q);
|
|
12717
12933
|
if (exact.length) return exact;
|
|
12718
|
-
return all.filter((s) => s.title.toLowerCase().includes(q) || s.slug.includes(
|
|
12934
|
+
return all.filter((s) => s.title.toLowerCase().includes(q) || s.slug.includes(slug2(q)));
|
|
12719
12935
|
}
|
|
12720
12936
|
function bullets(body) {
|
|
12721
12937
|
return body.split("\n").filter((l) => /^\s*[-*]\s+/.test(l) || /^\s+\S/.test(l)).reduce((acc, l) => {
|
|
@@ -12804,7 +13020,7 @@ async function people(gh, config) {
|
|
|
12804
13020
|
const { owners, rights } = config.governance;
|
|
12805
13021
|
const teams = await new TeamDirectory(gh).resolve(owners).catch(() => void 0);
|
|
12806
13022
|
const lists = approverLists(owners, rights, teams ?? null);
|
|
12807
|
-
const gov = { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {} };
|
|
13023
|
+
const gov = { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {}, ...config.governance.override ? { override: config.governance.override } : {} };
|
|
12808
13024
|
return { lists, gov, ship: shipPolicy(owners, rights, teams), solo: config.governance.soloOwnerCanApproveBoth ? soloApprover(lists) : null };
|
|
12809
13025
|
}
|
|
12810
13026
|
async function viewerOf(ctx, gh) {
|
|
@@ -12881,14 +13097,15 @@ async function branchFacts(ctx, gh, n, viewer) {
|
|
|
12881
13097
|
const readTokens = () => tokenRead ??= tokenChangesOf(gh, config, pr, files, compare?.mergeBase);
|
|
12882
13098
|
const figmaPush = pr.state === "open" ? await figmaPushFacts(gh, config, pr, files, compare?.mergeBase, readTokens).catch(() => null) : null;
|
|
12883
13099
|
const tokenValues = pr.state === "open" ? await tokenValuesFact(config, pr, files, readTokens).catch(() => false) : false;
|
|
12884
|
-
const
|
|
13100
|
+
const publishedBy = tier1PublishOf(reviews.map((r) => ({ state: r.state, body: r.body, commit_id: r.commitId ?? null })), pr.headSha);
|
|
13101
|
+
const verdict = branchVerdict({ tier, ci, approvals: own, suggestion, owners: who.lists, override: config.governance.override ?? DEFAULT_OVERRIDE, mergeable: pr.mergeable, ...pullLike, viewer, exploration, ship: who.ship, figmaPush, tokenValues, autoMerge: pr.autoMerge, tier1Published: Boolean(publishedBy), lookAgain });
|
|
12885
13102
|
const approvals = verdict.approvals;
|
|
12886
13103
|
const gate = verdict.gate;
|
|
12887
13104
|
const status = verdict.status;
|
|
12888
13105
|
const override = verdict.override ?? DEFAULT_OVERRIDE;
|
|
12889
13106
|
const fresh = freshness({ compare: compare ? { behind_by: compare.behindBy, merge_base_commit: compare.mergeBase ? { sha: compare.mergeBase } : null } : null, mergeable: pr.mergeable, open: pr.state === "open" });
|
|
12890
13107
|
const product = productOfBranch(productsOf(config), { files: files.map((f) => f.filename), labels: pr.labels });
|
|
12891
|
-
return { config, pr, files, ci, approvals, tier, gate, status, override, suggestion, fresh, compare, exploration, product, lookAgain, lifecycle: pullState(pullLike), write: repo ? hasWriteAccess(repo.permissions) : null, repo, solo: who.solo, lists: who.lists, figmaPush, tokenValues, autoShip: verdict.autoShip, statusLabel: verdict.label };
|
|
13108
|
+
return { config, pr, files, ci, approvals, tier, gate, status, override, suggestion, fresh, compare, exploration, product, lookAgain, lifecycle: pullState(pullLike), write: repo ? hasWriteAccess(repo.permissions) : null, repo, solo: who.solo, lists: who.lists, figmaPush, tokenValues, autoShip: verdict.autoShip, statusLabel: verdict.label, publishedBy };
|
|
12892
13109
|
}
|
|
12893
13110
|
var STATUS_WORDS = REVIEW_STATUS_LABEL;
|
|
12894
13111
|
async function branchStatus(ctx, a) {
|
|
@@ -12914,6 +13131,7 @@ async function branchStatus(ctx, a) {
|
|
|
12914
13131
|
blockedBy: f.gate.reasons,
|
|
12915
13132
|
...viewer ? { you: viewer, youCanShip: f.gate.canShip, ownerOverride: f.gate.overridable } : { you: null, note: "Without a GitHub token Bridge cannot tell whether you may merge." }
|
|
12916
13133
|
},
|
|
13134
|
+
...f.publishedBy ? { published: publishedWithoutReview(f.publishedBy) } : {},
|
|
12917
13135
|
...f.figmaPush ? { fromFigma: { scope: f.figmaPush.scope, shipsByItself: f.autoShip, said: figmaSaid(f.figmaPush, f.autoShip) } } : {},
|
|
12918
13136
|
next: nextStep(ctx, f, viewer)
|
|
12919
13137
|
};
|
|
@@ -13477,7 +13695,8 @@ __export(suggestions_exports, {
|
|
|
13477
13695
|
reviewSuggestionOp: () => reviewSuggestionOp,
|
|
13478
13696
|
suggestionBase: () => suggestionBase,
|
|
13479
13697
|
suggestionGovernance: () => suggestionGovernance,
|
|
13480
|
-
suggestionNumber: () => suggestionNumber
|
|
13698
|
+
suggestionNumber: () => suggestionNumber,
|
|
13699
|
+
suggestionOverrideLine: () => suggestionOverrideLine
|
|
13481
13700
|
});
|
|
13482
13701
|
var SUGGEST_RIGHTS = "Issues: read and write (Contents: read is enough)";
|
|
13483
13702
|
async function changesFor(ctx, explicit) {
|
|
@@ -13562,7 +13781,7 @@ async function suggestionGovernance(ctx, gh) {
|
|
|
13562
13781
|
const { owners, rights } = config.governance;
|
|
13563
13782
|
const hasTeams = [...owners.design, ...owners.tech].some((o) => o.includes("/"));
|
|
13564
13783
|
const teams = hasTeams ? await new TeamDirectory(gh).resolve(owners).catch(() => void 0) : void 0;
|
|
13565
|
-
return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {} } };
|
|
13784
|
+
return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {}, ...config.governance.override ? { override: config.governance.override } : {} } };
|
|
13566
13785
|
}
|
|
13567
13786
|
var OPEN_STATUSES = ["needs-review", "changes-requested", "approved", "applying", "outdated"];
|
|
13568
13787
|
var REVIEW_STATUSES = ["needs-review", "changes-requested", "approved"];
|
|
@@ -13662,7 +13881,8 @@ async function getSuggestionOp(ctx, a) {
|
|
|
13662
13881
|
changesRequestedBy: approvals.changesRequested,
|
|
13663
13882
|
...approvals.stale.length ? { staleReviews: approvals.stale } : {},
|
|
13664
13883
|
...approvals.ignored.length ? { notCounted: approvals.ignored } : {},
|
|
13665
|
-
...solo && p.tier.tier > 1 ? { soloOwner: soloOwnerSentence(solo) } : {}
|
|
13884
|
+
...solo && p.tier.tier > 1 ? { soloOwner: soloOwnerSentence(solo) } : {},
|
|
13885
|
+
...approvals.override ? { ownerOverride: suggestionOverrideLine(approvals.override) } : {}
|
|
13666
13886
|
},
|
|
13667
13887
|
applied: d.applied,
|
|
13668
13888
|
...stale?.outdated ? { outdated: { mainSha: stale.mainSha, conflicts: stale.conflicts.map((c) => ({ path: c.path, detail: c.detail })) } } : {},
|
|
@@ -13670,6 +13890,7 @@ async function getSuggestionOp(ctx, a) {
|
|
|
13670
13890
|
next: d.status === "approved" ? `Approved. Apply it: ${say(ctx, `bridge_apply_suggestion { number: ${n}, mode: "dry-run" } first, then mode "branch" (or \`bridge apply ${n}\`)`, `\`bridge apply ${n} --dry-run\` first, then \`bridge apply ${n}\``)}. The pull request must say "Closes #${n}".` : d.status === "needs-review" ? `Waiting for ${missing.map(roleText).join(" and ") || "a review"} from the owners in ${CONFIG_FILE}: ${say(ctx, `bridge_review_suggestion { number: ${n}, event: "approve" }`, `\`bridge review ${n} --approve\``)}.` : d.status === "changes-requested" ? "Changes requested: the author sends a new version (approvals then start again)." : d.status === "applying" ? `Being applied${d.linkedPrs.length ? ` in ${d.linkedPrs.map((x) => `#${x.number}`).join(", ")}` : ""}. It closes when the pull request merges.` : d.status === "outdated" ? `Outdated: ${stale?.conflicts[0] ? stale.conflicts[0].detail.replace(/\.$/, "") : "main changed what it changes since it was suggested"}. Ask the author to send it again on today's code.` : `${SUGGESTION_STATUS_LABEL[d.status]}: nothing to do.`
|
|
13671
13891
|
};
|
|
13672
13892
|
}
|
|
13893
|
+
var suggestionOverrideLine = (o) => `Approved with the owner override by @${o.login}${o.missing.length ? ` without ${o.missing.join(" and ")}` : ""}. Reason: ${o.reason}.`;
|
|
13673
13894
|
async function reviewSuggestionOp(ctx, a) {
|
|
13674
13895
|
const n = suggestionNumber(a.number);
|
|
13675
13896
|
const gh = await writer(ctx, "Reviewing a suggestion", "Issues: read and write");
|
|
@@ -13684,6 +13905,22 @@ async function reviewSuggestionOp(ctx, a) {
|
|
|
13684
13905
|
const r2 = await declineSuggestionOnGitHub(gh, n, a.message.trim(), viewer);
|
|
13685
13906
|
return { number: n, status: "declined", said: SUGGESTION_STATUS_LABEL.declined, ...r2.warnings.length ? { warnings: r2.warnings } : {}, next: "Closed as not planned, with your reason. Reopen it on GitHub to look at it again." };
|
|
13686
13907
|
}
|
|
13908
|
+
if (a.event === "override") {
|
|
13909
|
+
if (!a.message?.trim()) throw new Error("Say why it goes without the missing approvals, in a sentence: the reason is recorded on the suggestion and in History.");
|
|
13910
|
+
const payload = a.payloadSha && /^[0-9a-f]{64}$/.test(a.payloadSha) ? a.payloadSha : void 0;
|
|
13911
|
+
const r2 = await overrideSuggestionOnGitHub(gh, n, { reason: a.message, payloadSha: payload, ...viewer ? { by: viewer } : {} }, gov);
|
|
13912
|
+
return {
|
|
13913
|
+
number: n,
|
|
13914
|
+
event: a.event,
|
|
13915
|
+
counts: r2.counts,
|
|
13916
|
+
version: r2.payloadSha.slice(0, 7),
|
|
13917
|
+
status: r2.status,
|
|
13918
|
+
said: SUGGESTION_STATUS_LABEL[r2.status],
|
|
13919
|
+
comment: r2.commentUrl,
|
|
13920
|
+
...r2.warnings.length ? { warnings: r2.warnings } : {},
|
|
13921
|
+
next: r2.status === "approved" ? `Approved as owner: a developer applies it with \`bridge apply ${n}\`${say(ctx, " or bridge_apply_suggestion", "")}.` : "Waiting for the other approvals."
|
|
13922
|
+
};
|
|
13923
|
+
}
|
|
13687
13924
|
if (a.event !== "comment" && viewer) {
|
|
13688
13925
|
const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams ?? void 0);
|
|
13689
13926
|
const owners = [...lists.design, ...lists.tech].map((l) => l.toLowerCase().replace(/^@/, ""));
|
|
@@ -14482,6 +14719,7 @@ async function reviewRoles(gh, config, approvers) {
|
|
|
14482
14719
|
tech: role("tech"),
|
|
14483
14720
|
...solo ? { soloOwner: soloOwnerSentence(solo) } : {},
|
|
14484
14721
|
ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can ship.",
|
|
14722
|
+
tier1Publish: tier1PublishSentence(leadsPublishTier1On(config.governance)),
|
|
14485
14723
|
...unreadable.length ? { unreadableTeams: unreadable.map((t) => t.message) } : {}
|
|
14486
14724
|
};
|
|
14487
14725
|
}
|
|
@@ -14522,7 +14760,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
14522
14760
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
14523
14761
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
14524
14762
|
const out = a.out ? resolve3(ctx.root, a.out) : join5(ctx.root, "visual-impact");
|
|
14525
|
-
const runImpact = run2 ?? (await import("./src-
|
|
14763
|
+
const runImpact = run2 ?? (await import("./src-ATD4DJF3.js")).runImpact;
|
|
14526
14764
|
const r = await runImpact({
|
|
14527
14765
|
root: ctx.root,
|
|
14528
14766
|
base: a.base ?? "main",
|
|
@@ -15438,6 +15676,8 @@ async function getSettings(ctx) {
|
|
|
15438
15676
|
techApproval: entries.tech.map((e) => entryLabel(e, teams)),
|
|
15439
15677
|
ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can merge.",
|
|
15440
15678
|
...solo ? { soloOwner: soloOwnerSentence(solo) } : {},
|
|
15679
|
+
// Tier 1 publish by leads (docs/GOVERNANCE.md): admins and the matching reviewer publish without review.
|
|
15680
|
+
tier1Publish: tier1PublishSentence(leadsPublishTier1On(config.governance)),
|
|
15441
15681
|
...unreadable.length ? { teamsNotRead: unreadable.map((t) => t.message) } : {}
|
|
15442
15682
|
},
|
|
15443
15683
|
// Owners only (core overrideHolders): the same list the gate reads, and Studio Settings shows.
|
|
@@ -15727,25 +15967,34 @@ async function pushFigmaComponentOp(ctx, a) {
|
|
|
15727
15967
|
// src/figma-fixes.ts
|
|
15728
15968
|
var figma_fixes_exports = {};
|
|
15729
15969
|
__export(figma_fixes_exports, {
|
|
15730
|
-
FIGMA_FIXES_LABEL: () =>
|
|
15970
|
+
FIGMA_FIXES_LABEL: () => FIGMA_FIXES_LABEL2,
|
|
15731
15971
|
MEASURED_SPECS_FILE: () => MEASURED_SPECS_FILE,
|
|
15732
15972
|
figmaGapsOp: () => figmaGapsOp,
|
|
15733
15973
|
getGapCaseOp: () => getGapCaseOp,
|
|
15974
|
+
loadFixFiles: () => loadFixFiles,
|
|
15734
15975
|
proposeFigmaFixesOp: () => proposeFigmaFixesOp,
|
|
15735
15976
|
tryFigmaFixOp: () => tryFigmaFixOp
|
|
15736
15977
|
});
|
|
15737
|
-
var MEASURED_SPECS_FILE =
|
|
15738
|
-
var
|
|
15978
|
+
var MEASURED_SPECS_FILE = DEFAULT_MEASURED_SPECS_FILE;
|
|
15979
|
+
var FIGMA_FIXES_LABEL2 = FIGMA_FIXES_LABEL;
|
|
15739
15980
|
async function loadFixFiles(ctx, a) {
|
|
15740
15981
|
const measuredPath = a.measured ?? MEASURED_SPECS_FILE;
|
|
15741
|
-
const
|
|
15742
|
-
|
|
15743
|
-
|
|
15982
|
+
const gh = await ctx.github().catch(() => null);
|
|
15983
|
+
const found = await findMeasure(
|
|
15984
|
+
{
|
|
15985
|
+
repo: (path) => ctx.source.read(path),
|
|
15986
|
+
// getRawText: a measure of every variant is over the 1 MB the contents API inlines.
|
|
15987
|
+
...gh ? { branch: (path, branch) => gh.getRawText(path, branch) } : {}
|
|
15988
|
+
},
|
|
15989
|
+
measuredPath
|
|
15990
|
+
);
|
|
15991
|
+
if (!found) throw new MeasureMissingError(gh ? `${gh.owner}/${gh.name}` : void 0, [`Until then, measure by hand: bridge specs --measure --storybook <url> --variants --shots, then commit ${measuredPath}.`]);
|
|
15992
|
+
const measuredFrom = measureFromWords(found);
|
|
15744
15993
|
let measured;
|
|
15745
15994
|
try {
|
|
15746
|
-
measured = parseMeasuredSpecsFile(text);
|
|
15995
|
+
measured = parseMeasuredSpecsFile(found.text);
|
|
15747
15996
|
} catch (e) {
|
|
15748
|
-
throw new Error(`${
|
|
15997
|
+
throw new Error(`${measuredFrom}: ${e.message}. Measure again with bridge specs --measure.`);
|
|
15749
15998
|
}
|
|
15750
15999
|
const fixesPath = a.fixesFile ?? DEFAULT_FIGMA_FIXES_FILE;
|
|
15751
16000
|
const fixesText = await ctx.source.read(fixesPath);
|
|
@@ -15771,7 +16020,6 @@ async function loadFixFiles(ctx, a) {
|
|
|
15771
16020
|
runFrom = path;
|
|
15772
16021
|
} else if (typeof a.fidelity === "string") throw new Error(`No fidelity report at ${path}.`);
|
|
15773
16022
|
else {
|
|
15774
|
-
const gh = await ctx.github().catch(() => null);
|
|
15775
16023
|
const remote = gh ? await gh.getFileText(DEFAULT_FIDELITY_RUN_FILE, REQUESTS_BRANCH).catch(() => void 0) : void 0;
|
|
15776
16024
|
if (remote) {
|
|
15777
16025
|
run2 = parseFidelityRun(remote);
|
|
@@ -15779,18 +16027,24 @@ async function loadFixFiles(ctx, a) {
|
|
|
15779
16027
|
}
|
|
15780
16028
|
}
|
|
15781
16029
|
}
|
|
15782
|
-
return { measured, measuredPath, fixes, fixesPath, run: run2, runFrom };
|
|
16030
|
+
return { measured, measuredPath, measuredFrom, fixes, fixesPath, run: run2, runFrom };
|
|
15783
16031
|
}
|
|
15784
16032
|
var NO_RUN = `No Check fidelity report found (${DEFAULT_FIDELITY_RUN_FILE} in the repo or on ${REQUESTS_BRANCH}): the list comes from the measure's own check. In the DS Bridge Figma plugin, pull, run Check fidelity, then Fix with Claude (or Copy the report and pass it).`;
|
|
15785
16033
|
async function figmaGapsOp(ctx, a = {}) {
|
|
15786
|
-
|
|
16034
|
+
let files;
|
|
16035
|
+
try {
|
|
16036
|
+
files = await loadFixFiles(ctx, a);
|
|
16037
|
+
} catch (e) {
|
|
16038
|
+
if (e instanceof MeasureMissingError) return { status: "no-measure", say: MEASURE_MISSING_TEXT, steps: e.steps, gaps: [] };
|
|
16039
|
+
throw e;
|
|
16040
|
+
}
|
|
15787
16041
|
const fixed = files.fixes ? applyFixes(files.measured, files.fixes).file : files.measured;
|
|
15788
16042
|
const gaps = figmaGaps(fixed, files.run);
|
|
15789
16043
|
const differs = gaps.filter((g) => g.status === "differs").length;
|
|
15790
16044
|
return {
|
|
15791
16045
|
referee: files.run ? "shots" : "values",
|
|
15792
16046
|
...files.runFrom ? { report: files.runFrom, checkedAt: files.run.report.checkedAt || void 0 } : { note: NO_RUN },
|
|
15793
|
-
measured: files.
|
|
16047
|
+
measured: files.measuredFrom,
|
|
15794
16048
|
gaps,
|
|
15795
16049
|
next: differs ? say(ctx, `Fix one: bridge_get_gap_case { component: "${gaps[0].component}" }, then bridge_try_figma_fix (at most ${FIX_LOOP_MAX_ROUNDS} rounds), then bridge_propose_figma_fixes.`, `Fix one: \`bridge fix ${gaps[0].component} --with-claude\`, or \`bridge fix ${gaps[0].component} --case\` to read its gap case.`) : gaps.length ? "Nothing differs that a fix can move: the rest cannot be checked, each with its reason." : "Every checked component matches its story."
|
|
15796
16050
|
};
|
|
@@ -15806,7 +16060,7 @@ async function getGapCaseOp(ctx, a) {
|
|
|
15806
16060
|
const files = await loadFixFiles(ctx, a);
|
|
15807
16061
|
const { gap, spec } = gapCaseFor({ component: a.component, measured: files.measured, fixes: files.fixes, run: files.run });
|
|
15808
16062
|
if (!spec && !files.measured.stories.some((s) => lc4(s.title.split("/").at(-1)) === lc4(a.component)))
|
|
15809
|
-
throw new Error(`No measured story of ${a.component} in ${files.
|
|
16063
|
+
throw new Error(`No measured story of ${a.component} in ${files.measuredFrom}. Measure it with \`bridge specs --measure --variants --shots --only ${a.component}\`.`);
|
|
15810
16064
|
const used = roundsOf(ctx).get(lc4(a.component)) ?? 0;
|
|
15811
16065
|
return {
|
|
15812
16066
|
gap,
|
|
@@ -15842,49 +16096,25 @@ async function tryFigmaFixOp(ctx, a) {
|
|
|
15842
16096
|
next
|
|
15843
16097
|
};
|
|
15844
16098
|
}
|
|
15845
|
-
var slug2 = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "component";
|
|
15846
16099
|
async function proposeFigmaFixesOp(ctx, a) {
|
|
15847
16100
|
const config = await ctx.config().then((c) => c.config, () => ({ governance: DEFAULT_GOVERNANCE }));
|
|
15848
16101
|
const files = await loadFixFiles(ctx, a);
|
|
15849
|
-
const
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
if (summary2.length < 8) throw new Error("Not proposed: summary is one or two sentences a reviewer reads first (what the fixes do for the designer).");
|
|
15853
|
-
if (/\u2014/.test(summary2)) throw new Error("Not proposed: the summary uses an em dash: use a colon or a comma.");
|
|
15854
|
-
const ids = new Set(a.fixes.map((f) => f.id));
|
|
15855
|
-
const proposed = tried.file.fixes.filter((f) => ids.has(f.id));
|
|
15856
|
-
const { gap } = gapCaseFor({ component: a.component, measured: files.measured, fixes: files.fixes, run: files.run });
|
|
15857
|
-
const drawing = proposed.some((f) => f.kind === "part" || f.kind === "story" || f.kind === "axis" && f.action !== "map");
|
|
15858
|
-
const measurable = gap.variants.some((v) => v.differences.length);
|
|
15859
|
-
if (tried.verdict === "worse" || drawing && measurable && tried.verdict === "same" && !proposed.some(touchesLayout)) {
|
|
16102
|
+
const source = sourceOf(ctx, a.source);
|
|
16103
|
+
const p = figmaFixesProposal({ component: a.component, measured: files.measured, fixes: files.fixes, run: files.run, proposed: a.fixes, summary: a.summary, config, fixesPath: files.fixesPath, tool: toolTag(source), source });
|
|
16104
|
+
if (p.status === "blocked") {
|
|
15860
16105
|
return {
|
|
15861
16106
|
status: "blocked",
|
|
15862
|
-
verdict:
|
|
15863
|
-
why:
|
|
15864
|
-
detail:
|
|
16107
|
+
verdict: p.verdict,
|
|
16108
|
+
why: p.why,
|
|
16109
|
+
detail: p.detail,
|
|
15865
16110
|
hint: say(ctx, 'Try other fixes with bridge_try_figma_fix, or propose a status fix (kind "status", status "differs" or "cant-check", the reason) instead.', 'Try other fixes with `bridge fix <component> --try`, or propose a status fix (kind "status", status "differs" or "cant-check", the reason) instead.')
|
|
15866
16111
|
};
|
|
15867
16112
|
}
|
|
15868
|
-
const
|
|
15869
|
-
const content = `${JSON.stringify(tried.file, null, 2)}
|
|
15870
|
-
`;
|
|
15871
|
-
const fileContents = [{ path: files.fixesPath, content }];
|
|
15872
|
-
const risk = classifyRisk({ files: [{ path: files.fixesPath, status: files.fixes ? "modified" : "added" }], config });
|
|
15873
|
-
const prBody = `${figmaFixesPrBody({ component: gap.component, summary: summary2, gap, tried, fixes: proposed, tool: toolTag(source) })}
|
|
15874
|
-
|
|
15875
|
-
Risk: ${risk.label}.`;
|
|
15876
|
-
const title = `fix(figma): ${gap.component} drawn closer to its story`;
|
|
15877
|
-
const message = `${title}
|
|
15878
|
-
|
|
15879
|
-
${summary2}
|
|
15880
|
-
|
|
15881
|
-
Fixes: ${proposed.map((f) => f.id).join(", ")}. Drafted by Claude through ${toolTag(source)}.`;
|
|
15882
|
-
const labels = [.../* @__PURE__ */ new Set([...config.governance.labels, `tier: ${risk.tier}`, `source: ${source}`, FIGMA_FIXES_LABEL, DRAFTED_BY_CLAUDE_LABEL])];
|
|
15883
|
-
const report = { component: gap.component, verdict: tried.verdict, summary: tried.summary, variants: tried.variants, fixes: proposed.map((f) => f.id), tier: risk.tier, prTitle: title };
|
|
16113
|
+
const { report, labels, prBody, fileContents, title, message, draft } = p;
|
|
15884
16114
|
const after = "Pull this branch in the DS Bridge Figma plugin and run Check fidelity: the story shot decides. A person approves the pull request.";
|
|
15885
16115
|
if (a.dryRun) return { status: "dry-run", ...report, labels, prBody, fileContents };
|
|
15886
16116
|
const prefix = config.governance.branchPrefix;
|
|
15887
|
-
const stem2 =
|
|
16117
|
+
const stem2 = p.branchStem;
|
|
15888
16118
|
let branch = stem2;
|
|
15889
16119
|
if (ctx.mode === "local") {
|
|
15890
16120
|
if (!ctx.isGit) return { status: "preview", message: "Not a git repo: nothing written. Write fileContents yourself.", ...report, prBody, fileContents };
|
|
@@ -15908,14 +16138,25 @@ Fixes: ${proposed.map((f) => f.id).join(", ")}. Drafted by Claude through ${tool
|
|
|
15908
16138
|
const base = ctx.ref ?? baseBranchOf(config);
|
|
15909
16139
|
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = `${stem2}-${i}`;
|
|
15910
16140
|
const commit = await gh.commitToNewBranch({ base, branch, message, files: fileContents });
|
|
15911
|
-
const draft = risk.tier >= 2;
|
|
15912
16141
|
const pr = await gh.createPullRequest({ base, head: branch, title, body: prBody, draft });
|
|
15913
16142
|
await gh.addLabels(pr.number, labels);
|
|
15914
16143
|
return { status: "pr-opened", ...report, branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft, next: `${after} Opened through ${toolName(source)}.` };
|
|
15915
16144
|
}
|
|
15916
16145
|
|
|
15917
16146
|
// src/server.ts
|
|
15918
|
-
var VERSION = true ? "0.
|
|
16147
|
+
var VERSION = true ? "0.10.0" : "dev";
|
|
16148
|
+
var BRIDGE_SITE = "https://bridge.floriangouloubi.com";
|
|
16149
|
+
var SERVER_INFO = {
|
|
16150
|
+
name: "bridge",
|
|
16151
|
+
title: "Bridge",
|
|
16152
|
+
version: VERSION,
|
|
16153
|
+
websiteUrl: `${BRIDGE_SITE}/`,
|
|
16154
|
+
icons: [
|
|
16155
|
+
{ src: `${BRIDGE_SITE}/icon-192.png`, mimeType: "image/png", sizes: ["192x192"] },
|
|
16156
|
+
{ src: `${BRIDGE_SITE}/icon-512.png`, mimeType: "image/png", sizes: ["512x512"] },
|
|
16157
|
+
{ src: `${BRIDGE_SITE}/favicon.svg`, mimeType: "image/svg+xml", sizes: ["any"] }
|
|
16158
|
+
]
|
|
16159
|
+
};
|
|
15919
16160
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
15920
16161
|
var fail2 = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
15921
16162
|
var run = (fn) => async (a) => {
|
|
@@ -15938,9 +16179,10 @@ var EditArg = z.object({
|
|
|
15938
16179
|
});
|
|
15939
16180
|
var RO = { readOnlyHint: true, openWorldHint: false };
|
|
15940
16181
|
var ChangesArg = z.enum(["suggest", "direct"]).optional().describe('How this change reaches the code: "suggest" files a suggestion (a GitHub issue the owners approve and a developer applies, Issues write only), "direct" opens a branch and a pull request. Default: "changes" in bridge.config.json, else "suggest". Only set it when the person asked.');
|
|
15941
|
-
function createServer(ctx) {
|
|
16182
|
+
function createServer(ctx, opts = {}) {
|
|
16183
|
+
const excluded = new Set(opts.exclude ?? []);
|
|
15942
16184
|
const server = new McpServer(
|
|
15943
|
-
|
|
16185
|
+
SERVER_INFO,
|
|
15944
16186
|
{
|
|
15945
16187
|
instructions: [
|
|
15946
16188
|
"Bridge exposes a design system repo (DTCG tokens, components, docs) to you.",
|
|
@@ -15962,10 +16204,18 @@ function createServer(ctx) {
|
|
|
15962
16204
|
"Product branches and the library: when a designer explores a product feature with you (an exploration for a product), check it with bridge_missing_pieces (record: true) before they share it; for each piece the library lacks, offer bridge_open_library_branch, which opens the library side linked to their branch. bridge_product_branch says where each library branch stands; bridge_switch_to_published moves the product branch to the real version once the library ships.",
|
|
15963
16205
|
"Components from Figma: the DS Bridge plugin sends a component changed in Figma to a library branch. Token swaps go as exact token changes; new variants, properties and components go to a branch carrying the spec, waiting for Claude AI. When the person asks you to draft one, bridge_figma_changes lists them and bridge_get_figma_change gives the spec and the steps; write the code on that branch, push it, then bridge_mark_figma_change_drafted. Never claim a branch is drafted before its code is pushed.",
|
|
15964
16206
|
"Fix with Claude: when a component built in Figma differs from its story, you are the team's Claude and Bridge is the referee. bridge_figma_gaps lists them, bridge_get_gap_case gives one gap case and the fix schema, bridge_try_figma_fix checks fixes on the measured values (at most 3 rounds per component), bridge_propose_figma_fixes opens them for review; a component that will not come closer gets a status fix (Differs or Can't check, with the reason), never Matches. The plugin's Check fidelity (the story shots) decides after the next pull.",
|
|
15965
|
-
'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.'
|
|
16207
|
+
'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.',
|
|
16208
|
+
...excluded.has("bridge_merge_branch") ? ["On this server you never merge: bridge_merge_branch is not available here. When a branch is ready, say so; a person merges it from Bridge Studio or GitHub."] : []
|
|
15966
16209
|
].join(" ")
|
|
15967
16210
|
}
|
|
15968
16211
|
);
|
|
16212
|
+
const registered = /* @__PURE__ */ new Map();
|
|
16213
|
+
const register = server.registerTool.bind(server);
|
|
16214
|
+
server.registerTool = ((name, config, cb) => {
|
|
16215
|
+
const tool = register(name, config, cb);
|
|
16216
|
+
registered.set(name, tool);
|
|
16217
|
+
return tool;
|
|
16218
|
+
});
|
|
15969
16219
|
server.registerTool(
|
|
15970
16220
|
"bridge_get_config",
|
|
15971
16221
|
{ title: "Get Bridge config", description: "Where the design system lives and what Bridge may do: source (local path or GitHub repo), write access, token tiers, modes (brand/theme/density...) and the full bridge.config.json. Call first in a new repo.", inputSchema: {}, annotations: RO },
|
|
@@ -16580,7 +16830,7 @@ function createServer(ctx) {
|
|
|
16580
16830
|
run((a) => pushFigmaComponentOp(ctx, { diff: a.diff, dryRun: a.dryRun, source: "mcp", changes: a.changes }))
|
|
16581
16831
|
);
|
|
16582
16832
|
const FixFiles = {
|
|
16583
|
-
measured: z.string().optional().describe("measured-specs.json path
|
|
16833
|
+
measured: z.string().optional().describe("measured-specs.json path (default measured-specs.json): read in the repo, else on the bridge-requests branch where the automatic measure (bridge-measure workflow) publishes it."),
|
|
16584
16834
|
fidelity: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe(`The Check fidelity run: a path in the repo, or the report itself (pasted from the plugin's "Copy the report"). Default: figma-fidelity.json in the repo, else on the bridge-requests branch where the plugin's "Fix with Claude" saves it.`),
|
|
16585
16835
|
fixesFile: z.string().optional().describe("figma-fixes.json path in the repo (default figma-fixes.json).")
|
|
16586
16836
|
};
|
|
@@ -16880,10 +17130,10 @@ function createServer(ctx) {
|
|
|
16880
17130
|
"bridge_review_suggestion",
|
|
16881
17131
|
{
|
|
16882
17132
|
title: "Review a suggestion",
|
|
16883
|
-
description:
|
|
17133
|
+
description: 'Approve, request changes on, comment on or decline a suggestion, as the person whose GitHub token the server has. Approvals and requested changes count only from the owners in bridge.config.json (read from the default branch) and only on the version reviewed: pass payloadSha from bridge_get_suggestion so a newer version is never approved by mistake. Request changes and decline need a message. Decline closes the issue as not planned. "override": the owner override (docs/GOVERNANCE.md): an owner governance.override allows approves their own suggestion as owner when nobody else can review it, with the reason in message (at least the minimum length); it is recorded on the suggestion. Needs GITHUB_TOKEN (Issues: read and write). Only when the person asked, with the decision they asked for.',
|
|
16884
17134
|
inputSchema: {
|
|
16885
17135
|
number: SuggestionNumber,
|
|
16886
|
-
event: z.enum(["approve", "request-changes", "comment", "decline"]),
|
|
17136
|
+
event: z.enum(["approve", "request-changes", "comment", "decline", "override"]),
|
|
16887
17137
|
message: z.string().optional().describe("What should change, why it is declined, or the comment. Plain sentences, no em dash."),
|
|
16888
17138
|
role: z.enum(["design", "tech", "approval"]).optional().describe("The role you approve for, shown in the comment. Informative: what counts is who you are in the owner lists."),
|
|
16889
17139
|
payloadSha: z.string().optional().describe("The version you reviewed (payloadSha or version from bridge_get_suggestion).")
|
|
@@ -17097,6 +17347,8 @@ function createServer(ctx) {
|
|
|
17097
17347
|
};
|
|
17098
17348
|
}
|
|
17099
17349
|
);
|
|
17350
|
+
for (const name of excluded) registered.get(name)?.remove();
|
|
17351
|
+
server.registerTool = register;
|
|
17100
17352
|
return server;
|
|
17101
17353
|
}
|
|
17102
17354
|
|
|
@@ -17114,6 +17366,17 @@ function tokenNote(source) {
|
|
|
17114
17366
|
return "No GitHub token: GITHUB_TOKEN and BRIDGE_GITHUB_TOKEN are not set and gh is not signed in. Bridge reads GitHub without a token (60 requests an hour) and cannot write there. Set GITHUB_TOKEN where the server runs, or run gh auth login, then restart the server.";
|
|
17115
17367
|
return void 0;
|
|
17116
17368
|
}
|
|
17369
|
+
async function handleHttp(ctx, req, parsedBody, opts = {}) {
|
|
17370
|
+
const server = createServer(ctx, opts);
|
|
17371
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
|
|
17372
|
+
await server.connect(transport);
|
|
17373
|
+
try {
|
|
17374
|
+
return await transport.handleRequest(req, parsedBody === void 0 ? {} : { parsedBody });
|
|
17375
|
+
} finally {
|
|
17376
|
+
await server.close().catch(() => {
|
|
17377
|
+
});
|
|
17378
|
+
}
|
|
17379
|
+
}
|
|
17117
17380
|
async function serveStdio(opts = {}) {
|
|
17118
17381
|
const { tokenSource, ...context } = opts;
|
|
17119
17382
|
const ctx = await BridgeContext.create(context);
|
|
@@ -17173,9 +17436,11 @@ export {
|
|
|
17173
17436
|
figma_exports,
|
|
17174
17437
|
figma_fixes_exports,
|
|
17175
17438
|
VERSION,
|
|
17439
|
+
SERVER_INFO,
|
|
17176
17440
|
createServer,
|
|
17177
17441
|
ghAuthToken,
|
|
17178
17442
|
tokenNote,
|
|
17443
|
+
handleHttp,
|
|
17179
17444
|
serveStdio,
|
|
17180
17445
|
optionsFromArgv
|
|
17181
17446
|
};
|