@bridge-ds/core 0.18.0 → 0.20.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.
Files changed (69) hide show
  1. package/LICENSE +17 -0
  2. package/bridge.config.schema.json +21 -0
  3. package/dist/audit/index.js +3 -3
  4. package/dist/audit/report.d.ts +17 -2
  5. package/dist/branch-actions.d.ts +7 -1
  6. package/dist/build-requests-github.d.ts +25 -0
  7. package/dist/build-requests.d.ts +403 -0
  8. package/dist/changelog-model.d.ts +102 -0
  9. package/dist/component-spec.d.ts +3 -1
  10. package/dist/config.d.ts +26 -0
  11. package/dist/css-color.d.ts +25 -0
  12. package/dist/docs-proposal.d.ts +1 -0
  13. package/dist/explorations.d.ts +1 -0
  14. package/dist/fidelity-references.d.ts +12 -0
  15. package/dist/fidelity.d.ts +12 -1
  16. package/dist/figma-fix-loop.d.ts +153 -0
  17. package/dist/figma-fix-run.d.ts +103 -0
  18. package/dist/figma-fixes.d.ts +169 -0
  19. package/dist/figma-push-github.d.ts +7 -2
  20. package/dist/figma-push.d.ts +15 -2
  21. package/dist/gap-case.d.ts +143 -0
  22. package/dist/github-client.d.ts +31 -0
  23. package/dist/id-map.d.ts +10 -0
  24. package/dist/index.d.ts +17 -0
  25. package/dist/index.js +3996 -818
  26. package/dist/index.js.map +1 -1
  27. package/dist/layered-review.d.ts +2 -0
  28. package/dist/measure-location.d.ts +42 -0
  29. package/dist/measure-status.d.ts +127 -0
  30. package/dist/measure-workflow.d.ts +48 -0
  31. package/dist/measured-only.d.ts +30 -0
  32. package/dist/measured-spec.d.ts +349 -0
  33. package/dist/measured-tokens.d.ts +25 -0
  34. package/dist/motion-docs.d.ts +51 -0
  35. package/dist/node/audit.d.ts +11 -3
  36. package/dist/node/index.d.ts +1 -0
  37. package/dist/node/index.js +1640 -24
  38. package/dist/node/index.js.map +1 -1
  39. package/dist/node/measure-script.d.ts +1 -0
  40. package/dist/node/measure.d.ts +212 -0
  41. package/dist/node/spec-css.d.ts +9 -1
  42. package/dist/node/tokens-script.d.ts +3 -0
  43. package/dist/pr-composer.d.ts +5 -0
  44. package/dist/previews.d.ts +4 -1
  45. package/dist/{prod-branch-DenQkr_e.js → prod-branch-CJZwMfVD.js} +223 -183
  46. package/dist/prod-branch-CJZwMfVD.js.map +1 -0
  47. package/dist/prod-branch.d.ts +1 -1
  48. package/dist/repo-model.d.ts +7 -1
  49. package/dist/{repo-source-CRfhLO87.js → repo-source-CshjjC7x.js} +1485 -629
  50. package/dist/repo-source-CshjjC7x.js.map +1 -0
  51. package/dist/repo-source-github.d.ts +8 -0
  52. package/dist/repo-source.d.ts +6 -0
  53. package/dist/{report-DVEmUr0p.js → report-CE-jHAuw.js} +17 -6
  54. package/dist/{report-DVEmUr0p.js.map → report-CE-jHAuw.js.map} +1 -1
  55. package/dist/requester.d.ts +14 -0
  56. package/dist/requests.d.ts +2 -0
  57. package/dist/review-gate.d.ts +189 -2
  58. package/dist/risk.d.ts +12 -0
  59. package/dist/share-branch.d.ts +1 -1
  60. package/dist/snapshot.d.ts +34 -0
  61. package/dist/suggestion-review.d.ts +45 -2
  62. package/dist/suggestions-github.d.ts +33 -1
  63. package/dist/suggestions.d.ts +6 -0
  64. package/dist/tier1-publish.d.ts +48 -0
  65. package/dist/token-proposal.d.ts +7 -0
  66. package/dist/try-in-product.d.ts +9 -0
  67. package/package.json +1 -1
  68. package/dist/prod-branch-DenQkr_e.js.map +0 -1
  69. package/dist/repo-source-CRfhLO87.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { cn as tokenBump, aU as changeKind, L as LayeredTokenSet, aL as aliasOf, c1 as rgbaToDtcg, bq as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bt as inheritedTypeAt, u as upsertToken, H as readTokenAt, a_ as comboKey, c0 as renderedColor, b6 as contrastRatio, z as rgbaToHex, U as classifyRisk, W as RISK_LABELS, V as CONFIG_FILE, N as loadConfig, ah as ConfigError, _ as DEFAULT_TOKEN_PATHS, ag as globToRegExp, ae as slugify, Q as validateLayered, bM as parseJson, c5 as serializeJson, bR as plainEqual, cm as toPlain, bn as getEntry, bl as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aT as canShip, cv as whoCanShipSentence, cp as unreadableShipTeams, c8 as soloApprover, aP as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-DenQkr_e.js";
1
+ import { cr as tokenBump, aW as changeKind, L as LayeredTokenSet, aO as aliasOf, c4 as rgbaToDtcg, bu as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bx as inheritedTypeAt, u as upsertToken, H as readTokenAt, b0 as comboKey, c3 as renderedColor, b8 as contrastRatio, z as rgbaToHex, U as classifyRisk, bh as figmaPushNextLine, W as RISK_LABELS, bi as figmaPushTierLabel, V as CONFIG_FILE, N as loadConfig, ai as ConfigError, $ as DEFAULT_TOKEN_PATHS, ah as globToRegExp, af as slugify, Q as validateLayered, bP as parseJson, c8 as serializeJson, bU as plainEqual, cq as toPlain, br as getEntry, bp as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a2 as readPreviewComment, aV as canShip, cz as whoCanShipSentence, ct as unreadableShipTeams, cb as soloApprover, aS as approverLists, O as tokensPath, a7 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-CJZwMfVD.js";
2
2
  function changesetBump(changes) {
3
3
  return tokenBump(changes) ?? "patch";
4
4
  }
@@ -177,9 +177,9 @@ function checkLayeredPair(set, pair, combo) {
177
177
  const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
178
178
  return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
179
179
  }
180
- function runLayeredContrast(set, pairs, modes) {
180
+ function runLayeredContrast(set, pairs2, modes) {
181
181
  const combos = set.combos(modes ?? set.config.contrast?.modes);
182
- return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
182
+ return combos.flatMap((c) => pairs2.map((p) => checkLayeredPair(set, p, c)));
183
183
  }
184
184
  function contrastDelta(before, after) {
185
185
  const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
@@ -295,14 +295,15 @@ _${deltas.length - limit} more pair(s) not shown._` : "";
295
295
  }
296
296
  function composeProposalBody(args) {
297
297
  const t = args.tier.tier;
298
- const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
298
+ const figma = Boolean(args.fromFigma);
299
+ const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${figma && n === t ? figmaPushTierLabel(n, args.changes) : RISK_LABELS[n]}`;
299
300
  const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
300
301
  return [
301
302
  "## What changed and why",
302
303
  "",
303
304
  args.summary.trim(),
304
305
  "",
305
- `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
306
+ figma ? `Pushed from Figma through ${args.tool ?? "the Bridge Figma plugin"}${args.author ? ` by ${args.author}` : ""}. ${figmaPushNextLine(args.changes)}` : `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
306
307
  "",
307
308
  "## Risk tier",
308
309
  "",
@@ -332,8 +333,8 @@ function composeProposalBody(args) {
332
333
  "",
333
334
  "## Authored by",
334
335
  "",
335
- "- [ ] Human",
336
- "- [x] AI agent (Claude), reviewed by a human before merge",
336
+ `- [${figma ? "x" : " "}] Human`,
337
+ `- [${figma ? " " : "x"}] AI agent (Claude), reviewed by a human before merge`,
337
338
  "",
338
339
  "## Design review checklist",
339
340
  "",
@@ -368,6 +369,7 @@ class GitHubError extends Error {
368
369
  this.name = "GitHubError";
369
370
  this.auth = info.auth;
370
371
  this.permission = info.permission;
372
+ this.resetsAt = info.resetsAt;
371
373
  }
372
374
  status;
373
375
  detail;
@@ -375,6 +377,8 @@ class GitHubError extends Error {
375
377
  auth;
376
378
  /** With auth "missing-permission": the permission the call needed, when known. */
377
379
  permission;
380
+ /** For a rate limit (status 429): when the limit resets, when known. */
381
+ resetsAt;
378
382
  }
379
383
  const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
380
384
  const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
@@ -656,7 +660,7 @@ class GitHubClient {
656
660
  return copy(cached.body);
657
661
  }
658
662
  const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
659
- const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
663
+ const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail, { resetsAt });
660
664
  hourlyLimitErrors.add(limited);
661
665
  throw limited;
662
666
  }
@@ -693,6 +697,31 @@ class GitHubClient {
693
697
  );
694
698
  return { fullName: r.full_name, defaultBranch: r.default_branch, private: r.private, permissions: r.permissions };
695
699
  }
700
+ /**
701
+ * What the token may write here, found without writing anything: each probe is invalid on purpose (a zero sha, a
702
+ * missing head branch, an empty title), so GitHub answers 422 when the token has the permission and 403 or 404 when
703
+ * it does not. repo.permissions.push says what the user may do, not what a fine-grained token may do.
704
+ */
705
+ async writeAccess(base) {
706
+ this.assertToken("Checking the token");
707
+ const probe = async (path, body) => {
708
+ try {
709
+ await this.request("POST", `${this.repoPath}${path}`, body);
710
+ return true;
711
+ } catch (err) {
712
+ if (!(err instanceof GitHubError)) throw err;
713
+ if (err.status === 422) return true;
714
+ if (err.auth === "missing-permission" || err.status === 404 || err.status === 410) return false;
715
+ throw err;
716
+ }
717
+ };
718
+ const [contents, pullRequests, issues] = await Promise.all([
719
+ probe("/git/refs", { ref: "refs/heads/bridge-permission-probe", sha: "0".repeat(40) }),
720
+ probe("/pulls", { title: "Bridge permission probe", head: "bridge-permission-probe-missing-branch", base }),
721
+ probe("/issues", { title: "" })
722
+ ]);
723
+ return { contents, pullRequests, issues };
724
+ }
696
725
  /** Commit sha a branch points to, or undefined when the branch does not exist. */
697
726
  async getBranchSha(branch) {
698
727
  try {
@@ -703,6 +732,14 @@ class GitHubClient {
703
732
  throw err;
704
733
  }
705
734
  }
735
+ /**
736
+ * Whether a branch exists, through the matching-refs list: it answers 200 with an empty list when the branch is
737
+ * missing, so asking never leaves a 404 in the browser console (an optional branch like bridge-data).
738
+ */
739
+ async hasBranch(branch) {
740
+ const refs = await this.request("GET", `${this.repoPath}/git/matching-refs/heads/${encodeRef(branch)}`);
741
+ return Array.isArray(refs) && refs.some((r) => r.ref === `refs/heads/${branch}`);
742
+ }
706
743
  async getCommitTree(commitSha) {
707
744
  const r = await this.request("GET", `${this.repoPath}/git/commits/${commitSha}`);
708
745
  return r.tree.sha;
@@ -989,6 +1026,7 @@ class GitHubClient {
989
1026
  const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
990
1027
  const r = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`);
991
1028
  if (r.type !== "file") return void 0;
1029
+ 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 });
992
1030
  if (r.encoding === "base64" && r.content !== void 0) return base64ToUtf8(r.content);
993
1031
  return r.content;
994
1032
  } catch (err) {
@@ -1208,7 +1246,7 @@ class GitHubClient {
1208
1246
  }
1209
1247
  async getPullRequest(n) {
1210
1248
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
1211
- return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login) };
1249
+ return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login), autoMerge: Boolean(r.auto_merge) };
1212
1250
  }
1213
1251
  async listPullRequestFiles(n) {
1214
1252
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
@@ -1219,7 +1257,7 @@ class GitHubClient {
1219
1257
  }
1220
1258
  async listReviews(n) {
1221
1259
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}/reviews`);
1222
- return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {} }));
1260
+ return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {}, ...x.commit_id ? { commitId: x.commit_id } : {} }));
1223
1261
  }
1224
1262
  /** Submit a review. `COMMENT` works for any collaborator; approving your own PR is refused by GitHub. */
1225
1263
  async createReview(n, input) {
@@ -1413,7 +1451,10 @@ function toSummary(r) {
1413
1451
  labels: (r.labels ?? []).map((l) => l.name),
1414
1452
  url: r.html_url,
1415
1453
  updatedAt: r.updated_at,
1416
- ...typeof r.body === "string" ? { body: r.body } : {}
1454
+ ...typeof r.body === "string" ? { body: r.body } : {},
1455
+ ...r.created_at ? { createdAt: r.created_at } : {},
1456
+ ...r.merged_at ? { mergedAt: r.merged_at } : {},
1457
+ ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {}
1417
1458
  };
1418
1459
  }
1419
1460
  function friendlyMessage(status, detail, info = {}) {
@@ -1490,9 +1531,37 @@ function toIssue(r) {
1490
1531
  labels: r.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
1491
1532
  author: r.user?.login ?? "",
1492
1533
  createdAt: r.created_at,
1493
- updatedAt: r.updated_at
1534
+ updatedAt: r.updated_at,
1535
+ ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {},
1536
+ ...r.closed_at ? { closedAt: r.closed_at } : {}
1494
1537
  };
1495
1538
  }
