@bridge-ds/mcp 0.7.2 → 0.8.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.
@@ -60,6 +60,7 @@ export declare function branchFacts(ctx: BridgeContext, gh: GitHubClient, n: num
60
60
  push?: boolean;
61
61
  };
62
62
  } | null;
63
+ solo: string | null;
63
64
  }>;
64
65
  /** One branch, the way Studio shows it: status, what blocks the merge and who acts, how it stands against main. */
65
66
  export declare function branchStatus(ctx: BridgeContext, a: {
@@ -80,6 +81,7 @@ export declare function branchStatus(ctx: BridgeContext, a: {
80
81
  };
81
82
  approvalNeeded: string;
82
83
  approvals: {
84
+ soloOwner?: string | undefined;
83
85
  approvedBy: string[];
84
86
  changesRequestedBy: string[];
85
87
  design: boolean;
@@ -424,6 +424,19 @@ function approverEntries(owners, rights) {
424
424
  if (!rights) return { design: [...owners.design], tech: [...owners.tech] };
425
425
  return { design: holdersOf("approveDesign", owners, rights), tech: holdersOf("approveTech", owners, rights) };
426
426
  }
427
+ function soloApprover(owners) {
428
+ const d = [...new Set(owners.design.map(norm2))];
429
+ const t = [...new Set(owners.tech.map(norm2))];
430
+ if (d.length !== 1 || t.length !== 1 || d[0] !== t[0] || !d[0] || d[0].includes("/")) return null;
431
+ return d[0];
432
+ }
433
+ function soloOwnerOf(gov, teams) {
434
+ if (!gov.soloOwnerCanApproveBoth) return null;
435
+ return soloApprover(approverLists(gov.owners, gov.rights ?? null, teams));
436
+ }
437
+ function soloOwnerSentence(login, you = false) {
438
+ return you ? "As the only owner, your approval covers design and code." : `As the only owner, @${norm2(login)} approves design and code in one review.`;
439
+ }
427
440
  function shipPolicy(owners, rights, teams) {
428
441
  if (!rights) return null;
429
442
  const entries = holdersOf("ship", owners, rights);
@@ -700,7 +713,8 @@ function defineConfig(input) {
700
713
  minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
701
714
  }
702
715
  } : {},
703
- ...g.rights ? { rights: normalizeRights(g.rights) } : {}
716
+ ...g.rights ? { rights: normalizeRights(g.rights) } : {},
717
+ ...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {}
704
718
  },
705
719
  products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
706
720
  changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
@@ -824,6 +838,7 @@ function loadConfig(source) {
824
838
  }
825
839
  }
826
840
  problems.push(...rightsProblems(gov.rights));
841
+ if (gov.soloOwnerCanApproveBoth !== void 0 && typeof gov.soloOwnerCanApproveBoth !== "boolean") problems.push('"governance.soloOwnerCanApproveBoth" must be true or false');
827
842
  }
828
843
  }
829
844
  problems.push(...visualImpactProblems(d.visualImpact));
