@bridge-ds/core 0.12.0 → 0.13.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.
@@ -63,11 +63,18 @@ export interface ApprovalSummary {
63
63
  design: boolean;
64
64
  tech: boolean;
65
65
  }
66
+ export interface ApprovalRules {
67
+ /**
68
+ * `governance.soloOwnerCanApproveBoth`: when the owners resolve to one person holding both roles, that person's
69
+ * approval counts for design and tech. Default false. The author still never approves their own change.
70
+ */
71
+ soloOwnerCanApproveBoth?: boolean | undefined;
72
+ }
66
73
  /** Latest decisive review per person (comments do not reset an approval), then owner coverage. */
67
74
  export declare function summarizeApprovals(reviews: ReviewLike[], owners: {
68
75
  design: string[];
69
76
  tech: string[];
70
- }, author?: string): ApprovalSummary;
77
+ }, author?: string, rules?: ApprovalRules): ApprovalSummary;
71
78
  export type MissingApproval = 'design approval' | 'tech approval' | 'approval';
72
79
  export interface MergeGate {
73
80
  allowed: boolean;
package/dist/rights.d.ts CHANGED
@@ -27,6 +27,26 @@ export declare function holdersOf(right: Right, owners: Owners, rights: Rights |
27
27
  export declare function approverLists(owners: Owners, rights: Rights | null | undefined, teams?: TeamStates | null): Owners;
28
28
  /** approverLists before team expansion: the entries as the config names them, for sentences. */
29
29
  export declare function approverEntries(owners: Owners, rights: Rights | null | undefined): Owners;
30
+ /**
31
+ * The one person who holds both approver places, or null: the design and tech lists (after rights, teams expanded)
32
+ * name exactly one login between them, and it sits in both. A team, read or not, never counts as one person.
33
+ */
34
+ export declare function soloApprover(owners: {
35
+ design: string[];
36
+ tech: string[];
37
+ }): string | null;
38
+ /**
39
+ * The only owner whose approval covers design and tech, or null when the rule is off or does not apply:
40
+ * `governance.soloOwnerCanApproveBoth` is true and the approver lists (after rights, teams expanded) name one person
41
+ * in both places. Studio Settings, the review screens, the MCP server and the CLI word it from this.
42
+ */
43
+ export declare function soloOwnerOf(gov: {
44
+ owners: Owners;
45
+ rights?: Rights | null | undefined;
46
+ soloOwnerCanApproveBoth?: boolean | undefined;
47
+ }, teams?: TeamStates | null): string | null;
48
+ /** The rule in one sentence, for Settings, the CLI and the MCP server. `you`: the viewer is that owner. */
49
+ export declare function soloOwnerSentence(login: string, you?: boolean): string;
30
50
  /** Who may ship, or null when nobody is singled out (no `rights`, or its groups list no logins): anyone with write access. */
31
51
  export interface ShipPolicy {
32
52
  /** Logins allowed to ship: people, and team members once read. A team not read keeps its "@org/slug" key. */
@@ -1,4 +1,4 @@
1
- import { cg as tokenBump, aQ as changeKind, L as LayeredTokenSet, aH as aliasOf, bZ as rgbaToDtcg, bn as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bq as inheritedTypeAt, u as upsertToken, H as readTokenAt, aW as comboKey, bY as renderedColor, b2 as contrastRatio, z as rgbaToHex, U as classifyRisk, W as RISK_LABELS, V as CONFIG_FILE, N as loadConfig, ak as ConfigError, _ as DEFAULT_TOKEN_PATHS, bl as globToRegExp, ae as slugify, Q as validateLayered, bI as parseJson, c1 as serializeJson, bN as plainEqual, cf as toPlain, bk as getEntry, bi as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aP as canShip, cn as whoCanShipSentence, ci as unreadableShipTeams, aL as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-BdMPKffE.js";
1
+ import { cj as tokenBump, aQ as changeKind, L as LayeredTokenSet, aH as aliasOf, bZ as rgbaToDtcg, bn as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bq as inheritedTypeAt, u as upsertToken, H as readTokenAt, aW as comboKey, bY as renderedColor, b2 as contrastRatio, z as rgbaToHex, U as classifyRisk, W as RISK_LABELS, V as CONFIG_FILE, N as loadConfig, ak as ConfigError, _ as DEFAULT_TOKEN_PATHS, bl as globToRegExp, ae as slugify, Q as validateLayered, bI as parseJson, c1 as serializeJson, bN as plainEqual, ci as toPlain, bk as getEntry, bi as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aP as canShip, cq as whoCanShipSentence, cl as unreadableShipTeams, c4 as soloApprover, aL as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-6ftQWwdu.js";
2
2
  function changesetBump(changes) {
3
3
  return tokenBump(changes) ?? "patch";
4
4
  }
@@ -2804,7 +2804,7 @@ function summarizeCi(runs, statuses = []) {
2804
2804
  const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
2805
2805
  return { state, total, passed, failed, pending };
2806
2806
  }
2807
- function summarizeApprovals(reviews, owners, author) {
2807
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
2808
2808
  const latest = /* @__PURE__ */ new Map();
2809
2809
  const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
2810
2810
  for (const r of sorted) {
@@ -2822,7 +2822,8 @@ function summarizeApprovals(reviews, owners, author) {
2822
2822
  let tech;
2823
2823
  if (d.length || t.length) {
2824
2824
  design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
2825
- const soloBoth = (x) => a.length === 1 && d.includes(x);
2825
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
2826
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
2826
2827
  tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
2827
2828
  } else {
2828
2829
  design = a.length >= 1;
@@ -3088,7 +3089,7 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
3088
3089
  latestOld.delete(c.author);
3089
3090
  reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
3090
3091
  });
3091
- const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0);
3092
+ const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
3092
3093
  const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
3093
3094
  return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
3094
3095
  }
@@ -4425,7 +4426,7 @@ async function planSuggestionApply(input) {
4425
4426
  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.`);
4426
4427
  if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
4427
4428
  }
4428
- const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null };
4429
+ const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
4429
4430
  const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
4430
4431
  const review = suggestionReviewState(payload.tier.tier, approvals);
4431
4432
  const missing = pendingApprovals(payload.tier.tier, approvals);
@@ -4817,4 +4818,4 @@ export {
4817
4818
  explorationNote as y,
4818
4819
  explorationNotePath as z
4819
4820
  };
4820
- //# sourceMappingURL=suggestion-apply-DxXRGQrO.js.map
4821
+ //# sourceMappingURL=suggestion-apply-bH6-gZrE.js.map