1539
+ const FORMER_MEMBER = "A former member";
1540
+ const requesterFirstName = (name) => name.trim() === FORMER_MEMBER ? FORMER_MEMBER : name.trim().split(/\s+/)[0] ?? name;
1541
+ const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
1542
+ const LINE = /(?:^|\n)[ \t>*_-]*(?:<sub>)?\s*Requested (?:from [^\n<]{1,40}? )?by ([^\n<]{1,120}?)(?:\s+with Bridge)?\.?\s*(?:<\/sub>)?[ \t]*(?=\n|$)/i;
1543
+ const SUGGESTED = new RegExp(LINE.source.replace("Requested (?:from", "Suggested (?:from"), "i");
1544
+ function requesterOf(body) {
1545
+ const m = LINE.exec(body ?? "") ?? SUGGESTED.exec(body ?? "");
1546
+ if (!m) return null;
1547
+ const who = m[1].trim().replace(/\.$/, "").replace(/\s+through Bridge$/i, "").replace(/\s*\([^()\s@]+@[^()\s]+\)$/, "");
1548
+ const both = /^(.+?)\s*\(@([^)\s]+)\)$/.exec(who);
1549
+ if (both) return LOGIN.test(both[2]) ? { name: both[1].trim(), login: both[2] } : null;
1550
+ if (who.startsWith("@")) return LOGIN.test(who.slice(1)) ? { name: null, login: who.slice(1) } : null;
1551
+ return /^[\p{L}][\p{L}'’. -]{0,60}$/u.test(who) && who.split(/\s+/).length <= 5 ? { name: who, login: null } : null;
1552
+ }
1553
+ function requesterLine(r) {
1554
+ const name = r.name?.trim();
1555
+ const login = r.login?.replace(/^@/, "").trim();
1556
+ if (name && login) return `Requested by ${name} (@${login}).`;
1557
+ if (login) return `Requested by @${login}.`;
1558
+ if (name) return `Requested by ${name}.`;
1559
+ throw new Error("A requester needs a name or a login.");
1560
+ }
1561
+ function requesterName(body) {
1562
+ const r = requesterOf(body);
1563
+ return r ? r.name ?? r.login : null;
1564
+ }
1496
1565
  const BULK_LIMIT = 60;
1497
1566
  const UNRELEASED = "Unreleased";
1498
1567
  const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