@@ -2010,6 +2025,9 @@ export {
2010
2025
  DEFAULT_RIGHTS,
2011
2026
  approverLists,
2012
2027
  approverEntries,
2028
+ soloApprover,
2029
+ soloOwnerOf,
2030
+ soloOwnerSentence,
2013
2031
  shipPolicy,
2014
2032
  unreadableShipTeams,
2015
2033
  canShip,
@@ -68,6 +68,9 @@ import {
68
68
  selectTokenFiles,
69
69
  shipPolicy,
70
70
  showFile,
71
+ soloApprover,
72
+ soloOwnerOf,
73
+ soloOwnerSentence,
71
74
  threeWayDiff,
72
75
  toConfigJson,
73
76
  tokensPath,
@@ -76,7 +79,7 @@ import {
76
79
  validateLayered,
77
80
  variantKeyOf,
78
81
  whoCanShipSentence
79
- } from "./chunk-BZ3KXXMM.js";
82
+ } from "./chunk-BHODFN52.js";
80
83
 
81
84
  // src/lib.ts
82
85
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4246,7 +4249,7 @@ function summarizeCi(runs, statuses = []) {
4246
4249
  const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
4247
4250
  return { state, total, passed, failed, pending };
4248
4251
  }
4249
- function summarizeApprovals(reviews, owners, author) {
4252
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
4250
4253
  const latest = /* @__PURE__ */ new Map();
4251
4254
  const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
4252
4255
  for (const r of sorted) {
@@ -4264,7 +4267,8 @@ function summarizeApprovals(reviews, owners, author) {
4264
4267
  let tech;
4265
4268
  if (d.length || t.length) {
4266
4269
  design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
4267
- const soloBoth = (x) => a.length === 1 && d.includes(x);
4270
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
4271
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
4268
4272
  tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
4269
4273
  } else {
4270
4274
  design = a.length >= 1;
@@ -4735,7 +4739,7 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
4735
4739
  latestOld.delete(c.author);
4736
4740
  reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
4737
4741
  });
4738
- const summary2 = summarizeApprovals(reviews, lists, opts.author ?? void 0);
4742
+ const summary2 = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
4739
4743
  const stale = [...latestOld.keys()].filter((l) => !summary2.approvers.includes(l) && !summary2.changesRequested.includes(l));
4740
4744
  return { ...summary2, stale, ignored: [...ignored].filter((l) => !summary2.approvers.includes(l)) };
4741
4745
  }
@@ -6829,7 +6833,7 @@ async function planSuggestionApply(input) {
6829
6833
  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.`);
6830
6834
  if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
6831
6835
  }
6832
- const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null };
6836
+ const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
6833
6837
  const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
6834
6838
  const review = suggestionReviewState(payload.tier.tier, approvals);
6835
6839
  const missing = pendingApprovals(payload.tier.tier, approvals);
@@ -10200,7 +10204,7 @@ async function people(gh, config) {
10200
10204
  const { owners, rights } = config.governance;
10201
10205
  const teams = await new TeamDirectory(gh).resolve(owners).catch(() => void 0);
10202
10206
  const lists = expandOwners(approverEntries(owners, rights), teams);
10203
- return { lists, ship: shipPolicy(owners, rights, teams) };
10207
+ return { lists, ship: shipPolicy(owners, rights, teams), solo: config.governance.soloOwnerCanApproveBoth ? soloApprover(lists) : null };
10204
10208
  }
10205
10209
  async function viewerOf(ctx, gh) {
10206
10210
  if (!ctx.canWriteRemote) return null;
@@ -10226,7 +10230,8 @@ async function branchFacts(ctx, gh, n, viewer) {
10226
10230
  const approvals = summarizeApprovals(
10227
10231
  reviews.map((r) => ({ user: r.user ? { login: r.user } : null, state: r.state, submitted_at: r.submittedAt ?? null })),
10228
10232
  who.lists,
10229
- pr.author
10233
+ pr.author,
10234
+ { soloOwnerCanApproveBoth: Boolean(who.solo) }
10230
10235
  );
10231
10236
  const tier = effectiveTier(labels, pr.body, files);
10232
10237
  const merged = pr.state === "merged";
@@ -10236,7 +10241,7 @@ async function branchFacts(ctx, gh, n, viewer) {
10236
10241
  const status = reviewStatusKind({ ...pullLike, ci, approvals, tier, mergeable: pr.mergeable });
10237
10242
  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" });
10238
10243
  const product = productOfBranch(productsOf(config), { files: files.map((f) => f.filename), labels: pr.labels });
10239
- return { config, pr, files, ci, approvals, tier, gate, status, fresh, compare, exploration, product, lifecycle: pullState(pullLike), write: repo ? hasWriteAccess(repo.permissions) : null, repo };
10244
+ return { config, pr, files, ci, approvals, tier, gate, status, fresh, compare, exploration, product, lifecycle: pullState(pullLike), write: repo ? hasWriteAccess(repo.permissions) : null, repo, solo: who.solo };
10240
10245
  }
10241
10246
  var STATUS_WORDS = {
10242
10247
  merged: "Merged",
@@ -10267,7 +10272,7 @@ async function branchStatus(ctx, a) {
10267
10272
  lifecycle: f.lifecycle,
10268
10273
  status: { kind: f.status, label: STATUS_WORDS[f.status] },
10269
10274
  approvalNeeded: f.tier === 1 ? "One approval" : "Design and tech approval",
10270
- approvals: { approvedBy: f.approvals.approvers, changesRequestedBy: f.approvals.changesRequested, design: f.approvals.design, tech: f.approvals.tech },
10275
+ approvals: { approvedBy: f.approvals.approvers, changesRequestedBy: f.approvals.changesRequested, design: f.approvals.design, tech: f.approvals.tech, ...f.solo && (f.tier ?? 3) > 1 ? { soloOwner: soloOwnerSentence(f.solo, Boolean(viewer && viewer.toLowerCase() === f.solo)) } : {} },
10271
10276
  tests: { state: f.ci.state, failing: f.ci.failed.map((x) => x.name), running: f.ci.pending.map((x) => x.name) },
10272
10277
  main: { state: f.fresh.state, behindBy: f.fresh.behindBy },
10273
10278
  merge: {
@@ -10878,7 +10883,7 @@ async function suggestionGovernance(ctx, gh) {
10878
10883
  const { owners, rights } = config.governance;
10879
10884
  const hasTeams = [...owners.design, ...owners.tech].some((o) => o.includes("/"));
10880
10885
  const teams = hasTeams ? await new TeamDirectory(gh).resolve(owners).catch(() => void 0) : void 0;
10881
- return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {} } };
10886
+ return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {} } };
10882
10887
  }
10883
10888
  var OPEN_STATUSES = ["needs-review", "changes-requested", "approved", "applying", "outdated"];
10884
10889
  async function listSuggestionsOp(ctx, a = {}) {
@@ -10925,6 +10930,7 @@ async function getSuggestionOp(ctx, a) {
10925
10930
  const p = d.parsed.payload;
10926
10931
  const approvals = d.approvals;
10927
10932
  const missing = pendingApprovals(p.tier.tier, approvals);
10933
+ const solo = soloOwnerOf(gov, gov.teams);
10928
10934
  return {
10929
10935
  ...base,
10930
10936
  kind: p.kind,
@@ -10952,7 +10958,8 @@ async function getSuggestionOp(ctx, a) {
10952
10958
  missing: missing.map(roleText),
10953
10959
  changesRequestedBy: approvals.changesRequested,
10954
10960
  ...approvals.stale.length ? { staleReviews: approvals.stale } : {},
10955
- ...approvals.ignored.length ? { notCounted: approvals.ignored } : {}
10961
+ ...approvals.ignored.length ? { notCounted: approvals.ignored } : {},
10962
+ ...solo && p.tier.tier > 1 ? { soloOwner: soloOwnerSentence(solo) } : {}
10956
10963
  },
10957
10964
  applied: d.applied,
10958
10965
  ...p.componentSpec ? { brief: p.componentSpec.brief } : {},
@@ -11701,9 +11708,11 @@ async function reviewRoles(gh, config, approvers) {
11701
11708
  });
11702
11709
  const ship = shipPolicy(owners, rights, teams);
11703
11710
  const unreadable = unreadableTeams(teams);
11711
+ const solo = soloOwnerOf(config.governance, teams);
11704
11712
  return {
11705
11713
  design: role("design"),
11706
11714
  tech: role("tech"),
11715
+ ...solo ? { soloOwner: soloOwnerSentence(solo) } : {},
11707
11716
  ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can ship.",
11708
11717
  ...unreadable.length ? { unreadableTeams: unreadable.map((t) => t.message) } : {}
11709
11718
  };
@@ -11745,7 +11754,7 @@ async function visualImpact(ctx, a, run2) {
11745
11754
  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.");
11746
11755
  if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
11747
11756
  const out = a.out ? resolve3(ctx.root, a.out) : join5(ctx.root, "visual-impact");
11748
- const runImpact = run2 ?? (await import("./src-D6YUOJC7.js")).runImpact;
11757
+ const runImpact = run2 ?? (await import("./src-P3GHW467.js")).runImpact;
11749
11758
  const r = await runImpact({
11750
11759
  root: ctx.root,
11751
11760
  base: a.base ?? "main",
@@ -12525,6 +12534,7 @@ async function getSettings(ctx) {
12525
12534
  const protection = gh ? await loadProtection(gh, baseBranchOf(config)).catch(() => null) : null;
12526
12535
  const products = productsOf(config);
12527
12536
  const unreadable = teams ? unreadableTeams(teams) : [];
12537
+ const solo = soloOwnerOf(config.governance, teams);
12528
12538
  return {
12529
12539
  repo: config.repo ? `${config.repo.owner}/${config.repo.name}` : gh?.fullName ?? null,
12530
12540
  main: baseBranchOf(config),
@@ -12532,6 +12542,7 @@ async function getSettings(ctx) {
12532
12542
  designApproval: entries.design.map((e) => entryLabel(e, teams)),
12533
12543
  techApproval: entries.tech.map((e) => entryLabel(e, teams)),
12534
12544
  ship: ship ? whoCanShipSentence(ship) : "Anyone with write access can merge.",
12545
+ ...solo ? { soloOwner: soloOwnerSentence(solo) } : {},
12535
12546
  ...unreadable.length ? { teamsNotRead: unreadable.map((t) => t.message) } : {}
12536
12547
  },
12537
12548
  ownerOverride: override.allowed.length ? { on: true, who: override.allowed.map((l) => `@${l}`), reason: override.requireReason ? `A reason of at least ${override.minReasonLength} characters, posted on the branch.` : "No reason required." } : { on: false, who: [], reason: "Nobody can merge without the approvals." },
@@ -12811,7 +12822,7 @@ async function pushFigmaComponentOp(ctx, a) {
12811
12822
  }
12812
12823
 
12813
12824
  // src/server.ts
12814
- var VERSION = true ? "0.7.2" : "dev";
12825
+ var VERSION = true ? "0.8.0" : "dev";
12815
12826
  var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
12816
12827
  var fail2 = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
12817
12828
  var run = (fn) => async (a) => {
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  VERSION,
4
4
  optionsFromArgv,
5
5
  serveStdio
6
- } from "./chunk-FAB4HMPJ.js";
7
- import "./chunk-BZ3KXXMM.js";
6
+ } from "./chunk-VV5HHLLG.js";
7
+ import "./chunk-BHODFN52.js";
8
8
 
9
9
  // src/index.ts
10
10
  var argv = process.argv.slice(2);
package/dist/lib.js CHANGED
@@ -28,12 +28,12 @@ import {
28
28
  serveStdio,
29
29
  studio_exports,
30
30
  suggestions_exports
31
- } from "./chunk-FAB4HMPJ.js";
31
+ } from "./chunk-VV5HHLLG.js";
32
32
  import {
33
33
  CONFIG_FILE,
34
34
  loadConfig,
35
35
  toConfigJson
36
- } from "./chunk-BZ3KXXMM.js";
36
+ } from "./chunk-BHODFN52.js";
37
37
  export {
38
38
  BridgeContext,
39
39
  CONFIG_FILE,
package/dist/ops.d.ts CHANGED
@@ -736,6 +736,8 @@ export declare function getReview(ctx: BridgeContext, a: {
736
736
  */
737
737
  export declare function reviewRoles(gh: GitHubClient, config: BridgeConfig, approvers: string[]): Promise<{
738
738
  unreadableTeams?: string[] | undefined;
739
+ ship: string;
740
+ soloOwner?: string | undefined;
739
741
  design: {
740
742
  owners: string[];
741
743
  approvedBy: string[];
@@ -744,7 +746,6 @@ export declare function reviewRoles(gh: GitHubClient, config: BridgeConfig, appr
744
746
  owners: string[];
745
747
  approvedBy: string[];
746
748
  };
747
- ship: string;
748
749
  }>;
749
750
  export declare function reviewPr(ctx: BridgeContext, a: {
750
751
  pr: number;
@@ -11,7 +11,7 @@ import {
11
11
  referenceImagePath,
12
12
  selectTokenFiles,
13
13
  showFile
14
- } from "./chunk-BZ3KXXMM.js";
14
+ } from "./chunk-BHODFN52.js";
15
15
 
16
16
  // ../visual/src/run.ts
17
17
  import { existsSync as existsSync3 } from "node:fs";
package/dist/studio.d.ts CHANGED
@@ -94,6 +94,7 @@ export declare function getSettings(ctx: BridgeContext): Promise<{
94
94
  main: string;
95
95
  roles: {
96
96
  teamsNotRead?: string[] | undefined;
97
+ soloOwner?: string | undefined;
97
98
  designApproval: string[];
98
99
  techApproval: string[];
99
100
  ship: string;
@@ -137,6 +137,7 @@ export declare function getSuggestionOp(ctx: BridgeContext, a: {
137
137
  next: string;
138
138
  brief?: string | undefined;
139
139
  approvals: {
140
+ soloOwner?: string | undefined;
140
141
  notCounted?: string[] | undefined;
141
142
  staleReviews?: string[] | undefined;
142
143
  approvedBy: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/mcp",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Bridge MCP server: lets Claude (Claude Code, Claude Desktop or any MCP client) read, check and propose changes to a DTCG design system repo through pull requests.",
6
6
  "license": "MIT",
@@ -54,8 +54,8 @@
54
54
  "build:package": "node ../../scripts/build-packages.mjs mcp"
55
55
  },
56
56
  "dependencies": {
57
- "@bridge-ds/core": "^0.12.0",
58
- "@bridge-ds/visual": "^0.3.6",
57
+ "@bridge-ds/core": "^0.13.0",
58
+ "@bridge-ds/visual": "^0.3.7",
59
59
  "@modelcontextprotocol/sdk": "1.31.0",
60
60
  "typescript": "5.9.3",
61
61
  "zod": "3.25.76"