@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.
@@ -84,11 +84,12 @@ import {
84
84
  validateLayered,
85
85
  variantKeyOf,
86
86
  whoCanShipSentence
87
- } from "./chunk-ZVORAO5H.js";
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
- for (let p = mParts.get(id)?.parent; p; p = mParts.get(p)?.parent) out.push(p);
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
- for (let a = p.parent; a; a = sParts.get(a)?.parent) if (pairs.has(a)) return pairs.get(a);
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(", ")}.${missing}${note} The pixel check runs in the plugin after the next pull.`;
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
- ...tried.variants.flatMap((v) => [
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 autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed) && state === "open" && !input.merged_at && !input.exploration;
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
- return { ...summary2, stale, ignored: [...ignored].filter((l) => !summary2.approvers.includes(l)) };
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 slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
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: slug(h.title), body: lines2.slice(h.line, next ? next.line : lines2.length).join("\n").trim() };
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 === slug(q) || s.title.toLowerCase() === q);
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(slug(q)));
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 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, lookAgain });
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-RUS6SPYZ.js")).runImpact;
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: () => 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 = "measured-specs.json";
15738
- var FIGMA_FIXES_LABEL = "figma-fixes";
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 text = await ctx.source.read(measuredPath);
15742
- if (text === null || text === void 0)
15743
- throw new Error(`No ${measuredPath} in the repo: measure the stories first (\`bridge specs --measure --variants --shots --storybook <url>\`), then pull in the DS Bridge Figma plugin and run Check fidelity.`);
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(`${measuredPath}: ${e.message}. Measure again with bridge specs --measure.`);
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
- const files = await loadFixFiles(ctx, a);
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.measuredPath,
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.measuredPath}. Measure it with \`bridge specs --measure --variants --shots --only ${a.component}\`.`);
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 tried = tryFigmaFixes({ component: a.component, measured: files.measured, fixes: files.fixes, run: files.run, proposed: a.fixes });
15850
- if (tried.status === "invalid") throw new Error(`Not proposed: ${tried.problems.join("; ")}.`);
15851
- const summary2 = (a.summary ?? "").trim();
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: tried.verdict,
15863
- why: `${tried.summary} Bridge does not open fixes that move ${gap.component} ${tried.verdict === "worse" ? "further from" : "no closer to"} the code.`,
15864
- detail: tried.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 source = sourceOf(ctx, a.source);
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 = `${prefix}figma-fixes-${slug2(gap.component)}`;
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.9.0" : "dev";
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
- { name: "bridge", version: VERSION },
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 in the repo (default measured-specs.json)."),
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: "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. Needs GITHUB_TOKEN (Issues: read and write). Only when the person asked, with the decision they asked for.",
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
  };