@@ -1557,7 +1626,9 @@ function buildHistoryEntry(input) {
1557
1626
  sha: commit.sha,
1558
1627
  ...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
1559
1628
  date: commit.date,
1560
- author: commit.pr?.author ?? commit.author,
1629
+ // Who asked, as Bridge recorded it in the pull request ("Requested by Priya Shah ... through Bridge."), before the
1630
+ // account that opened it (the Bridge app for an email member, an agent): History names the person.
1631
+ author: requesterName(commit.pr?.body) ?? commit.pr?.author ?? commit.author,
1561
1632
  title,
1562
1633
  why,
1563
1634
  ...pr ? { pr } : {},
@@ -1910,9 +1981,9 @@ function finishPlan(p) {
1910
1981
  if (!changes.length) return nothing("The edits leave every token as it is now.");
1911
1982
  const prev = new Set(validateLayered(current).map(issueKey));
1912
1983
  const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
1913
- const pairs = p.pairs ?? [];
1984
+ const pairs2 = p.pairs ?? [];
1914
1985
  const modes = config.contrast?.modes;
1915
- const contrast = pairs.length ? contrastDelta(runLayeredContrast(current, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
1986
+ const contrast = pairs2.length ? contrastDelta(runLayeredContrast(current, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
1916
1987
  const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
1917
1988
  const tier = classifyLayered(changes, [], config);
1918
1989
  const impact = layeredImpact(current, after, changes);
@@ -2177,11 +2248,11 @@ function propShape(s) {
2177
2248
  }
2178
2249
  const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
2179
2250
  function shortVariant(key) {
2180
- const pairs = key.split(", ").map((p) => {
2251
+ const pairs2 = key.split(", ").map((p) => {
2181
2252
  const at = p.indexOf("=");
2182
2253
  return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
2183
2254
  });
2184
- const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
2255
+ const kept = pairs2.filter((p) => p.v && !/^default$/i.test(p.v));
2185
2256
  if (!kept.length) return key;
2186
2257
  const rank = (k) => {
2187
2258
  const i = VARIANT_FIRST.indexOf(k.toLowerCase());
@@ -2222,6 +2293,14 @@ function firstChange(component, kind, c, all) {
2222
2293
  }
2223
2294
  case "new-variant": {
2224
2295
  if (!c) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2296
+ if (c.field === "Variants") {
2297
+ const gone = (c.before ?? "").split("; ").filter(Boolean);
2298
+ const born = (c.after ?? "").split("; ").filter(Boolean);
2299
+ if (gone.length === 1 && !born.length) return { title: `Remove the ${shortVariant(gone[0])} variant`, summary: `${component} no longer has the ${shortVariant(gone[0])} variant (${gone[0]}).` };
2300
+ if (born.length === 1 && !gone.length) return { title: `Add the ${shortVariant(born[0])} variant`, summary: `${component} gets a new variant, ${shortVariant(born[0])} (${born[0]}).` };
2301
+ const parts = [born.length ? `gets ${plural(born.length, "new variant")}: ${andList(born.map(shortVariant))}` : "", gone.length ? `no longer has ${andList(gone.map(shortVariant))}` : ""].filter(Boolean);
2302
+ return { title: `Update the ${component} variants`, summary: `${component} ${parts.join(", and ")}.` };
2303
+ }
2225
2304
  const axis = c.field.replace(/^Variant /, "");
2226
2305
  const before = values(c.before);
2227
2306
  const after = values(c.after);
@@ -2250,7 +2329,7 @@ function firstChange(component, kind, c, all) {
2250
2329
  if (!c) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2251
2330
  const m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2252
2331
  const part = lower(m[1]);
2253
- const where2 = m[2] ? ` in ${m[2]}` : "";
2332
+ const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2254
2333
  const after = c.after && c.after !== "none" ? c.after : "";
2255
2334
  const before = c.before && c.before !== "none" ? c.before : "";
2256
2335
  if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where2} no longer uses ${before || "a token"}.` };
@@ -2300,6 +2379,11 @@ function sanitizeDraftLine(raw) {
2300
2379
  }
2301
2380
  function changePhrase(c) {
2302
2381
  let m;
2382
+ if (c.field === "Variants") {
2383
+ const gone = (c.before ?? "").split("; ").filter(Boolean).map(shortVariant);
2384
+ const born = (c.after ?? "").split("; ").filter(Boolean).map(shortVariant);
2385
+ return [born.length ? `New variant${born.length === 1 ? "" : "s"} ${andList(born)}` : "", gone.length ? `${andList(gone)} variant${gone.length === 1 ? "" : "s"} removed` : ""].filter(Boolean).join("; ");
2386
+ }
2303
2387
  if (m = /^Variant (.+)$/.exec(c.field)) {
2304
2388
  const axis = m[1];
2305
2389
  const before2 = values(c.before);
@@ -2325,7 +2409,7 @@ function changePhrase(c) {
2325
2409
  const layer = parseStructureChange(c);
2326
2410
  if (layer) return structurePhrase(layer);
2327
2411
  m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2328
- const where2 = m[2] ? ` in ${m[2]}` : "";
2412
+ const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2329
2413
  const before = c.before && c.before !== "none" ? c.before : "";
2330
2414
  const after = c.after && c.after !== "none" ? c.after : "";
2331
2415
  if (!after) return `${m[1]} no longer uses ${before || "a token"}${where2}`;
@@ -2403,10 +2487,10 @@ const EXPLORATION_NOTES_DIR = ".bridge/explorations";
2403
2487
  const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
2404
2488
  const EXPLORATION_CHECK = "Exploration not merged as is";
2405
2489
  const EXPLORATION_USE_CASES = [
2406
- { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype on a product staging branch, with the real components and tokens." },
2407
- { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone commits to it." },
2408
- { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a staging or experiment branch and compare it with what is live." },
2409
- { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server on this branch, never on main." },
2490
+ { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype in a test version of a product, with the real components and tokens." },
2491
+ { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
2492
+ { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
2493
+ { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server inside this exploration, never in the library." },
2410
2494
  { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
2411
2495
  ];
2412
2496
  const EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
@@ -2433,8 +2517,8 @@ function explorationProblems(e) {
2433
2517
  const name = e.name.trim();
2434
2518
  const slug = e.slug?.trim() || explorationSlug(name);
2435
2519
  if (!name) out.push('Name the exploration, for example "Denser data tables".');
2436
- else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the branch name.");
2437
- else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the branch explore/<name>.");
2520
+ else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
2521
+ else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the exploration's link name.");
2438
2522
  if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
2439
2523
  return out;
2440
2524
  }
@@ -2565,7 +2649,8 @@ function explorationShareMessage(input) {
2565
2649
  const name = input.name.trim().replace(/"/g, "'");
2566
2650
  const head = input.command ? `Exploration "${name}" (${input.branch}): try it in a staging branch of your product with: ${input.command}.` : `Exploration "${name}" (${input.branch}): its test build is being prepared, the install command for a staging branch of your product will be on the pull request in a few minutes.`;
2567
2651
  const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
2568
- return [head, previews.length ? `Previews: ${previews.join(", ")}.` : "", `Pull request: ${input.url}.`].filter(Boolean).join(" ");
2652
+ const local = !previews.length && input.local ? `Try it locally: ${input.local}, then start Storybook or the app with its dev command.` : "";
2653
+ return [head, previews.length ? `Previews: ${previews.join(", ")}.` : local, `Pull request: ${input.url}.`].filter(Boolean).join(" ");
2569
2654
  }
2570
2655
  function explorationRules(branch) {
2571
2656
  return [
@@ -2800,6 +2885,7 @@ const SPEC_FIELDS = [
2800
2885
  "minWidth",
2801
2886
  "minHeight",
2802
2887
  "opacity",
2888
+ "shadow",
2803
2889
  "textFill",
2804
2890
  "fontFamily",
2805
2891
  "fontSize",
@@ -2996,139 +3082,841 @@ function figmaCell(entry) {
2996
3082
  if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
2997
3083
  return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
2998
3084
  }
2999
- function parseTier(labels, body) {
3000
- const found = [];
3001
- for (const l of labels) {
3002
- const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
3003
- if (m) found.push(Number(m[1]));
3004
- }
3005
- for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
3006
- for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
3007
- return found.length ? Math.max(...found) : null;
3008
- }
3009
- function pullState(p) {
3010
- if (p.merged_at || p.merged) return "merged";
3011
- if (p.state && p.state !== "open") return "closed";
3012
- if (p.draft) return "draft";
3013
- return "open";
3014
- }
3015
- const PULL_STATE_PILL = {
3016
- merged: { label: "Merged", cls: "" },
3017
- closed: { label: "Closed", cls: "" },
3018
- draft: { label: "Draft", cls: "" },
3019
- open: { label: "Open", cls: "" }
3085
+ const px = (v) => {
3086
+ if (!v) return void 0;
3087
+ const m = /^(-?[\d.]+)px$/.exec(v.trim());
3088
+ return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
3020
3089
  };
3021
- function openPulls(pulls) {
3022
- return pulls.filter((p) => {
3023
- const s = pullState(p);
3024
- return s === "open" || s === "draft";
3090
+ function evalCalc(expr, resolve) {
3091
+ const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
3092
+ if (!body) return void 0;
3093
+ let missing = false;
3094
+ const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
3095
+ const v2 = resolve(path);
3096
+ const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
3097
+ if (n === void 0 || Number.isNaN(n)) missing = true;
3098
+ return String(n);
3025
3099
  });
3100
+ if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
3101
+ const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
3102
+ let i = 0;
3103
+ const factor = () => {
3104
+ const t = tokens[i++];
3105
+ if (t === "(") {
3106
+ const v2 = sum();
3107
+ i++;
3108
+ return v2;
3109
+ }
3110
+ if (t === "-") return -factor();
3111
+ return Number(t);
3112
+ };
3113
+ const product = () => {
3114
+ let v2 = factor();
3115
+ while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
3116
+ return v2;
3117
+ };
3118
+ const sum = () => {
3119
+ let v2 = product();
3120
+ while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
3121
+ return v2;
3122
+ };
3123
+ const v = sum();
3124
+ return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
3125
+ }
3126
+ const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
3127
+ function normColor(v) {
3128
+ const t = v.trim().toLowerCase();
3129
+ const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
3130
+ if (hex2) {
3131
+ const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
3132
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
3133
+ const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
3134
+ return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
3135
+ }
3136
+ const m = /^rgba?\(([^)]*)\)$/.exec(t);
3137
+ if (m) {
3138
+ const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
3139
+ const [r, g, b] = parts;
3140
+ const a = parts[3] ?? 1;
3141
+ return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
3142
+ }
3143
+ return t.replace(/\s+/g, "");
3144
+ }
3145
+ const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
3146
+ const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
3147
+ function withoutAddedWrappers(parts, measured) {
3148
+ const real = new Set(measured.parts.map((m) => m.id));
3149
+ const added = /* @__PURE__ */ new Map();
3150
+ for (const p of parts) {
3151
+ if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
3152
+ const kids = parts.filter((c) => c.parent === p.id);
3153
+ if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
3154
+ }
3155
+ if (!added.size) return parts;
3156
+ const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
3157
+ return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
3158
+ }
3159
+ function pairParts(staticParts, measured, o = {}) {
3160
+ const pairs2 = /* @__PURE__ */ new Map();
3161
+ const taken = /* @__PURE__ */ new Set();
3162
+ const mParts = new Map(measured.parts.map((p) => [p.id, p]));
3163
+ const sParts = new Map(staticParts.map((p) => [p.id, p]));
3164
+ const take = (s, m) => {
3165
+ pairs2.set(s, m);
3166
+ taken.add(m);
3167
+ };
3168
+ const ancestors = (id) => {
3169
+ const out = [];
3170
+ const seen = /* @__PURE__ */ new Set([id]);
3171
+ for (let p = mParts.get(id)?.parent; p && !seen.has(p); p = mParts.get(p)?.parent) {
3172
+ seen.add(p);
3173
+ out.push(p);
3174
+ }
3175
+ return out;
3176
+ };
3177
+ const pairedAncestor = (p) => {
3178
+ const seen = /* @__PURE__ */ new Set([p.id]);
3179
+ for (let a = p.parent; a && !seen.has(a); a = sParts.get(a)?.parent) {
3180
+ if (pairs2.has(a)) return pairs2.get(a);
3181
+ seen.add(a);
3182
+ }
3183
+ return void 0;
3184
+ };
3185
+ const best = (p, cands) => {
3186
+ if (!cands.length) return void 0;
3187
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
3188
+ if (parent) {
3189
+ const direct = cands.find((c) => c.parent === parent);
3190
+ if (direct) return direct;
3191
+ }
3192
+ const anc = pairedAncestor(p);
3193
+ if (anc) {
3194
+ const under2 = cands.find((c) => ancestors(c.id).includes(anc));
3195
+ if (under2) return under2;
3196
+ }
3197
+ return cands[0];
3198
+ };
3199
+ const root = staticParts.find((p) => !p.parent);
3200
+ if (root) take(root.id, "root");
3201
+ const textOf = (p) => {
3202
+ if (p.characters) return normText(p.characters);
3203
+ const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
3204
+ return typeof prop?.default === "string" ? normText(prop.default) : "";
3205
+ };
3206
+ const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
3207
+ const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
3208
+ const shared = (p, m) => {
3209
+ const mine = new Set(classesOf(p));
3210
+ return mClasses(m).filter((c) => mine.has(c)).length;
3211
+ };
3212
+ const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
3213
+ const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
3214
+ for (const p of staticParts) {
3215
+ if (p.kind !== "text" || pairs2.has(p.id)) continue;
3216
+ const t = textOf(p);
3217
+ if (!t) continue;
3218
+ const hit = best(p, measured.parts.filter((m) => m.kind === "text" && !taken.has(m.id) && normText(m.characters) === t && !(classesOf(p).length && mClasses(m).length && !shared(p, m))));
3219
+ if (hit) take(p.id, hit.id);
3220
+ }
3221
+ const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
3222
+ for (const p of staticParts) {
3223
+ if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
3224
+ const sp = sParts.get(p.parent);
3225
+ if (!sp || sp.kind !== "frame" || !sp.parent) continue;
3226
+ const cls = classOf(sp);
3227
+ const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
3228
+ if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
3229
+ }
3230
+ for (const p of staticParts) {
3231
+ if (pairs2.has(p.id) || !classesOf(p).length) continue;
3232
+ if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
3233
+ const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
3234
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
3235
+ const under2 = parent ? all.filter((m) => m.parent === parent) : [];
3236
+ const cands = under2.length ? under2 : all;
3237
+ const most = Math.max(0, ...cands.map((m) => shared(p, m)));
3238
+ const hit = best(p, cands.filter((m) => shared(p, m) === most));
3239
+ if (hit) take(p.id, hit.id);
3240
+ }
3241
+ for (const p of staticParts) {
3242
+ if (pairs2.has(p.id)) continue;
3243
+ if (p.parent && !pairs2.has(p.parent)) continue;
3244
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
3245
+ const known = p.kind === "text" && !!textOf(p);
3246
+ const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
3247
+ const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
3248
+ if (hit) take(p.id, hit.id);
3249
+ }
3250
+ for (const p of staticParts) {
3251
+ if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
3252
+ const mp = pairs2.get(p.parent);
3253
+ const t = textOf(p);
3254
+ if (mp && mParts.get(mp)?.kind === "text" && staticParts.filter((x) => x.parent === p.parent).length === 1 && (!t || normText(mParts.get(mp).characters) === t)) pairs2.set(p.id, mp);
3255
+ }
3256
+ return pairs2;
3257
+ }
3258
+ function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
3259
+ const pairs2 = pairParts(staticParts, measured, o);
3260
+ const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
3261
+ const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
3262
+ const near = (a, b) => {
3263
+ if (a === void 0 || b === void 0) return false;
3264
+ const na0 = num2(a);
3265
+ const nb0 = num2(b);
3266
+ if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
3267
+ if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
3268
+ const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
3269
+ const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
3270
+ return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
3271
+ };
3272
+ for (const sp of staticParts) {
3273
+ const mid = pairs2.get(sp.id);
3274
+ if (!mid) {
3275
+ out.unmatchedParts.push(sp.id);
3276
+ continue;
3277
+ }
3278
+ const sv = staticValues[sp.id] ?? {};
3279
+ const mv = mid ? measured.values[mid] ?? {} : {};
3280
+ const mr = mid ? measured.raw[mid] ?? {} : {};
3281
+ const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
3282
+ for (const f of fields) {
3283
+ if ((f === "width" || f === "height") && sv[f] === void 0) continue;
3284
+ const s = sv[f];
3285
+ const m = mv[f] !== void 0 ? mv[f] : mr[f];
3286
+ if (s === void 0) {
3287
+ if (m !== void 0 && m !== 0) out.measuredOnly++;
3288
+ continue;
3289
+ }
3290
+ if (m === void 0 && !(f in mr)) {
3291
+ if (s === 0 || s === null) continue;
3292
+ const r = typeof s === "string" && resolve ? resolve(s) : s;
3293
+ if (r === 0 || r === "0px" || r === null) continue;
3294
+ out.staticOnly++;
3295
+ out.diffs.push({ part: sp.id, field: f, static: s, same: false });
3296
+ continue;
3297
+ }
3298
+ out.compared++;
3299
+ let same2 = s === m;
3300
+ if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
3301
+ if (same2 && typeof s === "string") out.sameToken++;
3302
+ if (!same2 && resolve) {
3303
+ let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
3304
+ const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
3305
+ if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
3306
+ same2 = near(rs, mr[f]);
3307
+ }
3308
+ if (same2) out.same++;
3309
+ else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
3310
+ }
3311
+ }
3312
+ const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
3313
+ const box = measured.boxes.root;
3314
+ if (box) out.rootSize = { static: { width: typeof root.width === "number" ? root.width : void 0, height: typeof root.height === "number" ? root.height : void 0 }, measured: { width: box.width, height: box.height } };
3315
+ return out;
3026
3316
  }
3027
- const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
3028
- function summarizeCi(runs, statuses = []) {
3029
- const failed = [];
3030
- const pending = [];
3031
- let passed = 0;
3032
- for (const r of runs) {
3033
- if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
3034
- else if (r.conclusion && OK.has(r.conclusion)) passed++;
3035
- else failed.push({ name: r.name, url: r.html_url });
3317
+ function tokenVariablePath(name, prefix) {
3318
+ let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
3319
+ if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
3320
+ return parts.join("/");
3321
+ }
3322
+ function sharedTokenPrefix(names2) {
3323
+ if (names2.length < 20) return void 0;
3324
+ const counts = /* @__PURE__ */ new Map();
3325
+ for (const n2 of names2) {
3326
+ const first = n2.replace(/^--/, "").split("-")[0];
3327
+ counts.set(first, (counts.get(first) ?? 0) + 1);
3036
3328
  }
3329
+ const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
3330
+ return top && n / names2.length >= 0.6 ? top : void 0;
3331
+ }
3332
+ function markUsedTokens(tokens, stories) {
3333
+ const byName = new Map(tokens.properties.map((p) => [p.name, p]));
3037
3334
  const seen = /* @__PURE__ */ new Set();
3038
- for (const s of statuses) {
3039
- if (seen.has(s.context)) continue;
3040
- seen.add(s.context);
3041
- if (s.state === "success") passed++;
3042
- else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
3043
- else failed.push({ name: s.context, url: s.target_url ?? void 0 });
3335
+ const use = (name) => {
3336
+ if (seen.has(name)) return;
3337
+ seen.add(name);
3338
+ const p = byName.get(name);
3339
+ if (!p) return;
3340
+ p.used = true;
3341
+ for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
3342
+ if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
3343
+ };
3344
+ for (const s of stories)
3345
+ for (const spec of [s.spec, ...s.instances ?? []])
3346
+ for (const part of Object.values(spec?.tokens ?? {}))
3347
+ for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
3348
+ const used = tokens.properties.filter((p) => p.used).length;
3349
+ tokens.counts.used = used;
3350
+ return used;
3351
+ }
3352
+ class MeasuredSpecsError extends Error {
3353
+ }
3354
+ const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3355
+ function parseMeasuredSpecsFile(input) {
3356
+ let data = input;
3357
+ if (typeof input === "string") {
3358
+ try {
3359
+ data = JSON.parse(input);
3360
+ } catch (e) {
3361
+ throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
3362
+ }
3044
3363
  }
3045
- const total = passed + failed.length + pending.length;
3046
- const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
3047
- return { state, total, passed, failed, pending };
3048
- }
3049
- function summarizeApprovals(reviews, owners, author, rules = {}) {
3050
- const latest = /* @__PURE__ */ new Map();
3051
- const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
3052
- for (const r of sorted) {
3053
- const login = r.user?.login;
3054
- if (!login || login === author) continue;
3055
- if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
3364
+ if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
3365
+ if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
3366
+ if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
3367
+ if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
3368
+ data.stories.forEach((s, i) => {
3369
+ if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
3370
+ const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
3371
+ for (const sp of specs) {
3372
+ if (!isObj$1(sp) || !Array.isArray(sp.parts) || !isObj$1(sp.values)) throw new MeasuredSpecsError(`measured specs: story ${s.id} has a spec without parts and values`);
3373
+ for (const p of sp.parts) if (!isObj$1(p) || typeof p.id !== "string" || typeof p.kind !== "string") throw new MeasuredSpecsError(`measured specs: story ${s.id} has a part without id or kind`);
3374
+ }
3375
+ if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
3376
+ });
3377
+ if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
3378
+ if (data.tokens !== void 0) {
3379
+ const t = data.tokens;
3380
+ if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
3381
+ for (const p of t.properties) if (!isObj$1(p) || typeof p.name !== "string" || typeof p.path !== "string" || p.tier !== "primitive" && p.tier !== "semantic") throw new MeasuredSpecsError("measured specs: a token has no name, path or tier");
3382
+ }
3383
+ return data;
3384
+ }
3385
+ const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
3386
+ const XYZ_D65_TO_LINEAR_SRGB = [
3387
+ [3.2409699419045226, -1.537383177570094, -0.4986107602930034],
3388
+ [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
3389
+ [0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
3390
+ ];
3391
+ const D50_TO_D65 = [
3392
+ [0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
3393
+ [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
3394
+ [0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
3395
+ ];
3396
+ const LINEAR_P3_TO_XYZ_D65 = [
3397
+ [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
3398
+ [0.2289745640697488, 0.6917385218365064, 0.079286914093745],
3399
+ [0, 0.04511338185890264, 1.043944368900976]
3400
+ ];
3401
+ const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
3402
+ const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
3403
+ const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
3404
+ function oklabToLinearSrgb(L, a, b) {
3405
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
3406
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
3407
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
3408
+ return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
3409
+ }
3410
+ function labToXyzD50(L, a, b) {
3411
+ const e = 216 / 24389;
3412
+ const k = 24389 / 27;
3413
+ const fy = (L + 16) / 116;
3414
+ const fx = a / 500 + fy;
3415
+ const fz = fy - b / 200;
3416
+ const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
3417
+ const y = L > k * e ? fy ** 3 : L / k;
3418
+ const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
3419
+ return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
3420
+ }
3421
+ function num(s, full = 1) {
3422
+ if (s === void 0 || s === "none") return 0;
3423
+ if (s.endsWith("%")) return parseFloat(s) / 100 * full;
3424
+ if (/deg$/.test(s)) return parseFloat(s);
3425
+ if (/turn$/.test(s)) return parseFloat(s) * 360;
3426
+ if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
3427
+ return parseFloat(s);
3428
+ }
3429
+ function linearSrgb(fn, args) {
3430
+ switch (fn) {
3431
+ case "oklab":
3432
+ return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
3433
+ case "oklch": {
3434
+ const c = num(args[1], 0.4);
3435
+ const h = num(args[2]) * Math.PI / 180;
3436
+ return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
3437
+ }
3438
+ case "lab":
3439
+ return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
3440
+ case "lch": {
3441
+ const c = num(args[1], 150);
3442
+ const h = num(args[2]) * Math.PI / 180;
3443
+ return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
3444
+ }
3445
+ case "color": {
3446
+ const [space, ...c] = args;
3447
+ const v = [num(c[0]), num(c[1]), num(c[2])];
3448
+ if (space === "srgb") return v.map(toLinear);
3449
+ if (space === "srgb-linear") return v;
3450
+ if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
3451
+ if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
3452
+ if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
3453
+ return void 0;
3454
+ }
3056
3455
  }
3057
- const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
3058
- const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
3059
- const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
3060
- const d = lower2(owners.design);
3061
- const t = lower2(owners.tech);
3062
- const a = lower2(approvers);
3063
- let design;
3064
- let tech;
3065
- if (d.length || t.length) {
3066
- design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
3067
- const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
3068
- const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
3069
- tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
3070
- } else {
3071
- design = a.length >= 1;
3072
- tech = a.length >= 2;
3456
+ return void 0;
3457
+ }
3458
+ const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
3459
+ function cssColorToRgb(fn, inner) {
3460
+ const [main, alpha] = inner.split("/");
3461
+ const args = main.trim().split(/[\s,]+/).filter(Boolean);
3462
+ const lin = linearSrgb(fn.toLowerCase(), args);
3463
+ if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
3464
+ const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
3465
+ const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
3466
+ return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
3467
+ }
3468
+ function cssColorsToRgb(text) {
3469
+ if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
3470
+ return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
3471
+ }
3472
+ function withSrgbColors(value) {
3473
+ if (typeof value === "string") return cssColorsToRgb(value);
3474
+ if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
3475
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
3476
+ return value;
3477
+ }
3478
+ function parseBoxShadow(css) {
3479
+ if (!css || css.trim() === "none") return [];
3480
+ const out = [];
3481
+ for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
3482
+ const color = /rgba?\(([^)]*)\)/i.exec(one);
3483
+ const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
3484
+ const a = c[3] ?? 1;
3485
+ const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
3486
+ if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
3487
+ out.push({ inset: /\binset\b/i.test(one), x: lengths[0], y: lengths[1], blur: lengths[2] ?? 0, spread: lengths[3] ?? 0, color: { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a } });
3073
3488
  }
3074
- return { approvers, changesRequested, design, tech };
3489
+ return out;
3075
3490
  }
3076
- const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
3077
- function canOverride(login, policy) {
3078
- if (!login || !policy?.allowed.length) return false;
3079
- return policy.allowed.map(normLogin).includes(normLogin(login));
3491
+ const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
3492
+ const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
3493
+ const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
3494
+ function rawIssue(label, r, values2) {
3495
+ const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
3496
+ const todo = !matches.length ? "No variable has that value: bind it to one so it follows the library, or create the variable and send it with the variables." : matches.length === 1 ? `${matches[0]} has that value: bind it so it follows the library.` : `${and(matches)} have that value: bind one so it follows the library.`;
3497
+ return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
3080
3498
  }
3081
- function mergeGate(input) {
3082
- const blocking = [];
3083
- const missing = [];
3084
- const approvalReasons = [];
3085
- const lifecycle = pullState(input);
3086
- if (lifecycle === "merged") blocking.push("This branch is already merged.");
3087
- else if (lifecycle === "closed") blocking.push("This branch is closed.");
3088
- if (input.exploration && lifecycle !== "merged" && lifecycle !== "closed") blocking.push("It is an exploration: promote it to review first.");
3089
- if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
3090
- if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
3091
- if (input.ci.state === "pending") blocking.push("Tests are still running.");
3092
- if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
3093
- const tier = input.tier ?? 3;
3094
- if (tier >= 2) {
3095
- if (!input.approvals.design) {
3096
- approvalReasons.push("It needs a design owner approval.");
3097
- missing.push("design approval");
3499
+ function planComponentPush(d, ctx) {
3500
+ const issues = [...d.unknown];
3501
+ const reasons = [];
3502
+ const edits = /* @__PURE__ */ new Map();
3503
+ const also = /* @__PURE__ */ new Set();
3504
+ for (const r of d.rebinds) {
3505
+ const label = `${cap(r.kind)} in ${where(r)}`;
3506
+ if (r.raw?.length) {
3507
+ issues.push(rawIssue(label, r, r.raw));
3508
+ continue;
3098
3509
  }
3099
- if (!input.approvals.tech) {
3100
- approvalReasons.push("It needs a tech owner approval.");
3101
- missing.push("tech approval");
3510
+ if (!r.after.length) {
3511
+ issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and send it with the variables, then scan again." });
3512
+ continue;
3102
3513
  }
3103
- } else if (!input.approvals.approvers.length) {
3104
- approvalReasons.push("It needs one approval.");
3105
- missing.push("approval");
3514
+ const missing = r.after.filter((p) => !ctx.tierOf(p));
3515
+ if (missing.length) {
3516
+ issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Send the variable first (Variables, above), then scan again." });
3517
+ continue;
3518
+ }
3519
+ if (!r.before.length) {
3520
+ reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
3521
+ continue;
3522
+ }
3523
+ if (r.before.length !== 1 || r.after.length !== 1) {
3524
+ reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
3525
+ continue;
3526
+ }
3527
+ const [from, to] = [r.before[0], r.after[0]];
3528
+ if (ctx.tierOf(from) !== "component") {
3529
+ reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
3530
+ continue;
3531
+ }
3532
+ const elsewhere = ctx.usedElsewhere(from, r);
3533
+ if (elsewhere.variants.length) {
3534
+ const v = elsewhere.variants;
3535
+ const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
3536
+ reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
3537
+ continue;
3538
+ }
3539
+ for (const c of elsewhere.components) also.add(c);
3540
+ const prev = edits.get(from);
3541
+ if (prev && prev.value !== `{${to}}`) {
3542
+ reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
3543
+ edits.delete(from);
3544
+ continue;
3545
+ }
3546
+ edits.set(from, { op: "set", path: from, value: `{${to}}` });
3106
3547
  }
3107
- const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
3108
- const shipper = !input.viewer || canShip(input.viewer, input.ship);
3109
- const ship = !shipper && lifecycle !== "merged" && lifecycle !== "closed" ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
3110
- const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
3111
- const allowed = reasons.length === 0;
3112
- const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
3113
- return { allowed, reasons, overridable, missing, canShip: shipper };
3114
- }
3115
- const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
3116
- const OVERRIDE_HEADING = "**Bridge owner override**";
3117
- function overrideReasonProblem(reason, policy) {
3118
- if (!policy.requireReason) return null;
3119
- const n = reason.trim().length;
3120
- if (n === 0) return `Write why this merges without the missing approvals, in at least ${policy.minReasonLength} characters.`;
3121
- if (n < policy.minReasonLength) return `Write at least ${policy.minReasonLength} characters (${n} so far), so the history says why.`;
3122
- return null;
3548
+ const list = [...edits.values()];
3549
+ if (d.needsCode || reasons.length) {
3550
+ return {
3551
+ route: "claude-draft",
3552
+ said: `A developer builds it with Claude from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
3553
+ edits: list,
3554
+ reasons,
3555
+ issues,
3556
+ alsoChanges: [...also]
3557
+ };
3558
+ }
3559
+ if (list.length) {
3560
+ const n = list.length;
3561
+ const others = [...also];
3562
+ const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
3563
+ const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
3564
+ return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
3565
+ }
3566
+ return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
3123
3567
  }
3124
- function overrideComment(login, reason, missing) {
3125
- const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
3126
- return `${OVERRIDE_HEADING}
3127
-
3128
- Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
3568
+ function specsUsage(specs, component) {
3569
+ const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
3570
+ return (path, r) => {
3571
+ const variants = [];
3572
+ const components = [];
3573
+ for (const c of specs.components) {
3574
+ for (const v of c.variants) {
3575
+ if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
3576
+ const key = variantKeyOf(v.props);
3577
+ if (c === own) {
3578
+ if (!r.variants.includes(key)) variants.push(key);
3579
+ } else if (!components.includes(c.name)) components.push(c.name);
3580
+ }
3581
+ }
3582
+ return { variants, components };
3583
+ };
3129
3584
  }
3130
- function overrideRefusedComment(body, message) {
3131
- return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
3585
+ function componentContrastPairs(edits, pairs2, tokens) {
3586
+ const out = /* @__PURE__ */ new Map();
3587
+ for (const e of edits) {
3588
+ const target = tokens.aliasOf(e.path);
3589
+ if (!target) continue;
3590
+ const group = e.path.split(".")[0];
3591
+ const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
3592
+ for (const p of pairs2) {
3593
+ if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
3594
+ if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
3595
+ }
3596
+ }
3597
+ return [...out.values()];
3598
+ }
3599
+ const FROM_FIGMA_LABEL = "from-figma";
3600
+ const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
3601
+ const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
3602
+ const FIGMA_SPEC_DIR = ".bridge/figma";
3603
+ const ORIGIN_MARK = "bridge-from-figma";
3604
+ const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
3605
+ function parseFigmaOrigin(body) {
3606
+ if (!body) return void 0;
3607
+ const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
3608
+ if (!m) return void 0;
3609
+ try {
3610
+ const o = JSON.parse(m[1]);
3611
+ return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
3612
+ } catch {
3613
+ return void 0;
3614
+ }
3615
+ }
3616
+ function figmaOriginSection(o) {
3617
+ const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
3618
+ const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
3619
+ const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted with Claude** from the Figma spec. A person reviews it before it ships." : `**Waiting for a developer:** this branch carries the spec only, as a request. A developer builds the code here with Claude (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
3620
+ return [`**From Figma:** ${design}${who}. Once it ships, a pull in the Bridge Figma plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
3621
+ }
3622
+ function withFigmaOrigin(body, o) {
3623
+ let b = (body ?? "").trim();
3624
+ const at = b.indexOf("**From Figma:**");
3625
+ const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
3626
+ if (at >= 0 && mark > at) {
3627
+ const end = b.indexOf("-->", mark);
3628
+ b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
3629
+ }
3630
+ return `${figmaOriginSection(o)}${b ? `
3631
+
3632
+ ${b}` : ""}`;
3633
+ }
3634
+ const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
3635
+ const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
3636
+ const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
3637
+ function formatFigmaSpec(d, plan) {
3638
+ return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
3639
+ `;
3640
+ }
3641
+ function parseFigmaSpec(text) {
3642
+ if (!text) return void 0;
3643
+ try {
3644
+ const s = JSON.parse(text);
3645
+ return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
3646
+ } catch {
3647
+ return void 0;
3648
+ }
3649
+ }
3650
+ function figmaDraftBrief(d, ctx) {
3651
+ const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
3652
+ const suggested = "suggestion" in ctx;
3653
+ const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
3654
+ const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
3655
+ return [
3656
+ suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
3657
+ "",
3658
+ d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
3659
+ `Kind: ${kindLabel(d.kind)}`,
3660
+ `Change: ${d.title}`,
3661
+ ...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
3662
+ "",
3663
+ d.summary.trim(),
3664
+ "",
3665
+ ...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
3666
+ ...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
3667
+ ...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
3668
+ ...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
3669
+ ...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
3670
+ `Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
3671
+ ...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
3672
+ ...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
3673
+ ...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
3674
+ "",
3675
+ "Steps:",
3676
+ "suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
3677
+ `2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
3678
+ "3. Run npm run specs so component-specs.json matches the code, then npm test.",
3679
+ "suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The pull request is then ready for review, labelled ${DRAFTED_BY_CLAUDE_LABEL}, and a person reviews it.`,
3680
+ "",
3681
+ "Rules:",
3682
+ "- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
3683
+ "- Keep every existing variant and prop unchanged unless the spec changes it.",
3684
+ `- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
3685
+ ].join("\n");
3686
+ }
3687
+ function figmaDraftBody(d, plan, o) {
3688
+ const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
3689
+ const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
3690
+ return withFigmaOrigin(
3691
+ [
3692
+ `## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
3693
+ "",
3694
+ d.summary.trim(),
3695
+ "",
3696
+ ...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
3697
+ ...rows,
3698
+ ...d.images?.length ? [...d.images.map((u, i) => `![${d.component} in Figma ${i + 1}](${u})`), ""] : [],
3699
+ ...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
3700
+ ...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
3701
+ ...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
3702
+ `**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
3703
+ ].join("\n"),
3704
+ o
3705
+ );
3706
+ }
3707
+ const figmaBranchTitle = (d) => requestIssueTitle(d);
3708
+ function figmaPushStatus(x) {
3709
+ const labels = x.labels ?? [];
3710
+ if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
3711
+ if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
3712
+ if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for a developer", sentence: "A developer builds it with Claude from its spec, then a person reviews it.", rebuild: false };
3713
+ if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted with Claude", sentence: "The code is drafted. A person reviews it before it ships.", rebuild: false };
3714
+ return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
3715
+ }
3716
+ function parseTier(labels, body) {
3717
+ const found = [];
3718
+ for (const l of labels) {
3719
+ const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
3720
+ if (m) found.push(Number(m[1]));
3721
+ }
3722
+ for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
3723
+ for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
3724
+ return found.length ? Math.max(...found) : null;
3725
+ }
3726
+ function claudeStage(labels) {
3727
+ const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
3728
+ if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
3729
+ if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
3730
+ return null;
3731
+ }
3732
+ const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
3733
+ function pullState(p) {
3734
+ if (p.merged_at || p.merged) return "merged";
3735
+ if (p.state && p.state !== "open") return "closed";
3736
+ if (isDraft(p)) return "draft";
3737
+ return "open";
3738
+ }
3739
+ const PULL_STATE_PILL = {
3740
+ merged: { label: "Merged", cls: "" },
3741
+ closed: { label: "Closed", cls: "" },
3742
+ draft: { label: "Draft", cls: "" },
3743
+ open: { label: "Open", cls: "" }
3744
+ };
3745
+ function openPulls(pulls) {
3746
+ return pulls.filter((p) => {
3747
+ const s = pullState(p);
3748
+ return s === "open" || s === "draft";
3749
+ });
3750
+ }
3751
+ function isPlatformRefusal(s) {
3752
+ return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
3753
+ }
3754
+ const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
3755
+ function summarizeCi(runs, statuses = []) {
3756
+ const failed = [];
3757
+ const pending = [];
3758
+ let passed = 0;
3759
+ for (const r of runs) {
3760
+ if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
3761
+ else if (r.conclusion && OK.has(r.conclusion)) passed++;
3762
+ else failed.push({ name: r.name, url: r.html_url });
3763
+ }
3764
+ const seen = /* @__PURE__ */ new Set();
3765
+ for (const s of statuses) {
3766
+ if (seen.has(s.context)) continue;
3767
+ seen.add(s.context);
3768
+ if (isPlatformRefusal(s)) continue;
3769
+ if (s.state === "success") passed++;
3770
+ else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
3771
+ else failed.push({ name: s.context, url: s.target_url ?? void 0 });
3772
+ }
3773
+ const total = passed + failed.length + pending.length;
3774
+ const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
3775
+ return { state, total, passed, failed, pending };
3776
+ }
3777
+ const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
3778
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
3779
+ const latest = /* @__PURE__ */ new Map();
3780
+ const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
3781
+ for (const r of sorted) {
3782
+ const login = r.user?.login;
3783
+ if (!login || login === author) continue;
3784
+ if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
3785
+ }
3786
+ const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
3787
+ const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
3788
+ const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
3789
+ const d = lower2(owners.design);
3790
+ const t = lower2(owners.tech);
3791
+ const a = lower2(approvers);
3792
+ let design;
3793
+ let tech;
3794
+ const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
3795
+ if (d.length || t.length) {
3796
+ design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
3797
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
3798
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
3799
+ tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
3800
+ } else {
3801
+ design = a.length >= 1;
3802
+ tech = a.length >= 2;
3803
+ }
3804
+ return { approvers, changesRequested, design, tech, owner };
3805
+ }
3806
+ function lookAgainReviewers(reviews, headSha, author) {
3807
+ if (!headSha) return [];
3808
+ const latest = /* @__PURE__ */ new Map();
3809
+ const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
3810
+ for (const r of sorted) {
3811
+ const login = r.user?.login;
3812
+ if (!login || login === author) continue;
3813
+ if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
3814
+ }
3815
+ const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
3816
+ if (!asked.length) return [];
3817
+ return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
3818
+ }
3819
+ function withSuggestionApprovals(pr, suggestion, prTier) {
3820
+ if (!suggestion) return pr;
3821
+ const s = suggestion.approvals;
3822
+ if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
3823
+ const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
3824
+ if (!covered) return pr;
3825
+ const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
3826
+ return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
3827
+ }
3828
+ function withFigmaPushApprovals(pr, push) {
3829
+ if (!push || push.scope === "outside") return pr;
3830
+ const tech = pr.tech || push.scope === "token-values";
3831
+ return { ...pr, design: true, tech, owner: true };
3832
+ }
3833
+ function withTokenValueApprovals(pr, valuesOnly) {
3834
+ if (!valuesOnly) return pr;
3835
+ return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
3836
+ }
3837
+ const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
3838
+ function canOverride(login, policy) {
3839
+ if (!login || !policy?.allowed.length) return false;
3840
+ return policy.allowed.map(normLogin).includes(normLogin(login));
3841
+ }
3842
+ function overrideHolders(policy, owners) {
3843
+ const allowed = policy?.allowed ?? [];
3844
+ const all = [...owners.design, ...owners.tech];
3845
+ if (!all.length || all.some((o) => o.includes("/"))) return allowed;
3846
+ const named = all.map(normLogin);
3847
+ return allowed.filter((l) => named.includes(normLogin(l)));
3848
+ }
3849
+ const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
3850
+ function mergeGate(input) {
3851
+ const blocking = [];
3852
+ const missing = [];
3853
+ const approvalReasons = [];
3854
+ const lifecycle = pullState(input);
3855
+ if (lifecycle === "merged" || lifecycle === "closed") {
3856
+ return { allowed: false, reasons: [lifecycle === "merged" ? "This branch is already merged." : "This branch is closed."], overridable: false, missing: [], canShip: !input.viewer || canShip(input.viewer, input.ship) };
3857
+ }
3858
+ if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
3859
+ if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
3860
+ if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
3861
+ if (input.ci.state === "pending") blocking.push("Tests are still running.");
3862
+ if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
3863
+ const tier = input.tier ?? 3;
3864
+ if (tier >= 2) {
3865
+ if (!input.approvals.design) {
3866
+ approvalReasons.push("It needs a design owner approval.");
3867
+ missing.push("design approval");
3868
+ }
3869
+ if (!input.approvals.tech && !input.approvals.valuesOnly) {
3870
+ approvalReasons.push("It needs a tech owner approval.");
3871
+ missing.push("tech approval");
3872
+ }
3873
+ } else if (!hasOwnerApproval(input.approvals)) {
3874
+ approvalReasons.push("It needs one owner approval.");
3875
+ missing.push("approval");
3876
+ }
3877
+ const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
3878
+ const shipper = !input.viewer || canShip(input.viewer, input.ship);
3879
+ const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
3880
+ const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
3881
+ const allowed = reasons.length === 0;
3882
+ const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
3883
+ return { allowed, reasons, overridable, missing, canShip: shipper };
3884
+ }
3885
+ function branchVerdict(input) {
3886
+ const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
3887
+ const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
3888
+ const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
3889
+ const override = ownerOverridePolicy(input.override, input.owners);
3890
+ const state = input.state ?? "open";
3891
+ const draft = isDraft(input);
3892
+ const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
3893
+ 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;
3894
+ const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
3895
+ const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
3896
+ const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
3897
+ const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
3898
+ return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
3899
+ }
3900
+ const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
3901
+ const OVERRIDE_HEADING = "**Bridge owner override**";
3902
+ function overrideReasonMin(policy) {
3903
+ return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
3904
+ }
3905
+ function overrideReasonProblem(reason, policy) {
3906
+ const min = overrideReasonMin(policy);
3907
+ const n = reason.trim().length;
3908
+ if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
3909
+ if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
3910
+ return null;
3911
+ }
3912
+ function overrideComment(login, reason, missing) {
3913
+ const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
3914
+ return `${OVERRIDE_HEADING}
3915
+
3916
+ Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
3917
+ }
3918
+ function overrideRefusedComment(body, message) {
3919
+ return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
3132
3920
  }
3133
3921
  function parseOverrideComment(body) {
3134
3922
  const text = (body ?? "").trim();
@@ -3177,9 +3965,9 @@ function reviewStatusKind(input) {
3177
3965
  if (tier >= 2) {
3178
3966
  if (!input.approvals.design) return out("needs-design");
3179
3967
  if (!input.approvals.tech) return out("needs-tech");
3180
- } else if (!input.approvals.approvers.length) return out("needs-approval");
3968
+ } else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
3181
3969
  if (input.mergeable === null) return out("checking");
3182
- const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: Boolean(input.draft), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
3970
+ const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: isDraft(input), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
3183
3971
  return gate.allowed ? out("ready") : out("unknown");
3184
3972
  }
3185
3973
  const REVIEW_STATUS_LABEL = {
@@ -3197,6 +3985,10 @@ const REVIEW_STATUS_LABEL = {
3197
3985
  ready: "Ready to merge",
3198
3986
  unknown: "Status unknown"
3199
3987
  };
3988
+ const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
3989
+ function reviewStatusLabel(kind, lookAgain) {
3990
+ return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
3991
+ }
3200
3992
  const CI_STATE_LABEL = {
3201
3993
  pass: "Tests pass",
3202
3994
  fail: "Tests fail",
@@ -3234,8 +4026,8 @@ function reopenEligibility(x) {
3234
4026
  }
3235
4027
  const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
3236
4028
  const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
3237
- function requiredApprovals(tier) {
3238
- return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
4029
+ function requiredApprovals(tier, valuesOnly) {
4030
+ return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false, ...valuesOnly ? { valuesOnly: true } : {} }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
3239
4031
  }
3240
4032
  function pendingApprovals(tier, approvals) {
3241
4033
  if (tier == null || !approvals) return [];
@@ -3323,6 +4115,46 @@ function parseSuggestionAppliedComment(body) {
3323
4115
  return null;
3324
4116
  }
3325
4117
  }
4118
+ const SUGGESTION_OVERRIDE_MARK = "bridge-suggestion-override";
4119
+ function composeSuggestionOverrideComment(r) {
4120
+ const audit = overrideComment(r.login, r.reason, r.missing).replace(/ Merged from Bridge\.$/, " Approved as owner from Bridge.");
4121
+ const record = { payloadSha: r.payloadSha, login: r.login.replace(/^@/, "").toLowerCase(), missing: r.missing };
4122
+ return [audit, "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_OVERRIDE_MARK} ${JSON.stringify(record)} -->`].join("\n");
4123
+ }
4124
+ function parseSuggestionOverrideComment(body) {
4125
+ const text = (body ?? "").trim();
4126
+ if (!text.startsWith(OVERRIDE_HEADING)) return null;
4127
+ const m = new RegExp(`<!-- ${SUGGESTION_OVERRIDE_MARK} (\\{[^\\n]*?\\}) -->`).exec(text);
4128
+ if (!m) return null;
4129
+ try {
4130
+ const r = JSON.parse(m[1]);
4131
+ if (typeof r.payloadSha !== "string" || typeof r.login !== "string") return null;
4132
+ const why = /Owner override by @[\w-]+: ([\s\S]*?)\. Missing:/.exec(text);
4133
+ return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
4134
+ } catch {
4135
+ return null;
4136
+ }
4137
+ }
4138
+ function suggestionOverrideProblem(x) {
4139
+ const lists = approverLists(x.gov.owners, x.gov.rights ?? null, x.gov.teams);
4140
+ const holders = overrideHolders(x.gov.override, lists).map(norm);
4141
+ if (!x.viewer || !holders.includes(norm(x.viewer))) return "Only an owner the review rules allow to use the owner override can approve as owner.";
4142
+ if (norm(x.viewer) !== norm(x.author)) return "The owner override is for your own suggestion: approve this one the usual way.";
4143
+ const others = eligibleReviewers("approval", { owners: lists, author: x.author });
4144
+ if (others.open || others.logins.length) return "Someone else can review it: ask them instead.";
4145
+ return null;
4146
+ }
4147
+ function suggestionOverrideOf(comments, gov, opts) {
4148
+ if (!gov.override || !opts.author) return null;
4149
+ for (const c of [...comments].reverse()) {
4150
+ const r = parseSuggestionOverrideComment(c.body);
4151
+ if (!r || r.payloadSha !== opts.payloadSha || !c.author || norm(c.author) !== norm(r.login)) continue;
4152
+ if (suggestionOverrideProblem({ viewer: c.author, author: opts.author, gov })) continue;
4153
+ if (overrideReasonProblem(r.reason, gov.override)) continue;
4154
+ return r;
4155
+ }
4156
+ return null;
4157
+ }
3326
4158
  const norm = (l) => l.trim().replace(/^@/, "").toLowerCase();
3327
4159
  const COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
3328
4160
  function summarizeSuggestionApprovals(comments, gov, opts) {
@@ -3352,229 +4184,16 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
3352
4184
  latestOld.delete(c.author);
3353
4185
  reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
3354
4186
  });
3355
- const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
4187
+ const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
3356
4188
  const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
3357
- return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
4189
+ const out = { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
4190
+ const override = summary.changesRequested.length ? null : suggestionOverrideOf(comments, gov, { payloadSha: opts.payloadSha, author: opts.author });
4191
+ return override ? { ...out, design: true, tech: true, owner: true, override } : out;
3358
4192
  }
3359
4193
  function suggestionReviewState(tier, approvals) {
3360
4194
  if (approvals.changesRequested.length) return "changes-requested";
3361
4195
  return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
3362
4196
  }
3363
- const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
3364
- const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
3365
- function planComponentPush(d, ctx) {
3366
- const issues = [...d.unknown];
3367
- const reasons = [];
3368
- const edits = /* @__PURE__ */ new Map();
3369
- const also = /* @__PURE__ */ new Set();
3370
- for (const r of d.rebinds) {
3371
- const label = `${cap(r.kind)} in ${where(r)}`;
3372
- if (!r.after.length) {
3373
- issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and push it with the variables, then push again." });
3374
- continue;
3375
- }
3376
- const missing = r.after.filter((p) => !ctx.tierOf(p));
3377
- if (missing.length) {
3378
- issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Push the variable first (Variables, above), then push this component again." });
3379
- continue;
3380
- }
3381
- if (!r.before.length) {
3382
- reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: Claude AI adds it to the CSS.`);
3383
- continue;
3384
- }
3385
- if (r.before.length !== 1 || r.after.length !== 1) {
3386
- reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so Claude AI writes the CSS.`);
3387
- continue;
3388
- }
3389
- const [from, to] = [r.before[0], r.after[0]];
3390
- if (ctx.tierOf(from) !== "component") {
3391
- reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: Claude AI changes the CSS to ${to}.`);
3392
- continue;
3393
- }
3394
- const elsewhere = ctx.usedElsewhere(from, r);
3395
- if (elsewhere.variants.length) {
3396
- const v = elsewhere.variants;
3397
- const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
3398
- reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so Claude AI adds a token for ${where(r)}.`);
3399
- continue;
3400
- }
3401
- for (const c of elsewhere.components) also.add(c);
3402
- const prev = edits.get(from);
3403
- if (prev && prev.value !== `{${to}}`) {
3404
- reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): Claude AI splits it.`);
3405
- edits.delete(from);
3406
- continue;
3407
- }
3408
- edits.set(from, { op: "set", path: from, value: `{${to}}` });
3409
- }
3410
- const list = [...edits.values()];
3411
- if (d.needsCode || reasons.length) {
3412
- return {
3413
- route: "claude-draft",
3414
- said: `Claude AI drafts the code from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
3415
- edits: list,
3416
- reasons,
3417
- issues,
3418
- alsoChanges: [...also]
3419
- };
3420
- }
3421
- if (list.length) {
3422
- const n = list.length;
3423
- const others = [...also];
3424
- const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
3425
- const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
3426
- return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
3427
- }
3428
- return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
3429
- }
3430
- function specsUsage(specs, component) {
3431
- const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
3432
- return (path, r) => {
3433
- const variants = [];
3434
- const components = [];
3435
- for (const c of specs.components) {
3436
- for (const v of c.variants) {
3437
- if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
3438
- const key = variantKeyOf(v.props);
3439
- if (c === own) {
3440
- if (!r.variants.includes(key)) variants.push(key);
3441
- } else if (!components.includes(c.name)) components.push(c.name);
3442
- }
3443
- }
3444
- return { variants, components };
3445
- };
3446
- }
3447
- function componentContrastPairs(edits, pairs, tokens) {
3448
- const out = /* @__PURE__ */ new Map();
3449
- for (const e of edits) {
3450
- const target = tokens.aliasOf(e.path);
3451
- if (!target) continue;
3452
- const group = e.path.split(".")[0];
3453
- const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
3454
- for (const p of pairs) {
3455
- if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
3456
- if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
3457
- }
3458
- }
3459
- return [...out.values()];
3460
- }
3461
- const FROM_FIGMA_LABEL = "from-figma";
3462
- const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
3463
- const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
3464
- const FIGMA_SPEC_DIR = ".bridge/figma";
3465
- const ORIGIN_MARK = "bridge-from-figma";
3466
- const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
3467
- function parseFigmaOrigin(body) {
3468
- if (!body) return void 0;
3469
- const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
3470
- if (!m) return void 0;
3471
- try {
3472
- const o = JSON.parse(m[1]);
3473
- return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
3474
- } catch {
3475
- return void 0;
3476
- }
3477
- }
3478
- function figmaOriginSection(o) {
3479
- const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
3480
- const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
3481
- const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted by Claude AI** from the Figma spec. A person reviews it before it ships." : `**Waiting for Claude AI:** this branch carries the spec only, as a request. Claude writes the code here (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
3482
- return [`**From Figma:** ${design}${who}. Once it ships, a pull in the DS Bridge plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
3483
- }
3484
- function withFigmaOrigin(body, o) {
3485
- let b = (body ?? "").trim();
3486
- const at = b.indexOf("**From Figma:**");
3487
- const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
3488
- if (at >= 0 && mark > at) {
3489
- const end = b.indexOf("-->", mark);
3490
- b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
3491
- }
3492
- return `${figmaOriginSection(o)}${b ? `
3493
-
3494
- ${b}` : ""}`;
3495
- }
3496
- const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
3497
- const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
3498
- const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
3499
- function formatFigmaSpec(d, plan) {
3500
- return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
3501
- `;
3502
- }
3503
- function parseFigmaSpec(text) {
3504
- if (!text) return void 0;
3505
- try {
3506
- const s = JSON.parse(text);
3507
- return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
3508
- } catch {
3509
- return void 0;
3510
- }
3511
- }
3512
- function figmaDraftBrief(d, ctx) {
3513
- const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
3514
- const suggested = "suggestion" in ctx;
3515
- const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
3516
- const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
3517
- return [
3518
- suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
3519
- "",
3520
- d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
3521
- `Kind: ${kindLabel(d.kind)}`,
3522
- `Change: ${d.title}`,
3523
- ...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
3524
- "",
3525
- d.summary.trim(),
3526
- "",
3527
- ...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
3528
- ...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
3529
- ...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
3530
- ...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
3531
- ...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
3532
- `Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
3533
- ...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
3534
- ...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
3535
- ...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
3536
- "",
3537
- "Steps:",
3538
- "suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
3539
- `2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
3540
- "3. Run npm run specs so component-specs.json matches the code, then npm test.",
3541
- "suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The branch is then labelled ${DRAFTED_BY_CLAUDE_LABEL} and a person reviews it.`,
3542
- "",
3543
- "Rules:",
3544
- "- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
3545
- "- Keep every existing variant and prop unchanged unless the spec changes it.",
3546
- `- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
3547
- ].join("\n");
3548
- }
3549
- function figmaDraftBody(d, plan, o) {
3550
- const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
3551
- const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
3552
- return withFigmaOrigin(
3553
- [
3554
- `## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
3555
- "",
3556
- d.summary.trim(),
3557
- "",
3558
- ...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
3559
- ...rows,
3560
- ...d.images?.length ? [...d.images.map((u, i) => `![${d.component} in Figma ${i + 1}](${u})`), ""] : [],
3561
- ...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
3562
- ...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
3563
- ...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
3564
- `**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
3565
- ].join("\n"),
3566
- o
3567
- );
3568
- }
3569
- const figmaBranchTitle = (d) => requestIssueTitle(d);
3570
- function figmaPushStatus(x) {
3571
- const labels = x.labels ?? [];
3572
- if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
3573
- if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
3574
- if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for Claude AI", sentence: "The spec is on the library branch. Claude AI writes the code there, then a person reviews it.", rebuild: false };
3575
- if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted by Claude AI", sentence: "Claude AI drafted the code. A person reviews it before it ships.", rebuild: false };
3576
- return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
3577
- }
3578
4197
  const SUGGESTION_SCHEMA = "bridge.suggestion/1";
3579
4198
  const SUGGESTION_LABEL = "bridge-suggestion";
3580
4199
  const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
@@ -3836,6 +4455,11 @@ function suggestionKindFromLabels(labels) {
3836
4455
  function suggestionStateFromLabels(labels) {
3837
4456
  return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
3838
4457
  }
4458
+ function suggestionIsTokenValues(p) {
4459
+ if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
4460
+ if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
4461
+ return (p.files ?? []).every((f) => f.path.endsWith(".md"));
4462
+ }
3839
4463
  function suggestionStatus(i) {
3840
4464
  const prs = i.linkedPrs ?? [];
3841
4465
  if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
@@ -3906,7 +4530,7 @@ function checksSection(p, max) {
3906
4530
  "",
3907
4531
  `- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
3908
4532
  `- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
3909
- `- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one approval" : "design and tech approval"}`,
4533
+ `- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
3910
4534
  `- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
3911
4535
  "",
3912
4536
  ...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
@@ -4380,8 +5004,8 @@ async function planSuggestionApply(input) {
4380
5004
  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.`);
4381
5005
  if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
4382
5006
  }
4383
- const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
4384
- const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
5007
+ const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth, override: config.governance.override ?? null };
5008
+ const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
4385
5009
  const review = suggestionReviewState(payload.tier.tier, approvals);
4386
5010
  const missing = pendingApprovals(payload.tier.tier, approvals);
4387
5011
  const approved = review === "approved";
@@ -4458,16 +5082,16 @@ async function planSuggestionApply(input) {
4458
5082
  const issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
4459
5083
  const prevErrors = new Set(validateLayered(before).map(issueKey2));
4460
5084
  const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i))) : [];
4461
- let pairs = [];
5085
+ let pairs2 = [];
4462
5086
  if (config.contrast && tokenChanges.length) {
4463
5087
  try {
4464
- pairs = parseContrastChecks((await ws.read(config.contrast.path))?.text);
5088
+ pairs2 = parseContrastChecks((await ws.read(config.contrast.path))?.text);
4465
5089
  } catch {
4466
5090
  notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
4467
5091
  }
4468
5092
  }
4469
5093
  const modes = config.contrast?.modes;
4470
- const contrast = pairs.length ? contrastDelta(runLayeredContrast(before, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
5094
+ const contrast = pairs2.length ? contrastDelta(runLayeredContrast(before, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
4471
5095
  const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
4472
5096
  const otherFiles = [...writes.keys()].filter((p) => !tokenFileOf(config, p));
4473
5097
  const tier = classifyRisk({ tokenChanges: tokenChanges.map((c) => ({ kind: c.kind, path: c.path, ...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((x) => x === "description") } : {} })), files: otherFiles.map((path) => ({ path })), config });
@@ -4539,6 +5163,7 @@ function listItem(i, pulls) {
4539
5163
  title: i.title,
4540
5164
  url: i.url,
4541
5165
  author: i.author,
5166
+ requester: requesterOf(i.body),
4542
5167
  createdAt: i.createdAt,
4543
5168
  state: i.state,
4544
5169
  stateReason: i.stateReason,
@@ -4554,7 +5179,7 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
4554
5179
  const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
4555
5180
  const linkedPrs = linkedPullsOf(n, pulls);
4556
5181
  const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
4557
- const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
5182
+ const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author, valuesOnly: suggestionIsTokenValues(parsed.payload) }) : void 0;
4558
5183
  const status = suggestionStatus({
4559
5184
  state: issue.state,
4560
5185
  stateReason: issue.stateReason,
@@ -4565,6 +5190,19 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
4565
5190
  });
4566
5191
  return { issue, parsed, comments, approvals, linkedPrs, applied, status };
4567
5192
  }
5193
+ async function approvedSuggestionOnGitHub(gh, body, gov) {
5194
+ for (const n of closingIssueNumbers(body).slice(0, 3)) {
5195
+ const found = await approvedSuggestionNumber(gh, n, gov).catch(() => null);
5196
+ if (found) return found;
5197
+ }
5198
+ return null;
5199
+ }
5200
+ async function approvedSuggestionNumber(gh, n, gov) {
5201
+ const d = await getSuggestionOnGitHub(gh, n, gov, { pulls: [] });
5202
+ if (!d.parsed.ok || !d.approvals) return null;
5203
+ const tier = d.parsed.payload.tier.tier;
5204
+ return suggestionReviewState(tier, d.approvals) === "approved" ? { number: n, approvals: d.approvals, tier } : null;
5205
+ }
4568
5206
  async function syncSuggestionStateLabel(gh, n, labels, status) {
4569
5207
  const { add, remove } = suggestionLabelChanges(labels, status);
4570
5208
  let ok = true;
@@ -4662,6 +5300,27 @@ async function reviewSuggestionOnGitHub(gh, n, input, gov) {
4662
5300
  if (labelWarning) warnings.push(labelWarning);
4663
5301
  return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
4664
5302
  }
5303
+ async function overrideSuggestionOnGitHub(gh, n, input, gov) {
5304
+ if (!gov.override) throw new GitHubError(422, "The review rules allow no owner override.");
5305
+ const before = await getSuggestionOnGitHub(gh, n, gov);
5306
+ if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be approved: ${before.parsed.message}`);
5307
+ if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed.`);
5308
+ const sha = before.parsed.sha;
5309
+ if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and approve the new version.`);
5310
+ const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
5311
+ const problem = suggestionOverrideProblem({ viewer: by, author: before.issue.author, gov }) ?? overrideReasonProblem(input.reason, gov.override);
5312
+ if (problem) throw new GitHubError(422, problem);
5313
+ if (before.approvals?.changesRequested.length) throw new GitHubError(422, `Changes were asked for on #${n}: the owner override covers missing approvals only.`);
5314
+ 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");
5315
+ const posted = await gh.createIssueComment(n, composeSuggestionOverrideComment({ login: by, reason: input.reason, missing, payloadSha: sha }));
5316
+ const after = await getSuggestionOnGitHub(gh, n, gov);
5317
+ const counts = Boolean(after.approvals?.override);
5318
+ const warnings = [];
5319
+ if (!counts) warnings.push("The owner override is posted but does not count: check the owners and the override in bridge.config.json.");
5320
+ const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, after.status);
5321
+ if (labelWarning) warnings.push(labelWarning);
5322
+ return { commentUrl: posted.url, payloadSha: sha, status: after.status, counts, warnings };
5323
+ }
4665
5324
  async function linkSuggestionPrOnGitHub(gh, n, link) {
4666
5325
  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.");
4667
5326
  const issue = await gh.getIssue(n);
@@ -4689,6 +5348,11 @@ const DOCS_MCP_ORIGIN = {
4689
5348
  footer: 'Proposed with the Bridge MCP server on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
4690
5349
  label: "source: mcp"
4691
5350
  };
5351
+ const DOCS_CLI_ORIGIN = {
5352
+ where: "with the bridge CLI",
5353
+ footer: 'Proposed with the bridge CLI on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
5354
+ label: "source: cli"
5355
+ };
4692
5356
  const EM_DASH = /\u2014/;
4693
5357
  function validateDocs(d) {
4694
5358
  const out = [];
@@ -4821,7 +5485,7 @@ ${opts.notes.trim()}
4821
5485
  ].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
4822
5486
  return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
4823
5487
  }
4824
- const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : "studio";
5488
+ const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : origin.label === DOCS_CLI_ORIGIN.label ? "cli" : "studio";
4825
5489
  function docsSuggestionPayload(edits, opts) {
4826
5490
  const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
4827
5491
  const date = opts.date ?? /* @__PURE__ */ new Date();
@@ -4898,6 +5562,149 @@ async function commitFileOnGitHub(gh, input) {
4898
5562
  if (content === cur.text) return "";
4899
5563
  return gh.putFile({ path: input.path, content, branch: input.branch, message: input.message, sha: cur.sha });
4900
5564
  }
5565
+ class StorybookIndexError extends Error {
5566
+ constructor(code, message, status) {
5567
+ super(message);
5568
+ this.code = code;
5569
+ this.status = status;
5570
+ this.name = "StorybookIndexError";
5571
+ }
5572
+ code;
5573
+ status;
5574
+ }
5575
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5576
+ const str = (v) => typeof v === "string" && v.trim() ? v : void 0;
5577
+ function entryFrom(key, raw, v3) {
5578
+ if (!isObj(raw)) return null;
5579
+ const id = str(raw.id) ?? key;
5580
+ const title = str(raw.title) ?? str(raw.kind);
5581
+ const name = str(raw.name) ?? str(raw.story);
5582
+ if (!id || !title || !name) return null;
5583
+ const params = isObj(raw.parameters) ? raw.parameters : {};
5584
+ const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
5585
+ const e = {
5586
+ id,
5587
+ title,
5588
+ name,
5589
+ type,
5590
+ importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
5591
+ tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : []
5592
+ };
5593
+ const subtype = str(raw.subtype);
5594
+ if (subtype) e.subtype = subtype;
5595
+ const exportName = str(raw.exportName);
5596
+ if (exportName) e.exportName = exportName;
5597
+ const componentPath = str(raw.componentPath);
5598
+ if (componentPath) e.componentPath = componentPath;
5599
+ return e;
5600
+ }
5601
+ function groupStorybookComponents(entries) {
5602
+ const byTitle = /* @__PURE__ */ new Map();
5603
+ for (const e of entries) {
5604
+ let c = byTitle.get(e.title);
5605
+ if (!c) {
5606
+ const parts = e.title.split("/").map((p) => p.trim()).filter(Boolean);
5607
+ c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
5608
+ byTitle.set(e.title, c);
5609
+ }
5610
+ if (e.type === "docs") c.docs.push(e);
5611
+ else if (e.subtype !== "test") c.stories.push(e);
5612
+ for (const t of e.tags) if (!c.tags.includes(t)) c.tags.push(t);
5613
+ }
5614
+ const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
5615
+ for (const c of out) c.tags.sort();
5616
+ return out.sort((a, b) => a.title.localeCompare(b.title));
5617
+ }
5618
+ function parseStorybookIndex(json, source) {
5619
+ if (!isObj(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
5620
+ if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
5621
+ throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
5622
+ }
5623
+ const map = isObj(json.entries) ? json.entries : isObj(json.stories) ? json.stories : null;
5624
+ if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
5625
+ const v3 = !isObj(json.entries);
5626
+ const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
5627
+ if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
5628
+ const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
5629
+ return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
5630
+ }
5631
+ function normalizeStorybookUrl(input) {
5632
+ let u;
5633
+ try {
5634
+ u = new URL(input.trim());
5635
+ } catch {
5636
+ throw new StorybookIndexError("invalid-url", "Enter the full Storybook address, for example https://storybook.example.com");
5637
+ }
5638
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
5639
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
5640
+ throw new StorybookIndexError("invalid-url", "The Storybook address must start with https://");
5641
+ }
5642
+ if (u.username || u.password) throw new StorybookIndexError("invalid-url", "Remove the user name and password from the address.");
5643
+ const path = u.pathname.replace(/\/(index\.html|iframe\.html|index\.json|stories\.json)$/i, "/").replace(/\/+$/, "");
5644
+ return `${u.protocol}//${u.host}${path}`;
5645
+ }
5646
+ const storybookIndexUrls = (base) => {
5647
+ const b = base.replace(/\/+$/, "");
5648
+ return [
5649
+ { url: `${b}/index.json`, source: "index.json" },
5650
+ { url: `${b}/stories.json`, source: "stories.json" }
5651
+ ];
5652
+ };
5653
+ const pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
5654
+ function storybookIframeUrl(base, id, o = {}) {
5655
+ let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
5656
+ if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
5657
+ if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
5658
+ return url;
5659
+ }
5660
+ function storybookManagerUrl(base, id, viewMode = "story") {
5661
+ return `${base.replace(/\/+$/, "")}/?path=/${viewMode}/${encodeURIComponent(id)}`;
5662
+ }
5663
+ async function fetchStorybookIndex(base, o = {}) {
5664
+ const doFetch = o.fetch ?? ((url, init) => fetch(url, init));
5665
+ const rank = { private: 6, http: 5, network: 4, empty: 3, "not-json": 2, "not-found": 1, "invalid-url": 0 };
5666
+ let worst = null;
5667
+ const keep = (e) => {
5668
+ if (!worst || rank[e.code] > rank[worst.code]) worst = e;
5669
+ };
5670
+ for (const { url, source } of storybookIndexUrls(base)) {
5671
+ let res;
5672
+ try {
5673
+ res = await doFetch(url, { headers: { accept: "application/json" }, ...o.signal ? { signal: o.signal } : {} });
5674
+ } catch (err) {
5675
+ if (o.signal?.aborted) throw err;
5676
+ keep(new StorybookIndexError("network", `Could not reach ${url}. The host may be down or may not allow reading from another site.`));
5677
+ continue;
5678
+ }
5679
+ if (res.status === 404 || res.status === 410) {
5680
+ keep(new StorybookIndexError("not-found", `No Storybook index at ${base}: neither index.json nor stories.json was found.`, res.status));
5681
+ continue;
5682
+ }
5683
+ if (res.status === 401 || res.status === 403) {
5684
+ keep(new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.", res.status));
5685
+ continue;
5686
+ }
5687
+ if (!res.ok) {
5688
+ keep(new StorybookIndexError("http", `The Storybook host answered ${res.status} for ${url}.`, res.status));
5689
+ continue;
5690
+ }
5691
+ const text = await res.text();
5692
+ let json;
5693
+ try {
5694
+ json = JSON.parse(text);
5695
+ } catch {
5696
+ keep(new StorybookIndexError("not-json", `${url} is not a Storybook index (the host answered with a web page).`));
5697
+ continue;
5698
+ }
5699
+ try {
5700
+ return parseStorybookIndex(json, source);
5701
+ } catch (err) {
5702
+ if (err instanceof StorybookIndexError) keep(err);
5703
+ else throw err;
5704
+ }
5705
+ }
5706
+ throw worst ?? new StorybookIndexError("not-found", `No Storybook index at ${base}.`);
5707
+ }
4901
5708
  function normalizeRepoPath(path) {
4902
5709
  return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
4903
5710
  }
@@ -4944,274 +5751,323 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
4944
5751
  return out;
4945
5752
  }
4946
5753
  export {
4947
- figmaSpecPath as $,
5754
+ classifyLayered as $,
4948
5755
  explorationPrTitle as A,
4949
5756
  explorationPrBody as B,
4950
- variantKeyOf as C,
5757
+ compareSpecToMeasured as C,
4951
5758
  DRAFT_LINE_MAX as D,
4952
5759
  EXPLORATION_PREFIX as E,
4953
- overrideComment as F,
5760
+ variantKeyOf as F,
4954
5761
  GitHubError as G,
4955
- overrideRefusedComment as H,
4956
- suggestionStateFromLabels as I,
4957
- suggestionStatus as J,
4958
- composeRequestIssue as K,
4959
- EXPLORATION_NOTES_DIR as L,
4960
- isExplorationBranch as M,
4961
- diffLayered as N,
5762
+ withoutAddedWrappers as H,
5763
+ pairParts as I,
5764
+ COLOR_FIELDS as J,
5765
+ hashSpec as K,
5766
+ DRAFTED_BY_CLAUDE_LABEL as L,
5767
+ overrideComment as M,
5768
+ overrideRefusedComment as N,
4962
5769
  OVERRIDE_HEADING as O,
4963
5770
  PROSE_KEYS as P,
4964
- contrastDelta as Q,
5771
+ suggestionStateFromLabels as Q,
4965
5772
  REQUESTS_BRANCH as R,
4966
- runLayeredContrast as S,
4967
- layeredImpact as T,
4968
- classifyLayered as U,
4969
- composeProposalBody as V,
4970
- createSuggestionOnGitHub as W,
4971
- gitBlobSha as X,
4972
- SUGGESTION_SCHEMA as Y,
4973
- branchWithJiraKey as Z,
4974
- titleWithJiraKey as _,
5773
+ SPEC_FIELDS as S,
5774
+ suggestionStatus as T,
5775
+ composeRequestIssue as U,
5776
+ EXPLORATION_NOTES_DIR as V,
5777
+ isExplorationBranch as W,
5778
+ diffLayered as X,
5779
+ contrastDelta as Y,
5780
+ runLayeredContrast as Z,
5781
+ layeredImpact as _,
4975
5782
  parseChangelog as a,
4976
- TOKEN_SETTINGS_URL as a$,
4977
- formatFigmaSpec as a0,
4978
- figmaDraftBrief as a1,
4979
- parseFigmaOrigin as a2,
4980
- figmaPushStatus as a3,
4981
- parseFigmaSpec as a4,
4982
- FIGMA_SPEC_DIR as a5,
4983
- FROM_FIGMA_LABEL as a6,
4984
- withFigmaOrigin as a7,
4985
- WAITING_FOR_CLAUDE_LABEL as a8,
4986
- DRAFTED_BY_CLAUDE_LABEL as a9,
4987
- DOCS_EDITOR_ORIGIN as aA,
4988
- DOCS_KEYS as aB,
4989
- DOCS_MCP_ORIGIN as aC,
4990
- EXPLORATION_CHECK as aD,
4991
- EXPLORATION_STATUS_LABEL as aE,
4992
- EXPLORATION_TIER_LABEL as aF,
4993
- EXPLORATION_USE_CASES as aG,
4994
- PULL_STATE_PILL as aH,
4995
- ProtectedBranchError as aI,
4996
- REDUCED_MOTION_NOTE as aJ,
4997
- REQUEST_STATUS_LABEL as aK,
4998
- REVIEW_STATUS_LABEL as aL,
4999
- REVIEW_STATUS_ORDER as aM,
5000
- SPEC_FIELDS as aN,
5001
- SUGGESTION_APPLIED_MARK as aO,
5002
- SUGGESTION_APPLY_EXIT as aP,
5003
- SUGGESTION_FINAL_LABELS as aQ,
5004
- SUGGESTION_KINDS as aR,
5005
- SUGGESTION_LABEL as aS,
5006
- SUGGESTION_LIMITS as aT,
5007
- SUGGESTION_REVIEW_MARK as aU,
5008
- SUGGESTION_STATE_LABELS as aV,
5009
- SUGGESTION_STATUS_LABEL as aW,
5010
- SUGGESTION_STATUS_LABELS as aX,
5011
- SUGGESTION_TOO_LARGE as aY,
5012
- SuggestionError as aZ,
5013
- TOKEN_REJECTED_MESSAGE as a_,
5014
- planComponentPush as aa,
5015
- figmaOriginSection as ab,
5016
- figmaBranchTitle as ac,
5017
- figmaSpecSlug as ad,
5018
- figmaDraftBody as ae,
5019
- REQUEST_LABEL as af,
5020
- figmaNodeUrl as ag,
5021
- parentDir as ah,
5022
- parseComponentSpecs as ai,
5023
- readRepoTexts as aj,
5024
- normalizeRepoPath as ak,
5025
- isUnder as al,
5026
- GitHubClient as am,
5027
- effectiveTier as an,
5028
- parseRequestIssue as ao,
5029
- requestStatus as ap,
5030
- listSuggestionsOnGitHub as aq,
5031
- getSuggestionOnGitHub as ar,
5032
- BULK_LIMIT as as,
5033
- CI_STATE_LABEL as at,
5034
- COLOR_FIELDS as au,
5035
- COMPONENT_SPECS_VERSION as av,
5036
- CONFLICT_REASON as aw,
5037
- DEFAULT_COMPONENT_SPECS_FILE as ax,
5038
- DEFAULT_PROTECTED_BRANCHES as ay,
5039
- DOCS_CHANGE_BUMP as az,
5783
+ REVIEW_STATUS_ORDER as a$,
5784
+ composeProposalBody as a0,
5785
+ createSuggestionOnGitHub as a1,
5786
+ gitBlobSha as a2,
5787
+ SUGGESTION_SCHEMA as a3,
5788
+ branchWithJiraKey as a4,
5789
+ titleWithJiraKey as a5,
5790
+ figmaSpecPath as a6,
5791
+ formatFigmaSpec as a7,
5792
+ figmaDraftBrief as a8,
5793
+ parseFigmaOrigin as a9,
5794
+ requestIssueTitle as aA,
5795
+ pullState as aB,
5796
+ summarizeCi as aC,
5797
+ summarizeApprovals as aD,
5798
+ BULK_LIMIT as aE,
5799
+ CI_STATE_LABEL as aF,
5800
+ COMPONENT_SPECS_VERSION as aG,
5801
+ CONFLICT_REASON as aH,
5802
+ DEFAULT_COMPONENT_SPECS_FILE as aI,
5803
+ DEFAULT_PROTECTED_BRANCHES as aJ,
5804
+ DOCS_CHANGE_BUMP as aK,
5805
+ DOCS_CLI_ORIGIN as aL,
5806
+ DOCS_EDITOR_ORIGIN as aM,
5807
+ DOCS_KEYS as aN,
5808
+ DOCS_MCP_ORIGIN as aO,
5809
+ EXPLORATION_CHECK as aP,
5810
+ EXPLORATION_STATUS_LABEL as aQ,
5811
+ EXPLORATION_TIER_LABEL as aR,
5812
+ EXPLORATION_USE_CASES as aS,
5813
+ FORMER_MEMBER as aT,
5814
+ LOOK_AGAIN_STATUS_LABEL as aU,
5815
+ MeasuredSpecsError as aV,
5816
+ PULL_STATE_PILL as aW,
5817
+ ProtectedBranchError as aX,
5818
+ REDUCED_MOTION_NOTE as aY,
5819
+ REQUEST_STATUS_LABEL as aZ,
5820
+ REVIEW_STATUS_LABEL as a_,
5821
+ figmaPushStatus as aa,
5822
+ parseFigmaSpec as ab,
5823
+ FIGMA_SPEC_DIR as ac,
5824
+ FROM_FIGMA_LABEL as ad,
5825
+ withFigmaOrigin as ae,
5826
+ WAITING_FOR_CLAUDE_LABEL as af,
5827
+ planComponentPush as ag,
5828
+ figmaOriginSection as ah,
5829
+ figmaBranchTitle as ai,
5830
+ figmaSpecSlug as aj,
5831
+ figmaDraftBody as ak,
5832
+ REQUEST_LABEL as al,
5833
+ figmaNodeUrl as am,
5834
+ parentDir as an,
5835
+ parseComponentSpecs as ao,
5836
+ readRepoTexts as ap,
5837
+ normalizeRepoPath as aq,
5838
+ isUnder as ar,
5839
+ componentMotion as as,
5840
+ GitHubClient as at,
5841
+ effectiveTier as au,
5842
+ parseRequestIssue as av,
5843
+ requestStatus as aw,
5844
+ listSuggestionsOnGitHub as ax,
5845
+ getSuggestionOnGitHub as ay,
5846
+ closingIssueNumbers as az,
5040
5847
  buildReport as b,
5041
- hasWriteAccess as b$,
5042
- TRIGGER_LABELS as b0,
5043
- UNRELEASED as b1,
5044
- acceptDocsDraft as b2,
5045
- acceptDocsText as b3,
5046
- allowedMergeMethods as b4,
5047
- anatomyLabel as b5,
5048
- appliedStatus as b6,
5049
- assertSafeBranch as b7,
5050
- assignRequestOnGitHub as b8,
5051
- assignVersions as b9,
5052
- declineSuggestionOnGitHub as bA,
5053
- defaultDocsFooter as bB,
5054
- docsAuthorLine as bC,
5055
- docsFileText as bD,
5056
- docsProposal as bE,
5057
- docsSectionShapes as bF,
5058
- docsSlug as bG,
5059
- docsSuggestionPayload as bH,
5060
- docsText as bI,
5061
- draftChangeLine as bJ,
5062
- draftPushDescription as bK,
5063
- draftRequestText as bL,
5064
- eligibleReviewers as bM,
5065
- explorationBranch as bN,
5066
- explorationRules as bO,
5067
- explorationShareMessage as bP,
5068
- explorationSlugFromBranch as bQ,
5069
- figmaCell as bR,
5070
- fileModes as bS,
5071
- filterHistory as bT,
5072
- findOpenDocsProposal as bU,
5073
- findOpenDocsProposalOnGitHub as bV,
5074
- firstParagraph as bW,
5075
- formatDocsJson as bX,
5076
- friendlyMessage as bY,
5077
- githubSuggestionWorkspace as bZ,
5078
- hasSuggestionBlock as b_,
5079
- base64ToUtf8 as ba,
5080
- bridgeMcpInstallCommand as bb,
5081
- canOverride as bc,
5082
- canonicalJson as bd,
5083
- changeTable as be,
5084
- changesetBump as bf,
5085
- classifyAuth as bg,
5086
- cleanDocs as bh,
5087
- clearSharedEtags as bi,
5088
- closingIssueNumbers as bj,
5089
- commitFileOnGitHub as bk,
5090
- componentContrastPairs as bl,
5091
- componentMotion as bm,
5092
- componentsUsingMotionToken as bn,
5093
- composeChangeset as bo,
5094
- composeSuggestionAppliedComment as bp,
5095
- composeSuggestionIssue as bq,
5096
- composeSuggestionReviewComment as br,
5097
- continueDocsProposalOnGitHub as bs,
5098
- contrastDeltaTable as bt,
5099
- countChanges as bu,
5100
- coverageSummary as bv,
5101
- cssModuleClasses as bw,
5102
- declineComment as bx,
5103
- declineReasonProblems as by,
5104
- declineRequestOnGitHub as bz,
5848
+ docsSlug as b$,
5849
+ SUGGESTION_APPLIED_MARK as b0,
5850
+ SUGGESTION_APPLY_EXIT as b1,
5851
+ SUGGESTION_FINAL_LABELS as b2,
5852
+ SUGGESTION_KINDS as b3,
5853
+ SUGGESTION_LABEL as b4,
5854
+ SUGGESTION_LIMITS as b5,
5855
+ SUGGESTION_OVERRIDE_MARK as b6,
5856
+ SUGGESTION_REVIEW_MARK as b7,
5857
+ SUGGESTION_STATE_LABELS as b8,
5858
+ SUGGESTION_STATUS_LABEL as b9,
5859
+ claudeStage as bA,
5860
+ cleanDocs as bB,
5861
+ clearSharedEtags as bC,
5862
+ commitFileOnGitHub as bD,
5863
+ componentContrastPairs as bE,
5864
+ componentsUsingMotionToken as bF,
5865
+ composeChangeset as bG,
5866
+ composeSuggestionAppliedComment as bH,
5867
+ composeSuggestionIssue as bI,
5868
+ composeSuggestionOverrideComment as bJ,
5869
+ composeSuggestionReviewComment as bK,
5870
+ continueDocsProposalOnGitHub as bL,
5871
+ contrastDeltaTable as bM,
5872
+ countChanges as bN,
5873
+ coverageSummary as bO,
5874
+ cssColorToRgb as bP,
5875
+ cssColorsToRgb as bQ,
5876
+ cssModuleClasses as bR,
5877
+ declineComment as bS,
5878
+ declineReasonProblems as bT,
5879
+ declineRequestOnGitHub as bU,
5880
+ declineSuggestionOnGitHub as bV,
5881
+ defaultDocsFooter as bW,
5882
+ docsAuthorLine as bX,
5883
+ docsFileText as bY,
5884
+ docsProposal as bZ,
5885
+ docsSectionShapes as b_,
5886
+ SUGGESTION_STATUS_LABELS as ba,
5887
+ SUGGESTION_TOO_LARGE as bb,
5888
+ StorybookIndexError as bc,
5889
+ SuggestionError as bd,
5890
+ TOKEN_REJECTED_MESSAGE as be,
5891
+ TOKEN_SETTINGS_URL as bf,
5892
+ TRIGGER_LABELS as bg,
5893
+ UNRELEASED as bh,
5894
+ acceptDocsDraft as bi,
5895
+ acceptDocsText as bj,
5896
+ allowedMergeMethods as bk,
5897
+ anatomyLabel as bl,
5898
+ appliedStatus as bm,
5899
+ approvedSuggestionNumber as bn,
5900
+ approvedSuggestionOnGitHub as bo,
5901
+ assertSafeBranch as bp,
5902
+ assignRequestOnGitHub as bq,
5903
+ assignVersions as br,
5904
+ base64ToUtf8 as bs,
5905
+ branchVerdict as bt,
5906
+ bridgeMcpInstallCommand as bu,
5907
+ canOverride as bv,
5908
+ canonicalJson as bw,
5909
+ changeTable as bx,
5910
+ changesetBump as by,
5911
+ classifyAuth as bz,
5105
5912
  collectHistory as c,
5106
- sectionsNeedingReview as c$,
5107
- hashSpec as c0,
5108
- iconComponentName as c1,
5109
- installationFor as c2,
5110
- isActiveExploration as c3,
5111
- isAutomationPull as c4,
5112
- isDocsProposalBranch as c5,
5113
- isGuardrailPath as c6,
5114
- isJiraKey as c7,
5115
- isMissingPermission as c8,
5116
- isReleasePull as c9,
5117
- parseSuggestionReviewComment as cA,
5118
- parseTier as cB,
5119
- patchDocsJson as cC,
5120
- pendingApprovals as cD,
5121
- permissionLabel as cE,
5122
- planRestore as cF,
5123
- planRevert as cG,
5124
- planSuggestionApply as cH,
5125
- planToJson as cI,
5126
- prNumberFromSubject as cJ,
5127
- prefillDocs as cK,
5128
- proposeDocsOnGitHub as cL,
5129
- propsTable as cM,
5130
- pullState as cN,
5131
- pushChangeRows as cO,
5132
- rateLimitMessage as cP,
5133
- releasePull as cQ,
5134
- reopenEligibility as cR,
5135
- requestBrief as cS,
5136
- requestImagePath as cT,
5137
- requestIssueTitle as cU,
5138
- requiredApprovals as cV,
5139
- requiredPermission as cW,
5140
- reviewQueue as cX,
5141
- reviewStatusKind as cY,
5142
- reviewSuggestionOnGitHub as cZ,
5143
- reviewersToAsk as c_,
5144
- isTokenRejected as ca,
5145
- jiraIssueFields as cb,
5146
- jiraKeyOf as cc,
5147
- joinRepoPath as cd,
5148
- linkRequestToExplorationOnGitHub as ce,
5149
- linkSuggestionPrOnGitHub as cf,
5150
- linkedPullsOf as cg,
5151
- loadRepoFiles as ch,
5152
- lowerFirstWord as ci,
5153
- markSuggestionsAppliedOnGitHub as cj,
5154
- memoryEtagStore as ck,
5155
- memorySource as cl,
5156
- mergeGate as cm,
5157
- missingPermissionMessage as cn,
5158
- missingProse as co,
5159
- modeLabel as cp,
5160
- needsDocsReview as cq,
5161
- openPulls as cr,
5162
- overrideReasonProblem as cs,
5163
- parseChangesetText as ct,
5164
- parseJiraKey as cu,
5165
- parseOverrideComment as cv,
5166
- parseRepo as cw,
5167
- parseStructureChange as cx,
5168
- parseSuggestionAppliedComment as cy,
5169
- parseSuggestionIssue as cz,
5913
+ parseChangesetText as c$,
5914
+ docsSuggestionPayload as c0,
5915
+ docsText as c1,
5916
+ draftChangeLine as c2,
5917
+ draftPushDescription as c3,
5918
+ draftRequestText as c4,
5919
+ eligibleReviewers as c5,
5920
+ evalCalc as c6,
5921
+ explorationBranch as c7,
5922
+ explorationRules as c8,
5923
+ explorationShareMessage as c9,
5924
+ isTokenRejected as cA,
5925
+ jiraIssueFields as cB,
5926
+ jiraKeyOf as cC,
5927
+ joinRepoPath as cD,
5928
+ linkRequestToExplorationOnGitHub as cE,
5929
+ linkSuggestionPrOnGitHub as cF,
5930
+ linkedPullsOf as cG,
5931
+ loadRepoFiles as cH,
5932
+ lookAgainReviewers as cI,
5933
+ lowerFirstWord as cJ,
5934
+ markSuggestionsAppliedOnGitHub as cK,
5935
+ markUsedTokens as cL,
5936
+ memoryEtagStore as cM,
5937
+ memorySource as cN,
5938
+ mergeGate as cO,
5939
+ missingPermissionMessage as cP,
5940
+ missingProse as cQ,
5941
+ modeLabel as cR,
5942
+ needsDocsReview as cS,
5943
+ normalizeStorybookUrl as cT,
5944
+ openPulls as cU,
5945
+ overrideHolders as cV,
5946
+ overrideReasonMin as cW,
5947
+ overrideReasonProblem as cX,
5948
+ overrideSuggestionOnGitHub as cY,
5949
+ ownerOverridePolicy as cZ,
5950
+ parseBoxShadow as c_,
5951
+ explorationSlugFromBranch as ca,
5952
+ fetchStorybookIndex as cb,
5953
+ figmaCell as cc,
5954
+ fileModes as cd,
5955
+ filterHistory as ce,
5956
+ findOpenDocsProposal as cf,
5957
+ findOpenDocsProposalOnGitHub as cg,
5958
+ firstParagraph as ch,
5959
+ formatDocsJson as ci,
5960
+ friendlyMessage as cj,
5961
+ githubSuggestionWorkspace as ck,
5962
+ groupStorybookComponents as cl,
5963
+ hasOwnerApproval as cm,
5964
+ hasSuggestionBlock as cn,
5965
+ hasWriteAccess as co,
5966
+ iconComponentName as cp,
5967
+ installationFor as cq,
5968
+ isActiveExploration as cr,
5969
+ isAutomationPull as cs,
5970
+ isDocsProposalBranch as ct,
5971
+ isDraft as cu,
5972
+ isGuardrailPath as cv,
5973
+ isJiraKey as cw,
5974
+ isMissingPermission as cx,
5975
+ isPlatformRefusal as cy,
5976
+ isReleasePull as cz,
5170
5977
  applyTokenEdits as d,
5171
- sha256Hex as d0,
5172
- shortVariant as d1,
5173
- shortVariants as d2,
5174
- specHash as d3,
5175
- specsUsage as d4,
5176
- stalenessStatus as d5,
5177
- subjectTitle as d6,
5178
- suggestionAllowedRoots as d7,
5179
- suggestionAppliedComment as d8,
5180
- suggestionApplyTable as d9,
5181
- validateIntegrations as dA,
5182
- variantName as dB,
5183
- versionTimeline as dC,
5184
- webStorageEtagStore as dD,
5185
- withAuthorLine as dE,
5186
- withOutdated as dF,
5187
- withRequestExploration as dG,
5188
- withoutJiraKey as dH,
5189
- suggestionIssueTitle as da,
5190
- suggestionKindFromLabels as db,
5191
- suggestionLabelChanges as dc,
5192
- suggestionLabels as dd,
5193
- suggestionPathProblems as de,
5194
- suggestionPayloadSha as df,
5195
- suggestionReviewState as dg,
5196
- suggestionShapeProblems as dh,
5197
- suggestionStalenessOnGitHub as di,
5198
- suggestionStateLabel as dj,
5199
- suggestionSubject as dk,
5200
- suggestionTouches as dl,
5201
- summarizeApprovals as dm,
5202
- summarizeCi as dn,
5203
- summarizeSuggestionApprovals as dp,
5204
- syncSuggestionStateLabel as dq,
5205
- titleOf as dr,
5206
- toChangeSummary as ds,
5207
- tokenCssVar as dt,
5208
- tokenFingerprint as du,
5209
- tokenHistory as dv,
5210
- unsafePathProblem as dw,
5211
- utf8ToBase64 as dx,
5212
- validateComponentSpec as dy,
5213
- validateDocs as dz,
5978
+ suggestionReviewState as d$,
5979
+ parseJiraKey as d0,
5980
+ parseMeasuredSpecsFile as d1,
5981
+ parseOverrideComment as d2,
5982
+ parseRepo as d3,
5983
+ parseStorybookIndex as d4,
5984
+ parseStructureChange as d5,
5985
+ parseSuggestionAppliedComment as d6,
5986
+ parseSuggestionIssue as d7,
5987
+ parseSuggestionOverrideComment as d8,
5988
+ parseSuggestionReviewComment as d9,
5989
+ reviewStatusLabel as dA,
5990
+ reviewSuggestionOnGitHub as dB,
5991
+ reviewersToAsk as dC,
5992
+ sectionsNeedingReview as dD,
5993
+ sha256Hex as dE,
5994
+ sharedTokenPrefix as dF,
5995
+ shortVariant as dG,
5996
+ shortVariants as dH,
5997
+ specHash as dI,
5998
+ specsUsage as dJ,
5999
+ stalenessStatus as dK,
6000
+ storybookIframeUrl as dL,
6001
+ storybookIndexUrls as dM,
6002
+ storybookManagerUrl as dN,
6003
+ subjectTitle as dO,
6004
+ suggestionAllowedRoots as dP,
6005
+ suggestionAppliedComment as dQ,
6006
+ suggestionApplyTable as dR,
6007
+ suggestionIsTokenValues as dS,
6008
+ suggestionIssueTitle as dT,
6009
+ suggestionKindFromLabels as dU,
6010
+ suggestionLabelChanges as dV,
6011
+ suggestionLabels as dW,
6012
+ suggestionOverrideOf as dX,
6013
+ suggestionOverrideProblem as dY,
6014
+ suggestionPathProblems as dZ,
6015
+ suggestionPayloadSha as d_,
6016
+ parseTier as da,
6017
+ patchDocsJson as db,
6018
+ pendingApprovals as dc,
6019
+ permissionLabel as dd,
6020
+ planRestore as de,
6021
+ planRevert as df,
6022
+ planSuggestionApply as dg,
6023
+ planToJson as dh,
6024
+ prNumberFromSubject as di,
6025
+ prefillDocs as dj,
6026
+ proposeDocsOnGitHub as dk,
6027
+ propsTable as dl,
6028
+ pushChangeRows as dm,
6029
+ rateLimitMessage as dn,
6030
+ releasePull as dp,
6031
+ reopenEligibility as dq,
6032
+ requestBrief as dr,
6033
+ requestImagePath as ds,
6034
+ requesterFirstName as dt,
6035
+ requesterLine as du,
6036
+ requesterOf as dv,
6037
+ requiredApprovals as dw,
6038
+ requiredPermission as dx,
6039
+ reviewQueue as dy,
6040
+ reviewStatusKind as dz,
5214
6041
  buildHistoryEntry as e,
6042
+ suggestionShapeProblems as e0,
6043
+ suggestionStalenessOnGitHub as e1,
6044
+ suggestionStateLabel as e2,
6045
+ suggestionSubject as e3,
6046
+ suggestionTouches as e4,
6047
+ summarizeSuggestionApprovals as e5,
6048
+ syncSuggestionStateLabel as e6,
6049
+ titleOf as e7,
6050
+ toChangeSummary as e8,
6051
+ tokenCssVar as e9,
6052
+ tokenFingerprint as ea,
6053
+ tokenHistory as eb,
6054
+ tokenVariablePath as ec,
6055
+ unsafePathProblem as ed,
6056
+ utf8ToBase64 as ee,
6057
+ validateComponentSpec as ef,
6058
+ validateDocs as eg,
6059
+ validateIntegrations as eh,
6060
+ variantName as ei,
6061
+ versionTimeline as ej,
6062
+ webStorageEtagStore as ek,
6063
+ withAuthorLine as el,
6064
+ withFigmaPushApprovals as em,
6065
+ withOutdated as en,
6066
+ withRequestExploration as eo,
6067
+ withSrgbColors as ep,
6068
+ withSuggestionApprovals as eq,
6069
+ withTokenValueApprovals as er,
6070
+ withoutJiraKey as es,
5215
6071
  coerceValue as f,
5216
6072
  checkLayeredPair as g,
5217
6073
  formatChangeset as h,
@@ -5234,4 +6090,4 @@ export {
5234
6090
  explorationNote as y,
5235
6091
  explorationNotePath as z
5236
6092
  };
5237
- //# sourceMappingURL=repo-source-CRfhLO87.js.map
6093
+ //# sourceMappingURL=repo-source-CshjjC7x.js.map