@bridge-ds/core 0.19.0 → 0.21.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 (45) hide show
  1. package/LICENSE +17 -0
  2. package/bridge.config.schema.json +5 -0
  3. package/dist/audit/index.js +275 -52
  4. package/dist/audit/index.js.map +1 -1
  5. package/dist/audit/missing-pieces.d.ts +4 -0
  6. package/dist/component-spec.d.ts +13 -0
  7. package/dist/config.d.ts +5 -0
  8. package/dist/exploration-done-github.d.ts +50 -0
  9. package/dist/exploration-resume.d.ts +136 -0
  10. package/dist/explorations-github.d.ts +75 -3
  11. package/dist/explorations.d.ts +137 -8
  12. package/dist/fidelity.d.ts +12 -1
  13. package/dist/figma-fix-loop.d.ts +1 -1
  14. package/dist/figma-fix-run.d.ts +103 -0
  15. package/dist/figma-fixes.d.ts +14 -2
  16. package/dist/gap-case.d.ts +1 -2
  17. package/dist/github-client.d.ts +17 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +2139 -1290
  20. package/dist/index.js.map +1 -1
  21. package/dist/measure-location.d.ts +42 -0
  22. package/dist/measure-workflow.d.ts +48 -0
  23. package/dist/measured-only.d.ts +15 -1
  24. package/dist/node/index.js +15 -12
  25. package/dist/node/index.js.map +1 -1
  26. package/dist/previews-github.d.ts +13 -0
  27. package/dist/previews.d.ts +8 -1
  28. package/dist/{prod-branch-Bdx_2bRQ.js → prod-branch-C7tphFAA.js} +257 -192
  29. package/dist/prod-branch-C7tphFAA.js.map +1 -0
  30. package/dist/prod-branch-github.d.ts +54 -4
  31. package/dist/prod-branch.d.ts +42 -1
  32. package/dist/products.d.ts +17 -0
  33. package/dist/repo-model.d.ts +2 -0
  34. package/dist/{repo-source-m0Tc603o.js → repo-source-CEBBBtm1.js} +2183 -1937
  35. package/dist/repo-source-CEBBBtm1.js.map +1 -0
  36. package/dist/{report-DFPKygfk.js → report-B7wNAnSu.js} +38 -38
  37. package/dist/{report-DFPKygfk.js.map → report-B7wNAnSu.js.map} +1 -1
  38. package/dist/review-gate.d.ts +7 -1
  39. package/dist/suggestion-review.d.ts +43 -2
  40. package/dist/suggestions-github.d.ts +12 -1
  41. package/dist/tier1-publish.d.ts +48 -0
  42. package/dist/try-in-product.d.ts +4 -1
  43. package/package.json +1 -1
  44. package/dist/prod-branch-Bdx_2bRQ.js.map +0 -1
  45. package/dist/repo-source-m0Tc603o.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { cr as tokenBump, aW as changeKind, L as LayeredTokenSet, aN 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, ah as ConfigError, _ as DEFAULT_TOKEN_PATHS, ag as globToRegExp, ae 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, a1 as readPreviewComment, aV as canShip, cz as whoCanShipSentence, ct as unreadableShipTeams, cb as soloApprover, aR as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-Bdx_2bRQ.js";
1
+ import { cw as tokenBump, a_ as changeKind, L as LayeredTokenSet, aS as aliasOf, c9 as rgbaToDtcg, by as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bB as inheritedTypeAt, u as upsertToken, H as readTokenAt, b4 as comboKey, c8 as renderedColor, bc as contrastRatio, z as rgbaToHex, U as classifyRisk, bl as figmaPushNextLine, W as RISK_LABELS, bm as figmaPushTierLabel, V as CONFIG_FILE, N as loadConfig, am as ConfigError, af as DEFAULT_TOKEN_PATHS, al as globToRegExp, aj as slugify, Q as validateLayered, bT as parseJson, cd as serializeJson, bZ as plainEqual, cv as toPlain, bv as getEntry, bt as fromPlain, m as modeDisplayName, A as formatValue, _ as previewUrl, M as resolveBranchPreviews, ai as readPreviewComment, O as tokensPath, a4 as NEEDED_BY_LABEL, aZ as canShip, cE as whoCanShipSentence, cy as unreadableShipTeams, cg as soloApprover, aW as approverLists, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-C7tphFAA.js";
2
2
  function changesetBump(changes) {
3
3
  return tokenBump(changes) ?? "patch";
4
4
  }
@@ -1026,6 +1026,7 @@ class GitHubClient {
1026
1026
  const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
1027
1027
  const r = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`);
1028
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 });
1029
1030
  if (r.encoding === "base64" && r.content !== void 0) return base64ToUtf8(r.content);
1030
1031
  return r.content;
1031
1032
  } catch (err) {
@@ -1251,6 +1252,10 @@ class GitHubClient {
1251
1252
  const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
1252
1253
  return r.map((f) => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions }));
1253
1254
  }
1255
+ /** The commits of a pull request, oldest first, as GitHub lists them (one read, up to 100). */
1256
+ async listPullRequestCommits(n) {
1257
+ return this.request("GET", `${this.repoPath}/pulls/${n}/commits?per_page=100`);
1258
+ }
1254
1259
  async updatePullRequest(n, patch) {
1255
1260
  await this.request("PATCH", `${this.repoPath}/pulls/${n}`, patch);
1256
1261
  }
@@ -2485,6 +2490,8 @@ const EXPLORATION_PREFIX = "explore/";
2485
2490
  const EXPLORATION_NOTES_DIR = ".bridge/explorations";
2486
2491
  const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
2487
2492
  const EXPLORATION_CHECK = "Exploration not merged as is";
2493
+ const EXPLORATION_READY_LABEL = "ready-for-devs";
2494
+ const EXPLORATION_ARCHIVED_LABEL = "archived";
2488
2495
  const EXPLORATION_USE_CASES = [
2489
2496
  { 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." },
2490
2497
  { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
@@ -2492,13 +2499,21 @@ const EXPLORATION_USE_CASES = [
2492
2499
  { 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." },
2493
2500
  { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
2494
2501
  ];
2495
- const EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
2502
+ const EXPLORATION_STATUS_LABEL = {
2503
+ active: "Running",
2504
+ ready: "Waiting for code",
2505
+ building: "Being built",
2506
+ shipped: "Shipped",
2507
+ promoted: "Promoted to review",
2508
+ closed: "Closed"
2509
+ };
2496
2510
  const MARK$2 = "bridge-exploration";
2497
2511
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2512
+ const EXPLORATION_SLUG_MAX = 60;
2498
2513
  function explorationSlug(name) {
2499
2514
  const s = name.trim().replace(/^explore\//i, "");
2500
2515
  if (!/[a-z0-9]/i.test(s)) return "";
2501
- return slugify(s, 40);
2516
+ return slugify(s, EXPLORATION_SLUG_MAX);
2502
2517
  }
2503
2518
  function explorationBranch(nameOrBranch) {
2504
2519
  return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
@@ -2510,6 +2525,25 @@ function isExplorationBranch(ref) {
2510
2525
  function explorationSlugFromBranch(ref) {
2511
2526
  return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
2512
2527
  }
2528
+ const REPO_NAME = /^[\w.-]+\/[\w.-]+$/;
2529
+ const KEY_NAME = /^[a-z0-9][a-z0-9-]*$/i;
2530
+ function parseExplorationRef(input) {
2531
+ if (typeof input === "number") return { ref: input };
2532
+ const s = input.trim();
2533
+ const link = /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/i.exec(s);
2534
+ if (link) return { repo: `${link[1]}/${link[2]}`, ref: Number(link[3]) };
2535
+ if (/^#?\d+$/.test(s)) return { ref: Number(s.replace(/^#/, "")) };
2536
+ const hash = /^(.+)#(\d+)$/.exec(s);
2537
+ if (hash) {
2538
+ const where2 = hash[1].trim();
2539
+ if (REPO_NAME.test(where2)) return { repo: where2, ref: Number(hash[2]) };
2540
+ if (KEY_NAME.test(where2)) return { product: where2.toLowerCase(), ref: Number(hash[2]) };
2541
+ }
2542
+ const named = /^([a-z0-9][a-z0-9-]*):(.+)$/i.exec(s);
2543
+ if (named) return { product: named[1].toLowerCase(), ref: /^#?\d+$/.test(named[2].trim()) ? Number(named[2].trim().replace(/^#/, "")) : named[2].trim() };
2544
+ return { ref: s };
2545
+ }
2546
+ const explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
2513
2547
  const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
2514
2548
  function explorationProblems(e) {
2515
2549
  const out = [];
@@ -2518,7 +2552,6 @@ function explorationProblems(e) {
2518
2552
  if (!name) out.push('Name the exploration, for example "Denser data tables".');
2519
2553
  else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
2520
2554
  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.");
2521
- if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
2522
2555
  return out;
2523
2556
  }
2524
2557
  const useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
@@ -2528,19 +2561,18 @@ function explorationNote(e) {
2528
2561
  return [
2529
2562
  `# Exploration: ${e.name.trim()}`,
2530
2563
  "",
2531
- e.purpose.trim(),
2532
- "",
2564
+ ...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
2533
2565
  `- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
2534
2566
  ...uc ? [`- Use case: ${uc}`] : [],
2535
2567
  ...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
2536
2568
  "",
2537
- "This file marks the branch as an exploration. It is never merged as is: promoting the pull request to review removes it.",
2569
+ 'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
2538
2570
  ""
2539
2571
  ].join("\n");
2540
2572
  }
2541
2573
  function explorationPrTitle(e) {
2542
- const n = e.name.trim().replace(/\.$/, "");
2543
- return `Exploration: ${n.charAt(0).toLowerCase()}${n.slice(1)}`;
2574
+ const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
2575
+ return `Exploration: ${n}`;
2544
2576
  }
2545
2577
  function explorationPrBody(e, ctx) {
2546
2578
  const slug = e.slug || explorationSlug(e.name);
@@ -2550,44 +2582,78 @@ function explorationPrBody(e, ctx) {
2550
2582
  const lines = [
2551
2583
  `## Exploration: ${e.name.trim()}`,
2552
2584
  "",
2553
- `> Not mergeable as is. This draft pull request is a sandbox on \`${branch}\`: promote it to a normal review when the idea is validated, or close it.`,
2554
- "",
2555
- `**Purpose.** ${e.purpose.trim()}`,
2556
- ...uc ? ["", `**Use case.** ${uc}`] : [],
2585
+ `> Not mergeable as is. This draft pull request is a living prototype on \`${branch}\`: say "Done exploring" when the idea is ready for developers. Keep it open, so its preview link keeps working.`,
2557
2586
  "",
2587
+ ...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
2588
+ ...uc ? [`**Use case.** ${uc}`, ""] : [],
2558
2589
  "### What is experimental",
2559
2590
  "",
2560
- ...experimental.length ? experimental.map((x) => `- ${x}`) : ["- Token edits and component work committed to this branch. Nothing here reaches main or production."],
2591
+ ...experimental.length ? experimental.map((x) => `- ${x}`) : [ctx.inProduct ? `- The ${ctx.inProduct.name} screens and flows built on this branch with the library. Nothing here reaches main or production.` : "- Token edits and component work committed to this branch. Nothing here reaches main or production."],
2561
2592
  ...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
2562
- "",
2563
- "### Try it in a product staging branch",
2564
2593
  ""
2565
2594
  ];
2566
- lines.push(
2567
- 'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
2568
- );
2569
- lines.push("", "### Previews", "");
2570
- const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
2571
- if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url} |`));
2572
- else lines.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
2595
+ if (ctx.inProduct) {
2596
+ const p = ctx.inProduct;
2597
+ lines.push(
2598
+ "### Work on it",
2599
+ "",
2600
+ `This exploration lives in the ${p.name} repository, so it runs in your usual environment: \`git fetch origin && git switch ${branch}\`, then start ${p.name} as you always do.`,
2601
+ "",
2602
+ `The library comes from npm, as on main. To try library changes that are not released yet, install the snapshot of their branch in ${p.library} (\`npm i @bridge-ds/react@<snapshot>\`): the command is in its "Try this exploration in your product" comment. Library changes are made in ${p.library}, never on this branch.`,
2603
+ "",
2604
+ "### Previews",
2605
+ ""
2606
+ );
2607
+ const url = p.preview ? previewUrl(p.preview, { branch, ...ctx.pr ? { pr: ctx.pr } : {} }) : null;
2608
+ if (p.preview) lines.push("| App | Preview |", "| --- | --- |", `| ${p.name} | ${url ?? "Link in the Vercel comment on this pull request"} |`);
2609
+ else lines.push(`The preview deploys of ${p.name}, if it has them, build this branch like any other.`);
2610
+ } else {
2611
+ lines.push(
2612
+ "### Try it in a product staging branch",
2613
+ "",
2614
+ 'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
2615
+ );
2616
+ lines.push("", "### Previews", "");
2617
+ const links = resolveBranchPreviews(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
2618
+ if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url ?? "Link in the Vercel comment on this pull request"} |`));
2619
+ else lines.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
2620
+ }
2573
2621
  lines.push(
2574
2622
  "",
2575
2623
  "### When it is done",
2576
2624
  "",
2577
- `- **Promote:** in Bridge Studio (Changes, Explorations, Promote to review) or \`bridge explore promote ${slug}\`. Bridge marks the pull request ready for review, removes the \`${EXPLORATION_LABEL}\` label and the exploration note, and adds the tier labels. Then it goes through the usual review.`,
2578
- "- **Close:** close this pull request. The branch stays for reference; the preview prerelease is removed.",
2625
+ `- **Done exploring:** on the exploration's page in Bridge Studio, or \`bridge explore done ${slug}\`. Bridge removes the \`${EXPLORATION_LABEL}\` label and the exploration note, adds \`${EXPLORATION_READY_LABEL}\`, and sends the pieces the designer picks to the DS lead as requests. Nothing is closed or merged: developers continue on \`${branch}\`.`,
2626
+ `- **Archive:** \`bridge explore archive ${slug}\` adds the \`${EXPLORATION_ARCHIVED_LABEL}\` label. Keep this pull request open: ${ctx.inProduct ? "Bridge follows open explorations, and its preview link is how people come back to the prototype." : "closing it removes the preview prerelease."}`,
2579
2627
  "",
2580
- `<!-- ${MARK$2} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {}, ...e.source ? { source: e.source } : {}, ...e.agent ? { agent: e.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
2628
+ explorationBlock({ ...e, slug })
2581
2629
  );
2582
2630
  return lines.join("\n");
2583
2631
  }
2632
+ function explorationBlock(e) {
2633
+ const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
2634
+ const data = {
2635
+ name: e.name.trim(),
2636
+ purpose: (e.purpose ?? "").trim(),
2637
+ slug: e.slug || explorationSlug(e.name),
2638
+ ...e.useCase ? { useCase: e.useCase } : {},
2639
+ ...experimental.length ? { experimental } : {},
2640
+ ...e.author ? { author: e.author } : {},
2641
+ ...e.startedAt ? { startedAt: e.startedAt } : {},
2642
+ ...e.source ? { source: e.source } : {},
2643
+ ...e.agent ? { agent: e.agent } : {},
2644
+ ...e.doneAt ? { doneAt: e.doneAt } : {},
2645
+ ...e.doneSha ? { doneSha: e.doneSha } : {}
2646
+ };
2647
+ return `<!-- ${MARK$2} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
2648
+ }
2584
2649
  function parseExplorationBody(body) {
2585
2650
  if (!body) return void 0;
2586
2651
  const m = new RegExp(`<!-- ${MARK$2} (\\{[\\s\\S]*?\\}) -->`).exec(body);
2587
2652
  if (!m) return void 0;
2588
2653
  try {
2589
2654
  const e = JSON.parse(m[1]);
2590
- return e && typeof e.name === "string" && typeof e.purpose === "string" ? e : void 0;
2655
+ if (!e || typeof e.name !== "string") return void 0;
2656
+ return typeof e.purpose === "string" ? e : e.purpose === void 0 ? { ...e, purpose: "" } : void 0;
2591
2657
  } catch {
2592
2658
  return void 0;
2593
2659
  }
@@ -2596,13 +2662,84 @@ const labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.n
2596
2662
  function isExplorationPull(p) {
2597
2663
  return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
2598
2664
  }
2665
+ function isHandedOff(p) {
2666
+ return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
2667
+ }
2599
2668
  function explorationStatus(p) {
2600
2669
  const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
2601
2670
  if (p.state !== "open" && !merged) return "closed";
2602
2671
  if (p.state === "open" && labelNames(p.labels).includes(EXPLORATION_LABEL)) return "active";
2603
- return "promoted";
2672
+ if (!isHandedOff(p)) return "promoted";
2673
+ if (merged) return "shipped";
2674
+ if (p.draft === false) return "building";
2675
+ const doneSha = parseExplorationBody(p.body)?.doneSha;
2676
+ if (doneSha && p.headSha && p.headSha !== doneSha) return "building";
2677
+ return "ready";
2604
2678
  }
2605
2679
  const isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
2680
+ const isArchivedExploration = (labels) => labelNames(labels ?? []).includes(EXPLORATION_ARCHIVED_LABEL);
2681
+ function isHeldExploration(p) {
2682
+ if (p.state !== void 0 && p.state !== "open") return false;
2683
+ const names2 = labelNames(p.labels ?? []);
2684
+ if (names2.includes(EXPLORATION_LABEL) || names2.includes(EXPLORATION_ARCHIVED_LABEL)) return true;
2685
+ return names2.includes(EXPLORATION_READY_LABEL) && p.draft !== false;
2686
+ }
2687
+ const explorationTakeCommands = (branch) => ["git fetch origin", `git checkout ${branch}`];
2688
+ const CONTINUE = "Continue on this branch: its pull request already exists, so Bridge follows your pushes, and the merge reads as shipped.";
2689
+ const EXPLORATION_STARTING_POINT = `A starting point, not ready to ship: nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
2690
+ function explorationStartingPoint(checks) {
2691
+ switch (checks) {
2692
+ case "success":
2693
+ return `A starting point: its checks pass, but nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
2694
+ case "failure":
2695
+ return `A starting point, not ready to ship: some checks fail on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
2696
+ case "pending":
2697
+ return `A starting point, not ready to ship: its checks are still running and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
2698
+ case "none":
2699
+ return `A starting point, not ready to ship: no checks ran on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
2700
+ default:
2701
+ return EXPLORATION_STARTING_POINT;
2702
+ }
2703
+ }
2704
+ function doneExploringPlan(input) {
2705
+ const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
2706
+ return {
2707
+ removeLabels: [EXPLORATION_LABEL],
2708
+ addLabels: [EXPLORATION_READY_LABEL],
2709
+ removeFiles,
2710
+ body: withDoneExploring(input.body, { branch: input.branch, doneAt: (input.date ?? /* @__PURE__ */ new Date()).toISOString() }),
2711
+ take: explorationTakeCommands(input.branch),
2712
+ markReady: false
2713
+ };
2714
+ }
2715
+ const DEV_SECTION = "### For developers";
2716
+ function withDoneExploring(body, done) {
2717
+ const e = parseExplorationBody(body);
2718
+ const [fetch2, checkout] = explorationTakeCommands(done.branch);
2719
+ const section = [
2720
+ DEV_SECTION,
2721
+ "",
2722
+ `**Done exploring on ${done.doneAt.slice(0, 10)}.** ${explorationStartingPoint(done.checks)}`,
2723
+ "",
2724
+ "```sh",
2725
+ fetch2,
2726
+ checkout,
2727
+ "```",
2728
+ "",
2729
+ "Mark this pull request ready for review when the code is yours. Keep it open: its preview link is how designers, the DS lead and stakeholders come back to the prototype."
2730
+ ].join("\n");
2731
+ const block = new RegExp(`<!-- ${MARK$2} \\{[\\s\\S]*?\\} -->`);
2732
+ const kept = (body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/### For developers[\s\S]*?(?=### |<!-- |$)/, "");
2733
+ if (!e || !block.test(kept)) return `${kept.trim()}${kept.trim() ? "\n\n" : ""}${section}`;
2734
+ const at = kept.search(block);
2735
+ const nextBlock = explorationBlock({ ...e, doneAt: done.doneAt, ...done.doneSha ? { doneSha: done.doneSha } : {} });
2736
+ const after = kept.slice(at).replace(block, () => nextBlock).trim();
2737
+ return `${kept.slice(0, at).trim()}
2738
+
2739
+ ${section}
2740
+
2741
+ ${after}`.trim();
2742
+ }
2606
2743
  function promotionPlan(input) {
2607
2744
  const exploration = parseExplorationBody(input.body);
2608
2745
  const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
@@ -2654,8 +2791,8 @@ function explorationShareMessage(input) {
2654
2791
  function explorationRules(branch) {
2655
2792
  return [
2656
2793
  `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
2657
- `- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It is promoted to review only when the idea is validated.`,
2658
- "- Do not mark the pull request ready for review and do not remove the exploration label: promoting is a person's decision."
2794
+ `- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It stays open as a living prototype.`,
2795
+ `- Do not mark the pull request ready for review, do not remove the exploration label and never close it: "Done exploring" is a person's decision, and closing removes its preview.`
2659
2796
  ];
2660
2797
  }
2661
2798
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2701,7 +2838,8 @@ function requestIssueTitle(r) {
2701
2838
  return subjectTitle(r.component, r.title);
2702
2839
  }
2703
2840
  function composeRequestIssue(r, opts = {}) {
2704
- const rows = r.changes.length ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r.changes.map((c) => `| ${cell$1(c.field)} | ${cell$1(c.before ?? "")} | ${cell$1(c.after ?? "")} |`)] : [];
2841
+ const now = r.changes.some((c) => c.before?.trim());
2842
+ const rows = !r.changes.length ? [] : now ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r.changes.map((c) => `| ${cell$1(c.field)} | ${cell$1(c.before ?? "")} | ${cell$1(c.after ?? "")} |`)] : ["| What | Requested |", "| --- | --- |", ...r.changes.map((c) => `| ${cell$1(c.field)} | ${cell$1(c.after ?? "")} |`)];
2705
2843
  const figma = r.figma?.url ? `[${r.figma.nodeName ?? "Open in Figma"}](${r.figma.url})` : r.figma?.nodeName ? r.figma.nodeName : "";
2706
2844
  const body = [
2707
2845
  r.kind === "new-component" ? `## New component request: ${r.component}` : `## ${kindLabel$1(r.kind)} request for ${r.component}`,
@@ -2713,10 +2851,10 @@ function composeRequestIssue(r, opts = {}) {
2713
2851
  ...r.images?.length ? [...r.images.map((u, i) => `![${r.component} design ${i + 1}](${u})`), ""] : [],
2714
2852
  ...figma ? [`**Design:** ${figma}`, ""] : [],
2715
2853
  ...opts.componentUrl ? [`**Component:** ${opts.componentUrl}`, ""] : [],
2716
- ...r.exploration ? [`**Exploration:** \`${r.exploration}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot and is promoted to review only when the idea is validated.`, ""] : [],
2854
+ ...r.exploration ? [`**Exploration:** \`${r.exploration}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot, and developers take the branch once the designer is done exploring.`, ""] : [],
2717
2855
  "## How to implement",
2718
2856
  "",
2719
- r.exploration ? `Commit the change to \`${r.exploration}\` (its draft pull request is the place). With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool. Promoting the exploration closes this request when it merges.` : `Code is the source of truth: change the component in a pull request that says \`Closes #<this issue>\`, so this request closes when it merges. With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool.`,
2857
+ r.exploration ? `Commit the change to \`${r.exploration}\` (its draft pull request is the place). With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool. The request closes when developers ship that branch.` : `Code is the source of truth: change the component in a pull request that says \`Closes #<this issue>\`, so this request closes when it merges. With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool.`,
2720
2858
  "",
2721
2859
  `<sub>Requested from ${sourceLabel$1(r.source)}${r.requestedBy ? ` by @${r.requestedBy}` : ""} with Bridge.</sub>`,
2722
2860
  "",
@@ -2741,7 +2879,7 @@ function withRequestExploration(body, branch) {
2741
2879
  if (!r || !body) return void 0;
2742
2880
  const block = new RegExp(`<!-- ${MARK$1} \\{[\\s\\S]*?\\} -->`);
2743
2881
  let out = body.replace(block, () => `<!-- ${MARK$1} ${JSON.stringify({ ...r, exploration: branch })} -->`);
2744
- const line = `**Exploration:** \`${branch}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot and is promoted to review only when the idea is validated.`;
2882
+ const line = `**Exploration:** \`${branch}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot, and developers take the branch once the designer is done exploring.`;
2745
2883
  if (/^\*\*Exploration:\*\* .*$/m.test(out)) out = out.replace(/^\*\*Exploration:\*\* .*$/m, () => line);
2746
2884
  else if (out.includes("\n## How to implement")) out = out.replace("\n## How to implement", () => `
2747
2885
  ${line}
@@ -2798,7 +2936,7 @@ function requestBrief(r, ctx) {
2798
2936
  "Rules:",
2799
2937
  `- Follow ${ctx.agentsFile ?? "AGENTS.md"}: tokens only in component CSS, Base UI for behavior, states, tests, story, docs.json, changeset.`,
2800
2938
  "- A new component, prop or variant is Tier 2: open the pull request as a draft (new component: follow the new component checklist). A token add or rename is Tier 3: describe it, do not invent values.",
2801
- ...exploration ? [`- This request is part of the exploration ${exploration}.`, ...explorationRules(exploration), `- Reference the request in the commit message ("Refs #${ctx.issue}"). Promoting the exploration closes it.`] : [`- Work on a branch named agent/request-${ctx.issue}. The pull request body must contain "Closes #${ctx.issue}".`],
2939
+ ...exploration ? [`- This request is part of the exploration ${exploration}.`, ...explorationRules(exploration), `- Reference the request in the commit message ("Refs #${ctx.issue}"). Shipping the exploration closes it.`] : [`- Work on a branch named agent/request-${ctx.issue}. The pull request body must contain "Closes #${ctx.issue}".`],
2802
2940
  "- If the request is unclear or conflicts with the guidance, comment on the issue instead of guessing."
2803
2941
  ].join("\n");
2804
2942
  }
@@ -2831,37 +2969,6 @@ function jiraIssueFields(r, jira, issueUrl) {
2831
2969
  }
2832
2970
  };
2833
2971
  }
2834
- async function linkRequestToExplorationOnGitHub(gh, n, branch, opts = {}) {
2835
- if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
2836
- const issue = await gh.getIssue(n);
2837
- const body = withRequestExploration(issue.body, branch);
2838
- if (!body) throw new GitHubError(422, `#${n} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
2839
- await gh.updateIssue(n, { body });
2840
- const warnings = [];
2841
- if (!await gh.addLabels(n, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n}.`);
2842
- let assignees = [];
2843
- if (opts.assignee) {
2844
- try {
2845
- assignees = await gh.addAssignees(n, [opts.assignee.replace(/^@/, "")]);
2846
- } catch {
2847
- warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n}.`);
2848
- }
2849
- }
2850
- return { number: n, branch, assignees, warnings };
2851
- }
2852
- async function assignRequestOnGitHub(gh, n, login) {
2853
- const who = login.trim().replace(/^@/, "");
2854
- if (!who) throw new GitHubError(422, "Pick who should work on it.");
2855
- const assignees = await gh.addAssignees(n, [who]);
2856
- if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
2857
- return assignees;
2858
- }
2859
- async function declineRequestOnGitHub(gh, n, reason, by) {
2860
- const problems = declineReasonProblems(reason);
2861
- if (problems.length) throw new GitHubError(422, problems.join(" "));
2862
- await gh.addComment(n, declineComment(reason, by));
2863
- await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
2864
- }
2865
2972
  const COMPONENT_SPECS_VERSION = 1;
2866
2973
  const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
2867
2974
  const SPEC_FIELDS = [
@@ -3019,6 +3126,23 @@ function hashSpec(spec) {
3019
3126
  }
3020
3127
  const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
3021
3128
  const variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
3129
+ function matchVariant(spec, props) {
3130
+ if (!spec) return void 0;
3131
+ const key = variantKeyOf(props);
3132
+ const exact = spec.variants.find((v) => variantKeyOf(v.props) === key);
3133
+ if (exact) return exact;
3134
+ const asked = Object.entries(props);
3135
+ const fits = spec.variants.filter((v) => Object.keys(v.props).length > asked.length && asked.every(([k, val]) => v.props[k] === val));
3136
+ if (fits.length <= 1) return fits[0];
3137
+ const atDefault = (name, value) => {
3138
+ const axis = spec.axes?.find((a) => a.name === name);
3139
+ if (axis) return value === axis.default;
3140
+ const v = value.toLowerCase();
3141
+ return v === "false" || v === "default";
3142
+ };
3143
+ const best = fits.filter((v) => Object.entries(v.props).every(([k, val]) => k in props || atDefault(k, val)));
3144
+ return best.length === 1 ? best[0] : void 0;
3145
+ }
3022
3146
  const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
3023
3147
  function componentMotion(spec) {
3024
3148
  const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
@@ -3081,553 +3205,155 @@ function figmaCell(entry) {
3081
3205
  if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
3082
3206
  return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
3083
3207
  }
3084
- const px = (v) => {
3085
- if (!v) return void 0;
3086
- const m = /^(-?[\d.]+)px$/.exec(v.trim());
3087
- return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
3088
- };
3089
- function evalCalc(expr, resolve) {
3090
- const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
3091
- if (!body) return void 0;
3092
- let missing = false;
3093
- const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
3094
- const v2 = resolve(path);
3095
- const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
3096
- if (n === void 0 || Number.isNaN(n)) missing = true;
3097
- return String(n);
3098
- });
3099
- if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
3100
- const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
3101
- let i = 0;
3102
- const factor = () => {
3103
- const t = tokens[i++];
3104
- if (t === "(") {
3105
- const v2 = sum();
3106
- i++;
3107
- return v2;
3108
- }
3109
- if (t === "-") return -factor();
3110
- return Number(t);
3111
- };
3112
- const product = () => {
3113
- let v2 = factor();
3114
- while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
3115
- return v2;
3116
- };
3117
- const sum = () => {
3118
- let v2 = product();
3119
- while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
3120
- return v2;
3121
- };
3122
- const v = sum();
3123
- return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
3208
+ const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
3209
+ const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
3210
+ const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
3211
+ function rawIssue(label, r, values2) {
3212
+ const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
3213
+ 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.`;
3214
+ return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
3124
3215
  }
3125
- const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
3126
- function normColor(v) {
3127
- const t = v.trim().toLowerCase();
3128
- const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
3129
- if (hex2) {
3130
- const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
3131
- const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
3132
- const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
3133
- return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
3216
+ function planComponentPush(d, ctx) {
3217
+ const issues = [...d.unknown];
3218
+ const reasons = [];
3219
+ const edits = /* @__PURE__ */ new Map();
3220
+ const also = /* @__PURE__ */ new Set();
3221
+ for (const r of d.rebinds) {
3222
+ const label = `${cap(r.kind)} in ${where(r)}`;
3223
+ if (r.raw?.length) {
3224
+ issues.push(rawIssue(label, r, r.raw));
3225
+ continue;
3226
+ }
3227
+ if (!r.after.length) {
3228
+ 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." });
3229
+ continue;
3230
+ }
3231
+ const missing = r.after.filter((p) => !ctx.tierOf(p));
3232
+ if (missing.length) {
3233
+ 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." });
3234
+ continue;
3235
+ }
3236
+ if (!r.before.length) {
3237
+ reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
3238
+ continue;
3239
+ }
3240
+ if (r.before.length !== 1 || r.after.length !== 1) {
3241
+ reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
3242
+ continue;
3243
+ }
3244
+ const [from, to] = [r.before[0], r.after[0]];
3245
+ if (ctx.tierOf(from) !== "component") {
3246
+ reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
3247
+ continue;
3248
+ }
3249
+ const elsewhere = ctx.usedElsewhere(from, r);
3250
+ if (elsewhere.variants.length) {
3251
+ const v = elsewhere.variants;
3252
+ const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
3253
+ reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
3254
+ continue;
3255
+ }
3256
+ for (const c of elsewhere.components) also.add(c);
3257
+ const prev = edits.get(from);
3258
+ if (prev && prev.value !== `{${to}}`) {
3259
+ reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
3260
+ edits.delete(from);
3261
+ continue;
3262
+ }
3263
+ edits.set(from, { op: "set", path: from, value: `{${to}}` });
3134
3264
  }
3135
- const m = /^rgba?\(([^)]*)\)$/.exec(t);
3136
- if (m) {
3137
- const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
3138
- const [r, g, b] = parts;
3139
- const a = parts[3] ?? 1;
3140
- return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
3265
+ const list = [...edits.values()];
3266
+ if (d.needsCode || reasons.length) {
3267
+ return {
3268
+ route: "claude-draft",
3269
+ 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.` : ""}`,
3270
+ edits: list,
3271
+ reasons,
3272
+ issues,
3273
+ alsoChanges: [...also]
3274
+ };
3141
3275
  }
3142
- return t.replace(/\s+/g, "");
3143
- }
3144
- const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
3145
- const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
3146
- function withoutAddedWrappers(parts, measured) {
3147
- const real = new Set(measured.parts.map((m) => m.id));
3148
- const added = /* @__PURE__ */ new Map();
3149
- for (const p of parts) {
3150
- if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
3151
- const kids = parts.filter((c) => c.parent === p.id);
3152
- if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
3276
+ if (list.length) {
3277
+ const n = list.length;
3278
+ const others = [...also];
3279
+ const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
3280
+ 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"}.` : "";
3281
+ 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 };
3153
3282
  }
3154
- if (!added.size) return parts;
3155
- const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
3156
- return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
3283
+ 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: [] };
3157
3284
  }
3158
- function pairParts(staticParts, measured, o = {}) {
3159
- const pairs2 = /* @__PURE__ */ new Map();
3160
- const taken = /* @__PURE__ */ new Set();
3161
- const mParts = new Map(measured.parts.map((p) => [p.id, p]));
3162
- const sParts = new Map(staticParts.map((p) => [p.id, p]));
3163
- const take = (s, m) => {
3164
- pairs2.set(s, m);
3165
- taken.add(m);
3166
- };
3167
- const ancestors = (id) => {
3168
- const out = [];
3169
- for (let p = mParts.get(id)?.parent; p; p = mParts.get(p)?.parent) out.push(p);
3170
- return out;
3171
- };
3172
- const pairedAncestor = (p) => {
3173
- for (let a = p.parent; a; a = sParts.get(a)?.parent) if (pairs2.has(a)) return pairs2.get(a);
3174
- return void 0;
3175
- };
3176
- const best = (p, cands) => {
3177
- if (!cands.length) return void 0;
3178
- const parent = p.parent ? pairs2.get(p.parent) : void 0;
3179
- if (parent) {
3180
- const direct = cands.find((c) => c.parent === parent);
3181
- if (direct) return direct;
3182
- }
3183
- const anc = pairedAncestor(p);
3184
- if (anc) {
3185
- const under2 = cands.find((c) => ancestors(c.id).includes(anc));
3186
- if (under2) return under2;
3285
+ function specsUsage(specs, component) {
3286
+ const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
3287
+ return (path, r) => {
3288
+ const variants = [];
3289
+ const components = [];
3290
+ for (const c of specs.components) {
3291
+ for (const v of c.variants) {
3292
+ if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
3293
+ const key = variantKeyOf(v.props);
3294
+ if (c === own) {
3295
+ if (!r.variants.includes(key)) variants.push(key);
3296
+ } else if (!components.includes(c.name)) components.push(c.name);
3297
+ }
3187
3298
  }
3188
- return cands[0];
3299
+ return { variants, components };
3189
3300
  };
3190
- const root = staticParts.find((p) => !p.parent);
3191
- if (root) take(root.id, "root");
3192
- const textOf = (p) => {
3193
- if (p.characters) return normText(p.characters);
3194
- const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
3195
- return typeof prop?.default === "string" ? normText(prop.default) : "";
3196
- };
3197
- const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
3198
- const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
3199
- const shared = (p, m) => {
3200
- const mine = new Set(classesOf(p));
3201
- return mClasses(m).filter((c) => mine.has(c)).length;
3202
- };
3203
- const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
3204
- const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
3205
- for (const p of staticParts) {
3206
- if (p.kind !== "text" || pairs2.has(p.id)) continue;
3207
- const t = textOf(p);
3208
- if (!t) continue;
3209
- 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))));
3210
- if (hit) take(p.id, hit.id);
3211
- }
3212
- const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
3213
- for (const p of staticParts) {
3214
- if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
3215
- const sp = sParts.get(p.parent);
3216
- if (!sp || sp.kind !== "frame" || !sp.parent) continue;
3217
- const cls = classOf(sp);
3218
- const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
3219
- if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
3220
- }
3221
- for (const p of staticParts) {
3222
- if (pairs2.has(p.id) || !classesOf(p).length) continue;
3223
- if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
3224
- const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
3225
- const parent = p.parent ? pairs2.get(p.parent) : void 0;
3226
- const under2 = parent ? all.filter((m) => m.parent === parent) : [];
3227
- const cands = under2.length ? under2 : all;
3228
- const most = Math.max(0, ...cands.map((m) => shared(p, m)));
3229
- const hit = best(p, cands.filter((m) => shared(p, m) === most));
3230
- if (hit) take(p.id, hit.id);
3231
- }
3232
- for (const p of staticParts) {
3233
- if (pairs2.has(p.id)) continue;
3234
- if (p.parent && !pairs2.has(p.parent)) continue;
3235
- const parent = p.parent ? pairs2.get(p.parent) : void 0;
3236
- const known = p.kind === "text" && !!textOf(p);
3237
- const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
3238
- const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
3239
- if (hit) take(p.id, hit.id);
3240
- }
3241
- for (const p of staticParts) {
3242
- if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
3243
- const mp = pairs2.get(p.parent);
3244
- const t = textOf(p);
3245
- 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);
3246
- }
3247
- return pairs2;
3248
3301
  }
3249
- function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
3250
- const pairs2 = pairParts(staticParts, measured, o);
3251
- const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
3252
- const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
3253
- const near = (a, b) => {
3254
- if (a === void 0 || b === void 0) return false;
3255
- const na0 = num2(a);
3256
- const nb0 = num2(b);
3257
- if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
3258
- if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
3259
- const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
3260
- const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
3261
- return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
3262
- };
3263
- for (const sp of staticParts) {
3264
- const mid = pairs2.get(sp.id);
3265
- if (!mid) {
3266
- out.unmatchedParts.push(sp.id);
3267
- continue;
3268
- }
3269
- const sv = staticValues[sp.id] ?? {};
3270
- const mv = mid ? measured.values[mid] ?? {} : {};
3271
- const mr = mid ? measured.raw[mid] ?? {} : {};
3272
- const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
3273
- for (const f of fields) {
3274
- if ((f === "width" || f === "height") && sv[f] === void 0) continue;
3275
- const s = sv[f];
3276
- const m = mv[f] !== void 0 ? mv[f] : mr[f];
3277
- if (s === void 0) {
3278
- if (m !== void 0 && m !== 0) out.measuredOnly++;
3279
- continue;
3280
- }
3281
- if (m === void 0 && !(f in mr)) {
3282
- if (s === 0 || s === null) continue;
3283
- const r = typeof s === "string" && resolve ? resolve(s) : s;
3284
- if (r === 0 || r === "0px" || r === null) continue;
3285
- out.staticOnly++;
3286
- out.diffs.push({ part: sp.id, field: f, static: s, same: false });
3287
- continue;
3288
- }
3289
- out.compared++;
3290
- let same2 = s === m;
3291
- if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
3292
- if (same2 && typeof s === "string") out.sameToken++;
3293
- if (!same2 && resolve) {
3294
- let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
3295
- const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
3296
- if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
3297
- same2 = near(rs, mr[f]);
3298
- }
3299
- if (same2) out.same++;
3300
- else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
3302
+ function componentContrastPairs(edits, pairs2, tokens) {
3303
+ const out = /* @__PURE__ */ new Map();
3304
+ for (const e of edits) {
3305
+ const target = tokens.aliasOf(e.path);
3306
+ if (!target) continue;
3307
+ const group = e.path.split(".")[0];
3308
+ const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
3309
+ for (const p of pairs2) {
3310
+ if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
3311
+ if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
3301
3312
  }
3302
3313
  }
3303
- const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
3304
- const box = measured.boxes.root;
3305
- 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 } };
3306
- return out;
3307
- }
3308
- function tokenVariablePath(name, prefix) {
3309
- let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
3310
- if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
3311
- return parts.join("/");
3314
+ return [...out.values()];
3312
3315
  }
3313
- function sharedTokenPrefix(names2) {
3314
- if (names2.length < 20) return void 0;
3315
- const counts = /* @__PURE__ */ new Map();
3316
- for (const n2 of names2) {
3317
- const first = n2.replace(/^--/, "").split("-")[0];
3318
- counts.set(first, (counts.get(first) ?? 0) + 1);
3316
+ const FROM_FIGMA_LABEL = "from-figma";
3317
+ const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
3318
+ const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
3319
+ const FIGMA_SPEC_DIR = ".bridge/figma";
3320
+ const ORIGIN_MARK = "bridge-from-figma";
3321
+ const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
3322
+ function parseFigmaOrigin(body) {
3323
+ if (!body) return void 0;
3324
+ const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
3325
+ if (!m) return void 0;
3326
+ try {
3327
+ const o = JSON.parse(m[1]);
3328
+ return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
3329
+ } catch {
3330
+ return void 0;
3319
3331
  }
3320
- const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
3321
- return top && n / names2.length >= 0.6 ? top : void 0;
3322
- }
3323
- function markUsedTokens(tokens, stories) {
3324
- const byName = new Map(tokens.properties.map((p) => [p.name, p]));
3325
- const seen = /* @__PURE__ */ new Set();
3326
- const use = (name) => {
3327
- if (seen.has(name)) return;
3328
- seen.add(name);
3329
- const p = byName.get(name);
3330
- if (!p) return;
3331
- p.used = true;
3332
- for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
3333
- if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
3334
- };
3335
- for (const s of stories)
3336
- for (const spec of [s.spec, ...s.instances ?? []])
3337
- for (const part of Object.values(spec?.tokens ?? {}))
3338
- for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
3339
- const used = tokens.properties.filter((p) => p.used).length;
3340
- tokens.counts.used = used;
3341
- return used;
3342
3332
  }
3343
- class MeasuredSpecsError extends Error {
3333
+ function figmaOriginSection(o) {
3334
+ const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
3335
+ const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
3336
+ 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}.`;
3337
+ 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");
3344
3338
  }
3345
- const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3346
- function parseMeasuredSpecsFile(input) {
3347
- let data = input;
3348
- if (typeof input === "string") {
3349
- try {
3350
- data = JSON.parse(input);
3351
- } catch (e) {
3352
- throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
3353
- }
3354
- }
3355
- if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
3356
- if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
3357
- if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
3358
- if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
3359
- data.stories.forEach((s, i) => {
3360
- if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
3361
- const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
3362
- for (const sp of specs) {
3363
- 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`);
3364
- 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`);
3365
- }
3366
- if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
3367
- });
3368
- if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
3369
- if (data.tokens !== void 0) {
3370
- const t = data.tokens;
3371
- if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
3372
- 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");
3339
+ function withFigmaOrigin(body, o) {
3340
+ let b = (body ?? "").trim();
3341
+ const at = b.indexOf("**From Figma:**");
3342
+ const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
3343
+ if (at >= 0 && mark > at) {
3344
+ const end = b.indexOf("-->", mark);
3345
+ b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
3373
3346
  }
3374
- return data;
3375
- }
3376
- const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
3377
- const XYZ_D65_TO_LINEAR_SRGB = [
3378
- [3.2409699419045226, -1.537383177570094, -0.4986107602930034],
3379
- [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
3380
- [0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
3381
- ];
3382
- const D50_TO_D65 = [
3383
- [0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
3384
- [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
3385
- [0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
3386
- ];
3387
- const LINEAR_P3_TO_XYZ_D65 = [
3388
- [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
3389
- [0.2289745640697488, 0.6917385218365064, 0.079286914093745],
3390
- [0, 0.04511338185890264, 1.043944368900976]
3391
- ];
3392
- const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
3393
- const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
3394
- const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
3395
- function oklabToLinearSrgb(L, a, b) {
3396
- const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
3397
- const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
3398
- const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
3399
- 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];
3347
+ return `${figmaOriginSection(o)}${b ? `
3348
+
3349
+ ${b}` : ""}`;
3400
3350
  }
3401
- function labToXyzD50(L, a, b) {
3402
- const e = 216 / 24389;
3403
- const k = 24389 / 27;
3404
- const fy = (L + 16) / 116;
3405
- const fx = a / 500 + fy;
3406
- const fz = fy - b / 200;
3407
- const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
3408
- const y = L > k * e ? fy ** 3 : L / k;
3409
- const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
3410
- return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
3411
- }
3412
- function num(s, full = 1) {
3413
- if (s === void 0 || s === "none") return 0;
3414
- if (s.endsWith("%")) return parseFloat(s) / 100 * full;
3415
- if (/deg$/.test(s)) return parseFloat(s);
3416
- if (/turn$/.test(s)) return parseFloat(s) * 360;
3417
- if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
3418
- return parseFloat(s);
3419
- }
3420
- function linearSrgb(fn, args) {
3421
- switch (fn) {
3422
- case "oklab":
3423
- return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
3424
- case "oklch": {
3425
- const c = num(args[1], 0.4);
3426
- const h = num(args[2]) * Math.PI / 180;
3427
- return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
3428
- }
3429
- case "lab":
3430
- return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
3431
- case "lch": {
3432
- const c = num(args[1], 150);
3433
- const h = num(args[2]) * Math.PI / 180;
3434
- return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
3435
- }
3436
- case "color": {
3437
- const [space, ...c] = args;
3438
- const v = [num(c[0]), num(c[1]), num(c[2])];
3439
- if (space === "srgb") return v.map(toLinear);
3440
- if (space === "srgb-linear") return v;
3441
- if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
3442
- if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
3443
- if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
3444
- return void 0;
3445
- }
3446
- }
3447
- return void 0;
3448
- }
3449
- const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
3450
- function cssColorToRgb(fn, inner) {
3451
- const [main, alpha] = inner.split("/");
3452
- const args = main.trim().split(/[\s,]+/).filter(Boolean);
3453
- const lin = linearSrgb(fn.toLowerCase(), args);
3454
- if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
3455
- const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
3456
- const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
3457
- return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
3458
- }
3459
- function cssColorsToRgb(text) {
3460
- if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
3461
- return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
3462
- }
3463
- function withSrgbColors(value) {
3464
- if (typeof value === "string") return cssColorsToRgb(value);
3465
- if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
3466
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
3467
- return value;
3468
- }
3469
- function parseBoxShadow(css) {
3470
- if (!css || css.trim() === "none") return [];
3471
- const out = [];
3472
- for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
3473
- const color = /rgba?\(([^)]*)\)/i.exec(one);
3474
- const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
3475
- const a = c[3] ?? 1;
3476
- const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
3477
- if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
3478
- 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 } });
3479
- }
3480
- return out;
3481
- }
3482
- const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
3483
- const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
3484
- const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
3485
- function rawIssue(label, r, values2) {
3486
- const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
3487
- 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.`;
3488
- return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
3489
- }
3490
- function planComponentPush(d, ctx) {
3491
- const issues = [...d.unknown];
3492
- const reasons = [];
3493
- const edits = /* @__PURE__ */ new Map();
3494
- const also = /* @__PURE__ */ new Set();
3495
- for (const r of d.rebinds) {
3496
- const label = `${cap(r.kind)} in ${where(r)}`;
3497
- if (r.raw?.length) {
3498
- issues.push(rawIssue(label, r, r.raw));
3499
- continue;
3500
- }
3501
- if (!r.after.length) {
3502
- 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." });
3503
- continue;
3504
- }
3505
- const missing = r.after.filter((p) => !ctx.tierOf(p));
3506
- if (missing.length) {
3507
- 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." });
3508
- continue;
3509
- }
3510
- if (!r.before.length) {
3511
- reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
3512
- continue;
3513
- }
3514
- if (r.before.length !== 1 || r.after.length !== 1) {
3515
- reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
3516
- continue;
3517
- }
3518
- const [from, to] = [r.before[0], r.after[0]];
3519
- if (ctx.tierOf(from) !== "component") {
3520
- reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
3521
- continue;
3522
- }
3523
- const elsewhere = ctx.usedElsewhere(from, r);
3524
- if (elsewhere.variants.length) {
3525
- const v = elsewhere.variants;
3526
- const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
3527
- reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
3528
- continue;
3529
- }
3530
- for (const c of elsewhere.components) also.add(c);
3531
- const prev = edits.get(from);
3532
- if (prev && prev.value !== `{${to}}`) {
3533
- reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
3534
- edits.delete(from);
3535
- continue;
3536
- }
3537
- edits.set(from, { op: "set", path: from, value: `{${to}}` });
3538
- }
3539
- const list = [...edits.values()];
3540
- if (d.needsCode || reasons.length) {
3541
- return {
3542
- route: "claude-draft",
3543
- 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.` : ""}`,
3544
- edits: list,
3545
- reasons,
3546
- issues,
3547
- alsoChanges: [...also]
3548
- };
3549
- }
3550
- if (list.length) {
3551
- const n = list.length;
3552
- const others = [...also];
3553
- const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
3554
- 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"}.` : "";
3555
- 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 };
3556
- }
3557
- 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: [] };
3558
- }
3559
- function specsUsage(specs, component) {
3560
- const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
3561
- return (path, r) => {
3562
- const variants = [];
3563
- const components = [];
3564
- for (const c of specs.components) {
3565
- for (const v of c.variants) {
3566
- if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
3567
- const key = variantKeyOf(v.props);
3568
- if (c === own) {
3569
- if (!r.variants.includes(key)) variants.push(key);
3570
- } else if (!components.includes(c.name)) components.push(c.name);
3571
- }
3572
- }
3573
- return { variants, components };
3574
- };
3575
- }
3576
- function componentContrastPairs(edits, pairs2, tokens) {
3577
- const out = /* @__PURE__ */ new Map();
3578
- for (const e of edits) {
3579
- const target = tokens.aliasOf(e.path);
3580
- if (!target) continue;
3581
- const group = e.path.split(".")[0];
3582
- const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
3583
- for (const p of pairs2) {
3584
- if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
3585
- if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
3586
- }
3587
- }
3588
- return [...out.values()];
3589
- }
3590
- const FROM_FIGMA_LABEL = "from-figma";
3591
- const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
3592
- const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
3593
- const FIGMA_SPEC_DIR = ".bridge/figma";
3594
- const ORIGIN_MARK = "bridge-from-figma";
3595
- const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
3596
- function parseFigmaOrigin(body) {
3597
- if (!body) return void 0;
3598
- const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
3599
- if (!m) return void 0;
3600
- try {
3601
- const o = JSON.parse(m[1]);
3602
- return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
3603
- } catch {
3604
- return void 0;
3605
- }
3606
- }
3607
- function figmaOriginSection(o) {
3608
- const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
3609
- const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
3610
- 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}.`;
3611
- 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");
3612
- }
3613
- function withFigmaOrigin(body, o) {
3614
- let b = (body ?? "").trim();
3615
- const at = b.indexOf("**From Figma:**");
3616
- const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
3617
- if (at >= 0 && mark > at) {
3618
- const end = b.indexOf("-->", mark);
3619
- b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
3620
- }
3621
- return `${figmaOriginSection(o)}${b ? `
3622
-
3623
- ${b}` : ""}`;
3624
- }
3625
- const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
3626
- const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
3627
- const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
3628
- function formatFigmaSpec(d, plan) {
3629
- return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
3630
- `;
3351
+ const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
3352
+ const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
3353
+ const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
3354
+ function formatFigmaSpec(d, plan) {
3355
+ return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
3356
+ `;
3631
3357
  }
3632
3358
  function parseFigmaSpec(text) {
3633
3359
  if (!text) return void 0;
@@ -3704,1120 +3430,1599 @@ function figmaPushStatus(x) {
3704
3430
  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 };
3705
3431
  return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
3706
3432
  }
3707
- function parseTier(labels, body) {
3708
- const found = [];
3709
- for (const l of labels) {
3710
- const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
3711
- if (m) found.push(Number(m[1]));
3712
- }
3713
- for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
3714
- for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
3715
- return found.length ? Math.max(...found) : null;
3716
- }
3717
- function claudeStage(labels) {
3718
- const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
3719
- if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
3720
- if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
3721
- return null;
3722
- }
3723
- const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
3724
- function pullState(p) {
3725
- if (p.merged_at || p.merged) return "merged";
3726
- if (p.state && p.state !== "open") return "closed";
3727
- if (isDraft(p)) return "draft";
3728
- return "open";
3729
- }
3730
- const PULL_STATE_PILL = {
3731
- merged: { label: "Merged", cls: "" },
3732
- closed: { label: "Closed", cls: "" },
3733
- draft: { label: "Draft", cls: "" },
3734
- open: { label: "Open", cls: "" }
3433
+ const SUGGESTION_SCHEMA = "bridge.suggestion/1";
3434
+ const SUGGESTION_LABEL = "bridge-suggestion";
3435
+ const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
3436
+ const SUGGESTION_STATE_LABELS = {
3437
+ "needs-review": "suggestion: needs review",
3438
+ "changes-requested": "suggestion: changes requested",
3439
+ approved: "suggestion: approved",
3440
+ applying: "suggestion: applying"
3735
3441
  };
3736
- function openPulls(pulls) {
3737
- return pulls.filter((p) => {
3738
- const s = pullState(p);
3739
- return s === "open" || s === "draft";
3740
- });
3442
+ const SUGGESTION_FINAL_LABELS = {
3443
+ applied: "suggestion: applied",
3444
+ declined: "suggestion: declined"
3445
+ };
3446
+ const SUGGESTION_STATUS_LABEL = {
3447
+ "needs-review": "Needs review",
3448
+ "changes-requested": "Changes requested",
3449
+ approved: "Approved, ready to apply",
3450
+ applying: "Being applied",
3451
+ applied: "Applied",
3452
+ declined: "Declined",
3453
+ outdated: "Outdated"
3454
+ };
3455
+ const figmaNodeUrl = (fileKey, nodeId) => `https://www.figma.com/design/${fileKey}/?node-id=${encodeURIComponent(nodeId.replace(/:/g, "-"))}`;
3456
+ const SUGGESTION_LIMITS = {
3457
+ body: 65536,
3458
+ /** The readable part of the body (tables are cut with "and N more"). */
3459
+ human: 12e3,
3460
+ /** The JSON block in the body. */
3461
+ inline: 48e3,
3462
+ /** The JSON block of one continuation comment. */
3463
+ part: 6e4,
3464
+ /** Continuation comments at most (about 480 KB of file contents). */
3465
+ maxParts: 8
3466
+ };
3467
+ class SuggestionError extends Error {
3468
+ constructor(code, message) {
3469
+ super(message);
3470
+ this.code = code;
3471
+ this.name = "SuggestionError";
3472
+ }
3473
+ code;
3741
3474
  }
3742
- function isPlatformRefusal(s) {
3743
- return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
3475
+ const SUGGESTION_TOO_LARGE = "Too large for a suggestion: split it, or ask a developer to open it directly.";
3476
+ const utf8 = (text) => new TextEncoder().encode(text);
3477
+ const hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
3478
+ function padded(bytes) {
3479
+ const len = bytes.length;
3480
+ const total = Math.ceil((len + 9) / 64) * 64;
3481
+ const buf = new Uint8Array(total);
3482
+ buf.set(bytes);
3483
+ buf[len] = 128;
3484
+ const view = new DataView(buf.buffer);
3485
+ view.setUint32(total - 8, Math.floor(len / 536870912), false);
3486
+ view.setUint32(total - 4, len * 8 >>> 0, false);
3487
+ return view;
3744
3488
  }
3745
- const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
3746
- function summarizeCi(runs, statuses = []) {
3747
- const failed = [];
3748
- const pending = [];
3749
- let passed = 0;
3750
- for (const r of runs) {
3751
- if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
3752
- else if (r.conclusion && OK.has(r.conclusion)) passed++;
3753
- else failed.push({ name: r.name, url: r.html_url });
3754
- }
3755
- const seen = /* @__PURE__ */ new Set();
3756
- for (const s of statuses) {
3757
- if (seen.has(s.context)) continue;
3758
- seen.add(s.context);
3759
- if (isPlatformRefusal(s)) continue;
3760
- if (s.state === "success") passed++;
3761
- else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
3762
- else failed.push({ name: s.context, url: s.target_url ?? void 0 });
3489
+ const K256 = [
3490
+ 1116352408,
3491
+ 1899447441,
3492
+ 3049323471,
3493
+ 3921009573,
3494
+ 961987163,
3495
+ 1508970993,
3496
+ 2453635748,
3497
+ 2870763221,
3498
+ 3624381080,
3499
+ 310598401,
3500
+ 607225278,
3501
+ 1426881987,
3502
+ 1925078388,
3503
+ 2162078206,
3504
+ 2614888103,
3505
+ 3248222580,
3506
+ 3835390401,
3507
+ 4022224774,
3508
+ 264347078,
3509
+ 604807628,
3510
+ 770255983,
3511
+ 1249150122,
3512
+ 1555081692,
3513
+ 1996064986,
3514
+ 2554220882,
3515
+ 2821834349,
3516
+ 2952996808,
3517
+ 3210313671,
3518
+ 3336571891,
3519
+ 3584528711,
3520
+ 113926993,
3521
+ 338241895,
3522
+ 666307205,
3523
+ 773529912,
3524
+ 1294757372,
3525
+ 1396182291,
3526
+ 1695183700,
3527
+ 1986661051,
3528
+ 2177026350,
3529
+ 2456956037,
3530
+ 2730485921,
3531
+ 2820302411,
3532
+ 3259730800,
3533
+ 3345764771,
3534
+ 3516065817,
3535
+ 3600352804,
3536
+ 4094571909,
3537
+ 275423344,
3538
+ 430227734,
3539
+ 506948616,
3540
+ 659060556,
3541
+ 883997877,
3542
+ 958139571,
3543
+ 1322822218,
3544
+ 1537002063,
3545
+ 1747873779,
3546
+ 1955562222,
3547
+ 2024104815,
3548
+ 2227730452,
3549
+ 2361852424,
3550
+ 2428436474,
3551
+ 2756734187,
3552
+ 3204031479,
3553
+ 3329325298
3554
+ ];
3555
+ function sha256Hex(text) {
3556
+ const view = padded(utf8(text));
3557
+ const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
3558
+ const w = new Array(64);
3559
+ const rotr = (x, n) => x >>> n | x << 32 - n;
3560
+ for (let off = 0; off < view.byteLength; off += 64) {
3561
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
3562
+ for (let i = 16; i < 64; i++) {
3563
+ const a2 = w[i - 15];
3564
+ const b2 = w[i - 2];
3565
+ const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
3566
+ const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
3567
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
3568
+ }
3569
+ let [a, b, c, d, e, f, g, hh] = h;
3570
+ for (let i = 0; i < 64; i++) {
3571
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
3572
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
3573
+ hh = g;
3574
+ g = f;
3575
+ f = e;
3576
+ e = d + t1 | 0;
3577
+ d = c;
3578
+ c = b;
3579
+ b = a;
3580
+ a = t1 + t2 | 0;
3581
+ }
3582
+ h[0] = h[0] + a | 0;
3583
+ h[1] = h[1] + b | 0;
3584
+ h[2] = h[2] + c | 0;
3585
+ h[3] = h[3] + d | 0;
3586
+ h[4] = h[4] + e | 0;
3587
+ h[5] = h[5] + f | 0;
3588
+ h[6] = h[6] + g | 0;
3589
+ h[7] = h[7] + hh | 0;
3763
3590
  }
3764
- const total = passed + failed.length + pending.length;
3765
- const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
3766
- return { state, total, passed, failed, pending };
3591
+ return hex(h);
3767
3592
  }
3768
- const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
3769
- function summarizeApprovals(reviews, owners, author, rules = {}) {
3770
- const latest = /* @__PURE__ */ new Map();
3771
- const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
3772
- for (const r of sorted) {
3773
- const login = r.user?.login;
3774
- if (!login || login === author) continue;
3775
- if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
3776
- }
3777
- const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
3778
- const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
3779
- const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
3780
- const d = lower2(owners.design);
3781
- const t = lower2(owners.tech);
3782
- const a = lower2(approvers);
3783
- let design;
3784
- let tech;
3785
- const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
3786
- if (d.length || t.length) {
3787
- design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
3788
- const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
3789
- const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
3790
- tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
3791
- } else {
3792
- design = a.length >= 1;
3793
- tech = a.length >= 2;
3593
+ function sha1Bytes(bytes) {
3594
+ const view = padded(bytes);
3595
+ const h = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
3596
+ const w = new Array(80);
3597
+ const rotl = (x, n) => x << n | x >>> 32 - n;
3598
+ for (let off = 0; off < view.byteLength; off += 64) {
3599
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
3600
+ for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
3601
+ let [a, b, c, d, e] = h;
3602
+ for (let i = 0; i < 80; i++) {
3603
+ const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
3604
+ const t = rotl(a, 5) + f + e + k + w[i] | 0;
3605
+ e = d;
3606
+ d = c;
3607
+ c = rotl(b, 30);
3608
+ b = a;
3609
+ a = t;
3610
+ }
3611
+ h[0] = h[0] + a | 0;
3612
+ h[1] = h[1] + b | 0;
3613
+ h[2] = h[2] + c | 0;
3614
+ h[3] = h[3] + d | 0;
3615
+ h[4] = h[4] + e | 0;
3794
3616
  }
3795
- return { approvers, changesRequested, design, tech, owner };
3617
+ return hex(h);
3796
3618
  }
3797
- function lookAgainReviewers(reviews, headSha, author) {
3798
- if (!headSha) return [];
3799
- const latest = /* @__PURE__ */ new Map();
3800
- const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
3801
- for (const r of sorted) {
3802
- const login = r.user?.login;
3803
- if (!login || login === author) continue;
3804
- if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
3805
- }
3806
- const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
3807
- if (!asked.length) return [];
3808
- return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
3619
+ function gitBlobSha(text) {
3620
+ const body = utf8(text);
3621
+ const head = utf8(`blob ${body.length}\0`);
3622
+ const all = new Uint8Array(head.length + body.length);
3623
+ all.set(head);
3624
+ all.set(body, head.length);
3625
+ return sha1Bytes(all);
3809
3626
  }
3810
- function withSuggestionApprovals(pr, suggestion, prTier) {
3811
- if (!suggestion) return pr;
3812
- const s = suggestion.approvals;
3813
- if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
3814
- const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
3815
- if (!covered) return pr;
3816
- const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
3817
- return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
3627
+ function canonicalJson(value) {
3628
+ return JSON.stringify(sortKeys(value));
3818
3629
  }
3819
- function withFigmaPushApprovals(pr, push) {
3820
- if (!push || push.scope === "outside") return pr;
3821
- const tech = pr.tech || push.scope === "token-values";
3822
- return { ...pr, design: true, tech, owner: true };
3823
- }
3824
- function withTokenValueApprovals(pr, valuesOnly) {
3825
- if (!valuesOnly) return pr;
3826
- return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
3827
- }
3828
- const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
3829
- function canOverride(login, policy) {
3830
- if (!login || !policy?.allowed.length) return false;
3831
- return policy.allowed.map(normLogin).includes(normLogin(login));
3832
- }
3833
- function overrideHolders(policy, owners) {
3834
- const allowed = policy?.allowed ?? [];
3835
- const all = [...owners.design, ...owners.tech];
3836
- if (!all.length || all.some((o) => o.includes("/"))) return allowed;
3837
- const named = all.map(normLogin);
3838
- return allowed.filter((l) => named.includes(normLogin(l)));
3839
- }
3840
- const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
3841
- function mergeGate(input) {
3842
- const blocking = [];
3843
- const missing = [];
3844
- const approvalReasons = [];
3845
- const lifecycle = pullState(input);
3846
- if (lifecycle === "merged" || lifecycle === "closed") {
3847
- 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) };
3848
- }
3849
- if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
3850
- if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
3851
- if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
3852
- if (input.ci.state === "pending") blocking.push("Tests are still running.");
3853
- if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
3854
- const tier = input.tier ?? 3;
3855
- if (tier >= 2) {
3856
- if (!input.approvals.design) {
3857
- approvalReasons.push("It needs a design owner approval.");
3858
- missing.push("design approval");
3859
- }
3860
- if (!input.approvals.tech && !input.approvals.valuesOnly) {
3861
- approvalReasons.push("It needs a tech owner approval.");
3862
- missing.push("tech approval");
3630
+ function sortKeys(v) {
3631
+ if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
3632
+ if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
3633
+ const out = {};
3634
+ for (const k of Object.keys(v).sort()) {
3635
+ const x = v[k];
3636
+ if (x !== void 0) out[k] = sortKeys(x);
3863
3637
  }
3864
- } else if (!hasOwnerApproval(input.approvals)) {
3865
- approvalReasons.push("It needs one owner approval.");
3866
- missing.push("approval");
3638
+ return out;
3867
3639
  }
3868
- const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
3869
- const shipper = !input.viewer || canShip(input.viewer, input.ship);
3870
- const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
3871
- const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
3872
- const allowed = reasons.length === 0;
3873
- const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
3874
- return { allowed, reasons, overridable, missing, canShip: shipper };
3640
+ return v;
3875
3641
  }
3876
- function branchVerdict(input) {
3877
- const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
3878
- const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
3879
- const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
3880
- const override = ownerOverridePolicy(input.override, input.owners);
3881
- const state = input.state ?? "open";
3882
- const draft = isDraft(input);
3883
- const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
3884
- 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;
3885
- const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
3886
- const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed) && state === "open" && !input.merged_at && !input.exploration;
3887
- const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
3888
- return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
3642
+ function shaForm(p) {
3643
+ if (!p.files) return p;
3644
+ return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
3889
3645
  }
3890
- const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
3891
- const OVERRIDE_HEADING = "**Bridge owner override**";
3892
- function overrideReasonMin(policy) {
3893
- return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
3646
+ function suggestionPayloadSha(p) {
3647
+ return sha256Hex(canonicalJson(shaForm(p)));
3894
3648
  }
3895
- function overrideReasonProblem(reason, policy) {
3896
- const min = overrideReasonMin(policy);
3897
- const n = reason.trim().length;
3898
- if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
3899
- if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
3900
- return null;
3649
+ const KIND_SUBJECT = {
3650
+ "token-change": "Tokens",
3651
+ docs: "Docs",
3652
+ "component-spec": "Component",
3653
+ brand: "Brand",
3654
+ revert: "Revert",
3655
+ config: "Settings"
3656
+ };
3657
+ function suggestionSubject(p) {
3658
+ if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
3659
+ if (p.kind === "docs" && p.docs?.length) {
3660
+ const names2 = [...new Set(p.docs.map((d) => d.component))];
3661
+ return names2.length === 1 ? names2[0] : KIND_SUBJECT.docs;
3662
+ }
3663
+ return KIND_SUBJECT[p.kind];
3901
3664
  }
3902
- function overrideComment(login, reason, missing) {
3903
- const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
3904
- return `${OVERRIDE_HEADING}
3905
-
3906
- Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
3665
+ function suggestionIssueTitle(p) {
3666
+ const names2 = [...(p.docs ?? []).map((d) => d.component), ...p.componentSpec?.component ? [p.componentSpec.component] : []];
3667
+ return subjectTitle(suggestionSubject(p), p.title, names2);
3907
3668
  }
3908
- function overrideRefusedComment(body, message) {
3909
- return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
3669
+ function suggestionLabels(p, opts = {}) {
3670
+ return [
3671
+ .../* @__PURE__ */ new Set([
3672
+ SUGGESTION_LABEL,
3673
+ `suggestion: ${p.kind}`,
3674
+ `source: ${p.source}`,
3675
+ `tier: ${p.tier.tier}`,
3676
+ SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
3677
+ ...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
3678
+ ...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
3679
+ ...p.links?.exploration ? [EXPLORATION_LABEL] : [],
3680
+ ...opts.labels ?? []
3681
+ ])
3682
+ ];
3910
3683
  }
3911
- function parseOverrideComment(body) {
3912
- const text = (body ?? "").trim();
3913
- if (!text.startsWith(OVERRIDE_HEADING)) return null;
3914
- const m = text.match(/Owner override by @([\w-]+): ([\s\S]*?)\. Missing: ([^.]*)\. (Merged from Bridge\.|Merge refused by GitHub)/);
3915
- if (!m) return null;
3916
- return { login: m[1], reason: m[2], missing: m[3].split(/,\s*/).filter(Boolean), merged: m[4].startsWith("Merged") };
3684
+ function suggestionKindFromLabels(labels) {
3685
+ for (const l of labels) {
3686
+ const m = /^suggestion: (.+)$/.exec(l);
3687
+ if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
3688
+ }
3689
+ return void 0;
3917
3690
  }
3918
- function allowedMergeMethods(repo) {
3919
- const out = [];
3920
- if (repo?.allow_squash_merge !== false) out.push("squash");
3921
- if (repo?.allow_merge_commit !== false) out.push("merge");
3922
- if (repo?.allow_rebase_merge !== false) out.push("rebase");
3923
- return out;
3691
+ function suggestionStateFromLabels(labels) {
3692
+ return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
3924
3693
  }
3925
- function hasWriteAccess(perms) {
3926
- return Boolean(perms && (perms.admin || perms.maintain || perms.push));
3694
+ function suggestionIsTokenValues(p) {
3695
+ if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
3696
+ if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
3697
+ return (p.files ?? []).every((f) => f.path.endsWith(".md"));
3927
3698
  }
3928
- const REVIEW_STATUS_ORDER = {
3929
- "ci-failing": 0,
3930
- conflict: 1,
3931
- "changes-requested": 2,
3932
- "needs-design": 3,
3933
- "needs-tech": 4,
3934
- "needs-approval": 5,
3935
- ready: 6,
3936
- "ci-running": 7,
3937
- checking: 8,
3938
- unknown: 9,
3939
- draft: 10,
3940
- merged: 11,
3941
- closed: 12
3942
- };
3943
- function reviewStatusKind(input) {
3944
- const out = (kind) => kind;
3945
- const lifecycle = pullState(input);
3946
- if (lifecycle === "merged") return out("merged");
3947
- if (lifecycle === "closed") return out("closed");
3948
- if (lifecycle === "draft") return out("draft");
3949
- if (input.ci?.state === "fail") return out("ci-failing");
3950
- if (input.approvals?.changesRequested.length) return out("changes-requested");
3951
- if (input.mergeable === false) return out("conflict");
3952
- if (input.ci?.state === "pending") return out("ci-running");
3953
- if (!input.ci || !input.approvals) return out("unknown");
3954
- const tier = input.tier ?? 3;
3955
- if (tier >= 2) {
3956
- if (!input.approvals.design) return out("needs-design");
3957
- if (!input.approvals.tech) return out("needs-tech");
3958
- } else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
3959
- if (input.mergeable === null) return out("checking");
3960
- 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 });
3961
- return gate.allowed ? out("ready") : out("unknown");
3699
+ function suggestionStatus(i) {
3700
+ const prs = i.linkedPrs ?? [];
3701
+ if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
3702
+ if (prs.some((p) => p.state === "open") || i.applied) return "applying";
3703
+ if (i.outdated) return "outdated";
3704
+ return i.review ?? "needs-review";
3962
3705
  }
3963
- const REVIEW_STATUS_LABEL = {
3964
- merged: "Merged",
3965
- closed: "Closed",
3966
- draft: "Draft",
3967
- "ci-failing": "Blocked by tests",
3968
- "changes-requested": "Changes requested",
3969
- "ci-running": "Tests running",
3970
- "needs-design": "Needs design approval",
3971
- "needs-tech": "Needs tech approval",
3972
- "needs-approval": "Needs approval",
3973
- conflict: "Conflict with main",
3974
- checking: "Checking for conflicts",
3975
- ready: "Ready to merge",
3976
- unknown: "Status unknown"
3977
- };
3978
- const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
3979
- function reviewStatusLabel(kind, lookAgain) {
3980
- return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
3706
+ function withOutdated(status, outdated) {
3707
+ return outdated && (status === "needs-review" || status === "changes-requested" || status === "approved") ? "outdated" : status;
3981
3708
  }
3982
- const CI_STATE_LABEL = {
3983
- pass: "Tests pass",
3984
- fail: "Tests fail",
3985
- pending: "Tests running",
3986
- none: "No automated checks"
3987
- };
3988
- const BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
3989
- const isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
3990
- function isAutomationPull(p) {
3991
- return isReleasePull(p) || BOTS.has((p.user?.login ?? "").toLowerCase());
3709
+ function suggestionStateLabel(status) {
3710
+ if (status === "applied" || status === "declined") return SUGGESTION_FINAL_LABELS[status];
3711
+ return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
3992
3712
  }
3993
- const runningExploration = (p) => p.state === "open" && isActiveExploration(p.labels ?? []);
3994
- function reviewQueue(pulls) {
3995
- return openPulls(pulls).filter((p) => !isAutomationPull(p) && !runningExploration(p));
3713
+ const SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
3714
+ function suggestionLabelChanges(labels, status) {
3715
+ if (status === "outdated") return { add: [], remove: [] };
3716
+ const want = suggestionStateLabel(status);
3717
+ return {
3718
+ add: want && !labels.includes(want) ? [want] : [],
3719
+ remove: SUGGESTION_STATUS_LABELS.filter((l) => l !== want && labels.includes(l))
3720
+ };
3996
3721
  }
3997
- function releasePull(pulls) {
3998
- return openPulls(pulls).find((p) => isReleasePull(p)) ?? null;
3722
+ const bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
3723
+ const MARK = "bridge-suggestion";
3724
+ const PART_SCHEMA = "bridge.suggestion-part/1";
3725
+ const cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
3726
+ const safeText = (s) => s.replace(/<!--/g, "&lt;!--");
3727
+ const sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
3728
+ const show$1 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
3729
+ function table(head, rows, max) {
3730
+ if (!rows.length) return [];
3731
+ const shown = rows.slice(0, max);
3732
+ return [
3733
+ `| ${head.join(" | ")} |`,
3734
+ `| ${head.map(() => "---").join(" | ")} |`,
3735
+ ...shown.map((r) => `| ${r.map((c) => cell(c)).join(" | ")} |`),
3736
+ ...rows.length > max ? ["", `and ${rows.length - max} more`] : []
3737
+ ];
3999
3738
  }
4000
- function effectiveTier(labels, body, files) {
4001
- const labelled = parseTier(labels, body);
4002
- if (!files) return labelled;
4003
- const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
4004
- return Math.max(labelled ?? 0, computed);
4005
- }
4006
- const norm$1 = (x) => x.trim().toLowerCase().replace(/^@/, "");
4007
- function reopenEligibility(x) {
4008
- if (x.state !== "closed" || !x.viewer) return { kind: "none" };
4009
- const isAuthor = norm$1(x.viewer) === norm$1(x.author);
4010
- const isOwner = x.owners.some((o) => norm$1(o) === norm$1(x.viewer));
4011
- if (!isAuthor && !isOwner) return { kind: "none" };
4012
- if (!x.canWrite && !isAuthor) return { kind: "none" };
4013
- if (x.branch === "exists") return { kind: "reopen" };
4014
- if (x.branch === "gone") return { kind: "gone" };
4015
- return { kind: "none" };
4016
- }
4017
- const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
4018
- const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
4019
- function requiredApprovals(tier, valuesOnly) {
4020
- 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]);
4021
- }
4022
- function pendingApprovals(tier, approvals) {
4023
- if (tier == null || !approvals) return [];
4024
- return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
4025
- }
4026
- const uniqLogins = (xs) => xs.filter((x, i) => xs.findIndex((y) => normLogin(y) === normLogin(x)) === i);
4027
- function eligibleReviewers(role, p) {
4028
- const raw = role === "design" ? p.owners.design : role === "tech" ? p.owners.tech : uniqLogins([...p.owners.design, ...p.owners.tech]);
4029
- const listed = raw.map((x) => x.replace(/^@/, ""));
4030
- const approvers = (p.approvers ?? []).map(normLogin);
4031
- const logins = listed.filter((l) => normLogin(l) !== normLogin(p.author) && !approvers.includes(normLogin(l)));
4032
- return { open: listed.length === 0, logins, listed };
4033
- }
4034
- function reviewersToAsk(x) {
4035
- const roles = pendingApprovals(x.tier, x.approvals);
4036
- const all = roles.flatMap((r) => eligibleReviewers(r, { owners: x.owners, author: x.author, approvers: x.approvals.approvers }).logins);
4037
- return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
4038
- }
4039
- const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6}$/;
4040
- function parseJiraKey(input) {
4041
- const t = (input ?? "").trim().toUpperCase();
4042
- if (!t) return null;
4043
- if (!JIRA_KEY.test(t)) throw new Error("Write the Jira ticket as its key, like DS-42, or leave it empty.");
4044
- return t;
4045
- }
4046
- const isJiraKey = (input) => JIRA_KEY.test(input.trim().toUpperCase());
4047
- const LEADING_KEY = /^\[?([A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6})\]?[:\s]\s*/;
4048
- function jiraKeyOf(title) {
4049
- return LEADING_KEY.exec(title.trim())?.[1] ?? null;
4050
- }
4051
- const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
4052
- function titleWithJiraKey(title, key) {
4053
- if (!key) return title;
4054
- return `${key} ${withoutJiraKey(title)}`;
4055
- }
4056
- function branchWithJiraKey(branch, key) {
4057
- if (!key || branch.includes(key)) return branch;
4058
- const i = branch.lastIndexOf("/");
4059
- return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
3739
+ function changesSection(p, max) {
3740
+ const out = [];
3741
+ if (p.tokens?.changes.length) {
3742
+ const rows = p.tokens.changes.map((c) => [
3743
+ `\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
3744
+ c.beforeCss ?? show$1(c.before),
3745
+ c.afterCss ?? show$1(c.after)
3746
+ ]);
3747
+ out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
3748
+ } else if (p.tokens?.edits.length) {
3749
+ const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show$1(e.value)]);
3750
+ out.push(...table(["Token", "Edit", "Value"], rows, max), "");
3751
+ }
3752
+ if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
3753
+ if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
3754
+ if (p.files?.length) {
3755
+ const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
3756
+ out.push(...table(["File", "Change"], rows, max), "");
3757
+ }
3758
+ return out;
4060
3759
  }
4061
- const SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
4062
- const SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
4063
- const EVENTS = ["approve", "request-changes", "comment"];
4064
- const ROLE_TEXT = { design: "for design", tech: "for tech", approval: "" };
4065
- function composeSuggestionReviewComment(r) {
4066
- const who = r.by ? ` by @${r.by.replace(/^@/, "")}` : "";
4067
- const role = r.role ? ROLE_TEXT[r.role] : "";
4068
- const head = r.event === "approve" ? `Approved${role ? ` ${role}` : ""}${who} in Bridge.` : r.event === "request-changes" ? `Changes requested${who} in Bridge.` : `Comment${who} in Bridge.`;
4069
- const quote = r.message?.trim() ? r.message.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n") : "";
4070
- const record = { event: r.event, ...r.role ? { role: r.role } : {}, payloadSha: r.payloadSha, at: r.at };
4071
- return [head, ...quote ? ["", quote] : [], "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_REVIEW_MARK} ${JSON.stringify(record)} -->`].join("\n");
3760
+ function checksSection(p, max) {
3761
+ const c = p.checks;
3762
+ const failing = c.contrast.filter((x) => !x.pass);
3763
+ const rows = [...failing, ...c.contrast.filter((x) => x.pass)].map((x) => [x.pair, x.mode ?? "", x.before.toFixed(2), x.after.toFixed(2), String(x.min), x.pass ? "pass" : "**fails**"]);
3764
+ return [
3765
+ "## Checks when suggested",
3766
+ "",
3767
+ `- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
3768
+ `- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
3769
+ `- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
3770
+ `- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
3771
+ "",
3772
+ ...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
3773
+ ];
4072
3774
  }
4073
- function parseSuggestionReviewComment(body) {
4074
- const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4075
- if (!m) return null;
4076
- try {
4077
- const r = JSON.parse(m[1]);
4078
- if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
4079
- return {
4080
- event: r.event,
4081
- ...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
4082
- payloadSha: r.payloadSha,
4083
- at: typeof r.at === "string" ? r.at : ""
4084
- };
4085
- } catch {
4086
- return null;
3775
+ function linksSection(p) {
3776
+ const l = p.links ?? {};
3777
+ const out = [];
3778
+ if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell(l.neededBy.title)}](${l.neededBy.url})`);
3779
+ if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
3780
+ const key = l.figma?.fileKey;
3781
+ const nodes = (l.figma?.nodes ?? []).filter((n) => n && typeof n.nodeId === "string");
3782
+ for (const n of nodes) {
3783
+ const url = n.url ?? (key ? figmaNodeUrl(key, n.nodeId) : void 0);
3784
+ const what = `${n.state === "before" ? "Before" : n.state === "after" ? "After" : "Node"}${n.name ? ` (${cell(n.name)})` : ""}`;
3785
+ out.push(`- ${what}: ${url ? `[node ${n.nodeId}](${url})` : `node ${n.nodeId}`}`);
3786
+ }
3787
+ if (key && (nodes.length || l.figma?.nodeId)) {
3788
+ const ids = nodes.length ? nodes.map((n) => n.nodeId) : [l.figma.nodeId];
3789
+ out.push(
3790
+ `- Claude Code reads this design straight from Figma: the Figma MCP server (\`get_design_context\` with file key \`${key}\` and node ${ids.map((i) => `\`${i}\``).join(", ")}) or the REST API (\`GET https://api.figma.com/v1/files/${key}/nodes?ids=${ids.map(encodeURIComponent).join(",")}\`). No image hosting is needed.`
3791
+ );
4087
3792
  }
3793
+ if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
3794
+ if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
3795
+ for (const v of p.previews ?? []) out.push(`- ${cell(v.label)}: ${v.url}`);
3796
+ return out.length ? ["## Previews and links", "", ...out, ""] : [];
4088
3797
  }
4089
- function composeSuggestionAppliedComment(r) {
4090
- const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
4091
- const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
3798
+ function humanPart(p, details, max, repo) {
4092
3799
  return [
4093
- `Being applied ${where2}${r.by ? ` by @${r.by.replace(/^@/, "")}` : ""}.${r.forced ? " Applied before every approval was in." : ""}`,
3800
+ `## Suggested change: ${safeText(p.title.trim())}`,
4094
3801
  "",
4095
- `<!-- ${SUGGESTION_APPLIED_MARK} ${JSON.stringify(record)} -->`
3802
+ safeText(p.summary.trim()),
3803
+ "",
3804
+ ...changesSection(p, max),
3805
+ ...details?.trim() ? [safeText(details.trim()), ""] : [],
3806
+ ...checksSection(p, max),
3807
+ ...linksSection(p),
3808
+ "## How to apply",
3809
+ "",
3810
+ "- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
3811
+ '- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
3812
+ ` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
3813
+ "",
3814
+ "The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
3815
+ "",
3816
+ `<sub>Suggested from ${sourceLabel(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
4096
3817
  ].join("\n");
4097
3818
  }
4098
- function parseSuggestionAppliedComment(body) {
4099
- const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4100
- if (!m) return null;
4101
- try {
4102
- const r = JSON.parse(m[1]);
4103
- return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
4104
- } catch {
4105
- return null;
3819
+ function fitHuman(p, details, repo) {
3820
+ for (const max of [50, 20, 8, 3]) {
3821
+ const h2 = humanPart(p, details, max, repo);
3822
+ if (h2.length <= SUGGESTION_LIMITS.human) return h2;
4106
3823
  }
3824
+ const h = humanPart(p, void 0, 3, repo);
3825
+ if (h.length <= SUGGESTION_LIMITS.human) return h;
3826
+ const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
3827
+ return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
4107
3828
  }
4108
- const norm = (l) => l.trim().replace(/^@/, "").toLowerCase();
4109
- const COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
4110
- function summarizeSuggestionApprovals(comments, gov, opts) {
4111
- const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
4112
- const owners = new Set([...lists.design, ...lists.tech].map(norm));
4113
- const anyOwner = owners.size > 0;
4114
- const reviews = [];
4115
- const latestOld = /* @__PURE__ */ new Map();
4116
- const ignored = /* @__PURE__ */ new Set();
4117
- const ordered = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
4118
- ordered.forEach(({ c }, i) => {
4119
- const r = parseSuggestionReviewComment(c.body);
4120
- if (!r || r.event === "comment" || !c.author) return;
4121
- const login = norm(c.author);
4122
- if (opts.author && login === norm(opts.author)) return;
4123
- const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
4124
- if (!counts) {
4125
- ignored.add(c.author);
4126
- return;
4127
- }
4128
- const at = String(i).padStart(6, "0");
4129
- if (r.payloadSha !== opts.payloadSha) {
4130
- latestOld.set(c.author, r.event);
4131
- reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
4132
- return;
4133
- }
4134
- latestOld.delete(c.author);
4135
- reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
4136
- });
4137
- const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
4138
- const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
4139
- return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
3829
+ const pretty = (v) => JSON.stringify(sortKeys(v), null, 2);
3830
+ const fit = (v) => {
3831
+ const p = pretty(v);
3832
+ return p.length <= SUGGESTION_LIMITS.inline ? p : canonicalJson(v);
3833
+ };
3834
+ const fenced = (json) => ["```json", json, "```"].join("\n");
3835
+ function payloadBlock(json, sha, parts) {
3836
+ return [
3837
+ "<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
3838
+ "",
3839
+ `<!-- ${MARK}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
3840
+ fenced(json),
3841
+ "",
3842
+ "</details>"
3843
+ ].join("\n");
4140
3844
  }
4141
- function suggestionReviewState(tier, approvals) {
4142
- if (approvals.changesRequested.length) return "changes-requested";
4143
- return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
3845
+ function partComment(i, n, json) {
3846
+ return [
3847
+ `<details><summary>Machine-readable change, part ${i} of ${n} (Bridge suggestion v1)</summary>`,
3848
+ "",
3849
+ `<!-- ${MARK}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
3850
+ fenced(json),
3851
+ "",
3852
+ "</details>"
3853
+ ].join("\n");
4144
3854
  }
4145
- const SUGGESTION_SCHEMA = "bridge.suggestion/1";
4146
- const SUGGESTION_LABEL = "bridge-suggestion";
4147
- const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
4148
- const SUGGESTION_STATE_LABELS = {
4149
- "needs-review": "suggestion: needs review",
4150
- "changes-requested": "suggestion: changes requested",
4151
- approved: "suggestion: approved",
4152
- applying: "suggestion: applying"
4153
- };
4154
- const SUGGESTION_FINAL_LABELS = {
4155
- applied: "suggestion: applied",
4156
- declined: "suggestion: declined"
4157
- };
4158
- const SUGGESTION_STATUS_LABEL = {
4159
- "needs-review": "Needs review",
4160
- "changes-requested": "Changes requested",
4161
- approved: "Approved, ready to apply",
4162
- applying: "Being applied",
4163
- applied: "Applied",
4164
- declined: "Declined",
4165
- outdated: "Outdated"
4166
- };
4167
- const figmaNodeUrl = (fileKey, nodeId) => `https://www.figma.com/design/${fileKey}/?node-id=${encodeURIComponent(nodeId.replace(/:/g, "-"))}`;
4168
- const SUGGESTION_LIMITS = {
4169
- body: 65536,
4170
- /** The readable part of the body (tables are cut with "and N more"). */
4171
- human: 12e3,
4172
- /** The JSON block in the body. */
4173
- inline: 48e3,
4174
- /** The JSON block of one continuation comment. */
4175
- part: 6e4,
4176
- /** Continuation comments at most (about 480 KB of file contents). */
4177
- maxParts: 8
3855
+ function escapedEnd(s, from, budget) {
3856
+ let used = 0;
3857
+ let i = from;
3858
+ while (i < s.length) {
3859
+ const c = s.charCodeAt(i);
3860
+ let w = 1;
3861
+ let len = 1;
3862
+ if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
3863
+ else if (c < 32) len = 6;
3864
+ else if (c >= 55296 && c <= 56319) {
3865
+ const d = s.charCodeAt(i + 1);
3866
+ if (d >= 56320 && d <= 57343) {
3867
+ w = 2;
3868
+ len = 2;
3869
+ } else len = 6;
3870
+ } else if (c >= 56320 && c <= 57343) len = 6;
3871
+ if (used + len > budget) break;
3872
+ used += len;
3873
+ i += w;
3874
+ }
3875
+ return i;
3876
+ }
3877
+ function composeSuggestionIssue(payload, opts = {}) {
3878
+ const problems = suggestionShapeProblems(payload);
3879
+ if (problems.length) throw new SuggestionError("invalid", problems.join(" "));
3880
+ if (payload.checks.blocked) throw new SuggestionError("invalid", `This change cannot be suggested: ${payload.checks.blocked}`);
3881
+ const full = shaForm(payload);
3882
+ const sha = suggestionPayloadSha(full);
3883
+ const human = fitHuman(full, opts.details, opts.repo);
3884
+ const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
3885
+ const inline = fit(full);
3886
+ if (inline.length <= SUGGESTION_LIMITS.inline) {
3887
+ return { ...base, body: `${human}
3888
+
3889
+ ${payloadBlock(inline, sha, 0)}
3890
+ `, parts: [] };
3891
+ }
3892
+ const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
3893
+ const parts = [];
3894
+ const firstPart = /* @__PURE__ */ new Map();
3895
+ const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
3896
+ const emptyOverhead = partJson([], 9, 9).length;
3897
+ let cur = [];
3898
+ let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
3899
+ for (const { f, i } of moved) {
3900
+ const text = f.content;
3901
+ let at = 0;
3902
+ while (at < text.length) {
3903
+ const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
3904
+ if (room - entryOverhead < 256) {
3905
+ parts.push(cur);
3906
+ cur = [];
3907
+ room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
3908
+ }
3909
+ const end = escapedEnd(text, at, room - entryOverhead);
3910
+ if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
3911
+ if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
3912
+ const chunk = { i, path: f.path, text: text.slice(at, end) };
3913
+ cur.push(chunk);
3914
+ room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
3915
+ at = end;
3916
+ }
3917
+ }
3918
+ if (cur.length) parts.push(cur);
3919
+ if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
3920
+ const skeleton = {
3921
+ ...full,
3922
+ files: (full.files ?? []).map((f, i) => {
3923
+ if (!firstPart.has(i)) return f;
3924
+ const { content: _content, ...rest } = f;
3925
+ return { ...rest, part: firstPart.get(i) };
3926
+ })
3927
+ };
3928
+ const skeletonJson = fit(skeleton);
3929
+ if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
3930
+ const n = parts.length;
3931
+ const partBodies = parts.map((chunks, k) => {
3932
+ const json = partJson(chunks, k + 1, n);
3933
+ if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
3934
+ return partComment(k + 1, n, json);
3935
+ });
3936
+ const body = `${human}
3937
+
3938
+ ${payloadBlock(skeletonJson, sha, n)}
3939
+ `;
3940
+ if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
3941
+ return { ...base, body, parts: partBodies };
3942
+ }
3943
+ function hasSuggestionBlock(body) {
3944
+ return new RegExp(`^<!-- ${MARK}:v\\d+ sha256=`, "m").test(body ?? "");
3945
+ }
3946
+ function fencedAfter(text, from) {
3947
+ const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
3948
+ return m?.[1];
3949
+ }
3950
+ const fail = (reason, message) => ({ ok: false, reason, message });
3951
+ function parseSuggestionIssue(body, opts = {}) {
3952
+ const text = body ?? "";
3953
+ const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
3954
+ const marker = markers[markers.length - 1];
3955
+ if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
3956
+ if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
3957
+ const json = fencedAfter(text, marker.index + marker[0].length);
3958
+ if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
3959
+ let raw;
3960
+ try {
3961
+ raw = JSON.parse(json);
3962
+ } catch {
3963
+ return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
3964
+ }
3965
+ const schema = raw?.schema;
3966
+ if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
3967
+ const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
3968
+ return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
3969
+ }
3970
+ const problems = suggestionShapeProblems(raw);
3971
+ if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
3972
+ const payload = raw;
3973
+ const markedSha = marker[2];
3974
+ const expected = marker[3] ? Number(marker[3]) : 0;
3975
+ let edited = false;
3976
+ if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
3977
+ const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
3978
+ const found = /* @__PURE__ */ new Map();
3979
+ const author = opts.author?.toLowerCase();
3980
+ for (const c of opts.comments ?? []) {
3981
+ if (author && c.author && c.author.toLowerCase() !== author) continue;
3982
+ const m = new RegExp(`^<!-- ${MARK}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
3983
+ if (!m) continue;
3984
+ const i = Number(m[1]);
3985
+ if (found.has(i)) continue;
3986
+ const pj = fencedAfter(c.body, m.index + m[0].length);
3987
+ if (pj === void 0) continue;
3988
+ if (sha256Hex(pj) !== m[3]) edited = true;
3989
+ try {
3990
+ const part = JSON.parse(pj);
3991
+ if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
3992
+ found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
3993
+ } catch {
3994
+ continue;
3995
+ }
3996
+ }
3997
+ for (let i = 1; i <= want; i++) {
3998
+ if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
3999
+ }
4000
+ const files = (payload.files ?? []).map((f, idx) => {
4001
+ if (f.part === void 0) return f;
4002
+ let content = "";
4003
+ for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
4004
+ const { part: _part, ...rest } = f;
4005
+ return { ...rest, content };
4006
+ });
4007
+ payload.files = files;
4008
+ }
4009
+ const sha = suggestionPayloadSha(payload);
4010
+ return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
4011
+ }
4012
+ function suggestionShapeProblems(raw) {
4013
+ const out = [];
4014
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
4015
+ const p = raw;
4016
+ if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
4017
+ if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
4018
+ if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
4019
+ if (typeof p.summary !== "string") out.push('"summary" must be text.');
4020
+ if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
4021
+ if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
4022
+ const base = p.base;
4023
+ if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
4024
+ const tier = p.tier;
4025
+ if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
4026
+ const checks = p.checks;
4027
+ if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
4028
+ out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
4029
+ const tokens = p.tokens;
4030
+ if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
4031
+ if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
4032
+ const spec = p.componentSpec;
4033
+ if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
4034
+ const nodes = p.links?.figma?.nodes;
4035
+ if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
4036
+ if (p.files !== void 0) {
4037
+ if (!Array.isArray(p.files)) out.push('"files" must be a list.');
4038
+ else {
4039
+ const seen = /* @__PURE__ */ new Set();
4040
+ for (const f of p.files) {
4041
+ const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
4042
+ if (!ok) {
4043
+ out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
4044
+ break;
4045
+ }
4046
+ if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
4047
+ seen.add(f.path);
4048
+ }
4049
+ }
4050
+ }
4051
+ return out;
4052
+ }
4053
+ function unsafePathProblem(path) {
4054
+ if (typeof path !== "string" || !path.length) return "A path is empty.";
4055
+ if (/[\0-\x1f\x7f]/.test(path)) return `"${path.replace(/[\0-\x1f\x7f]/g, "?")}" has control characters.`;
4056
+ if (path.includes("\\")) return `"${path}" uses backslashes: paths use forward slashes.`;
4057
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.startsWith("~")) return `"${path}" is absolute: paths are relative to the repository root.`;
4058
+ const segs = path.split("/");
4059
+ if (segs.some((s) => s === "" || s === "." || s === "..")) return `"${path}" has an empty, "." or ".." part: paths stay inside the repository.`;
4060
+ if (segs.some((s) => s.toLowerCase() === ".git")) return `"${path}" is inside .git.`;
4061
+ return null;
4062
+ }
4063
+ const under = (path, root) => {
4064
+ const r = root.replace(/\/+$/, "");
4065
+ return !r || r === "." || path === r || path.startsWith(`${r}/`);
4178
4066
  };
4179
- class SuggestionError extends Error {
4180
- constructor(code, message) {
4181
- super(message);
4182
- this.code = code;
4183
- this.name = "SuggestionError";
4067
+ function isGuardrailPath(path, guardrails) {
4068
+ return guardrails.some((g) => g.endsWith("/") ? path.startsWith(g) || path === g.slice(0, -1) : path === g);
4069
+ }
4070
+ function suggestionAllowedRoots(kind, config) {
4071
+ const tokenRoot2 = tokensPath(config) || ".";
4072
+ const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
4073
+ switch (kind) {
4074
+ case "token-change":
4075
+ return [tokenRoot2, ...changesets];
4076
+ case "docs":
4077
+ return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
4078
+ case "component-spec":
4079
+ return [".bridge/figma"];
4080
+ case "brand":
4081
+ return [tokenRoot2, ...changesets, CONFIG_FILE];
4082
+ case "config":
4083
+ return [CONFIG_FILE];
4084
+ case "revert":
4085
+ return void 0;
4086
+ }
4087
+ }
4088
+ function suggestionPathProblems(p, config) {
4089
+ const out = [];
4090
+ const roots = suggestionAllowedRoots(p.kind, config);
4091
+ const guardrails = config.governance.guardrails;
4092
+ const check = (path) => {
4093
+ const bad = unsafePathProblem(path);
4094
+ if (bad) return out.push(bad);
4095
+ if (roots && !roots.some((r) => under(path, r))) return out.push(`"${path}" is outside what a ${p.kind} suggestion may change (${roots.join(", ")}).`);
4096
+ const configAllowed = path === CONFIG_FILE && (p.kind === "config" || p.kind === "brand");
4097
+ if (!configAllowed && isGuardrailPath(path, guardrails)) return out.push(`"${path}" is a guardrail file: change it in a pull request, not a suggestion.`);
4098
+ return 0;
4099
+ };
4100
+ for (const f of p.files ?? []) check(f.path);
4101
+ for (const d of p.docs ?? []) check(d.path);
4102
+ if (p.componentSpec) check(p.componentSpec.specPath);
4103
+ for (const e of p.tokens?.edits ?? []) {
4104
+ if (e.file === void 0) continue;
4105
+ const bad = unsafePathProblem(e.file);
4106
+ if (bad) out.push(bad);
4107
+ }
4108
+ return [...new Set(out)];
4109
+ }
4110
+ async function linkRequestToExplorationOnGitHub(gh, n, branch, opts = {}) {
4111
+ if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
4112
+ const issue = await gh.getIssue(n);
4113
+ const body = withRequestExploration(issue.body, branch);
4114
+ if (!body) throw new GitHubError(422, `#${n} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
4115
+ await gh.updateIssue(n, { body });
4116
+ const warnings = [];
4117
+ if (!await gh.addLabels(n, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n}.`);
4118
+ let assignees = [];
4119
+ if (opts.assignee) {
4120
+ try {
4121
+ assignees = await gh.addAssignees(n, [opts.assignee.replace(/^@/, "")]);
4122
+ } catch {
4123
+ warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n}.`);
4124
+ }
4125
+ }
4126
+ return { number: n, branch, assignees, warnings };
4127
+ }
4128
+ async function assignRequestOnGitHub(gh, n, login) {
4129
+ const who = login.trim().replace(/^@/, "");
4130
+ if (!who) throw new GitHubError(422, "Pick who should work on it.");
4131
+ const assignees = await gh.addAssignees(n, [who]);
4132
+ if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
4133
+ return assignees;
4134
+ }
4135
+ async function declineRequestOnGitHub(gh, n, reason, by) {
4136
+ const problems = declineReasonProblems(reason);
4137
+ if (problems.length) throw new GitHubError(422, problems.join(" "));
4138
+ await gh.addComment(n, declineComment(reason, by));
4139
+ await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
4140
+ }
4141
+ const px = (v) => {
4142
+ if (!v) return void 0;
4143
+ const m = /^(-?[\d.]+)px$/.exec(v.trim());
4144
+ return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
4145
+ };
4146
+ function evalCalc(expr, resolve) {
4147
+ const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
4148
+ if (!body) return void 0;
4149
+ let missing = false;
4150
+ const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
4151
+ const v2 = resolve(path);
4152
+ const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
4153
+ if (n === void 0 || Number.isNaN(n)) missing = true;
4154
+ return String(n);
4155
+ });
4156
+ if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
4157
+ const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
4158
+ let i = 0;
4159
+ const factor = () => {
4160
+ const t = tokens[i++];
4161
+ if (t === "(") {
4162
+ const v2 = sum();
4163
+ i++;
4164
+ return v2;
4165
+ }
4166
+ if (t === "-") return -factor();
4167
+ return Number(t);
4168
+ };
4169
+ const product = () => {
4170
+ let v2 = factor();
4171
+ while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
4172
+ return v2;
4173
+ };
4174
+ const sum = () => {
4175
+ let v2 = product();
4176
+ while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
4177
+ return v2;
4178
+ };
4179
+ const v = sum();
4180
+ return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
4181
+ }
4182
+ const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
4183
+ function normColor(v) {
4184
+ const t = v.trim().toLowerCase();
4185
+ const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
4186
+ if (hex2) {
4187
+ const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
4188
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
4189
+ const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
4190
+ return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
4191
+ }
4192
+ const m = /^rgba?\(([^)]*)\)$/.exec(t);
4193
+ if (m) {
4194
+ const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
4195
+ const [r, g, b] = parts;
4196
+ const a = parts[3] ?? 1;
4197
+ return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
4198
+ }
4199
+ return t.replace(/\s+/g, "");
4200
+ }
4201
+ const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
4202
+ const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
4203
+ function withoutAddedWrappers(parts, measured) {
4204
+ const real = new Set(measured.parts.map((m) => m.id));
4205
+ const added = /* @__PURE__ */ new Map();
4206
+ for (const p of parts) {
4207
+ if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
4208
+ const kids = parts.filter((c) => c.parent === p.id);
4209
+ if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
4210
+ }
4211
+ if (!added.size) return parts;
4212
+ const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
4213
+ return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
4214
+ }
4215
+ function pairParts(staticParts, measured, o = {}) {
4216
+ const pairs2 = /* @__PURE__ */ new Map();
4217
+ const taken = /* @__PURE__ */ new Set();
4218
+ const mParts = new Map(measured.parts.map((p) => [p.id, p]));
4219
+ const sParts = new Map(staticParts.map((p) => [p.id, p]));
4220
+ const take = (s, m) => {
4221
+ pairs2.set(s, m);
4222
+ taken.add(m);
4223
+ };
4224
+ const ancestors = (id) => {
4225
+ const out = [];
4226
+ const seen = /* @__PURE__ */ new Set([id]);
4227
+ for (let p = mParts.get(id)?.parent; p && !seen.has(p); p = mParts.get(p)?.parent) {
4228
+ seen.add(p);
4229
+ out.push(p);
4230
+ }
4231
+ return out;
4232
+ };
4233
+ const pairedAncestor = (p) => {
4234
+ const seen = /* @__PURE__ */ new Set([p.id]);
4235
+ for (let a = p.parent; a && !seen.has(a); a = sParts.get(a)?.parent) {
4236
+ if (pairs2.has(a)) return pairs2.get(a);
4237
+ seen.add(a);
4238
+ }
4239
+ return void 0;
4240
+ };
4241
+ const best = (p, cands) => {
4242
+ if (!cands.length) return void 0;
4243
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
4244
+ if (parent) {
4245
+ const direct = cands.find((c) => c.parent === parent);
4246
+ if (direct) return direct;
4247
+ }
4248
+ const anc = pairedAncestor(p);
4249
+ if (anc) {
4250
+ const under2 = cands.find((c) => ancestors(c.id).includes(anc));
4251
+ if (under2) return under2;
4252
+ }
4253
+ return cands[0];
4254
+ };
4255
+ const root = staticParts.find((p) => !p.parent);
4256
+ if (root) take(root.id, "root");
4257
+ const textOf = (p) => {
4258
+ if (p.characters) return normText(p.characters);
4259
+ const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
4260
+ return typeof prop?.default === "string" ? normText(prop.default) : "";
4261
+ };
4262
+ const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
4263
+ const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
4264
+ const shared = (p, m) => {
4265
+ const mine = new Set(classesOf(p));
4266
+ return mClasses(m).filter((c) => mine.has(c)).length;
4267
+ };
4268
+ const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
4269
+ const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
4270
+ for (const p of staticParts) {
4271
+ if (p.kind !== "text" || pairs2.has(p.id)) continue;
4272
+ const t = textOf(p);
4273
+ if (!t) continue;
4274
+ 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))));
4275
+ if (hit) take(p.id, hit.id);
4184
4276
  }
4185
- code;
4277
+ const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
4278
+ for (const p of staticParts) {
4279
+ if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
4280
+ const sp = sParts.get(p.parent);
4281
+ if (!sp || sp.kind !== "frame" || !sp.parent) continue;
4282
+ const cls = classOf(sp);
4283
+ const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
4284
+ if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
4285
+ }
4286
+ for (const p of staticParts) {
4287
+ if (pairs2.has(p.id) || !classesOf(p).length) continue;
4288
+ if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
4289
+ const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
4290
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
4291
+ const under2 = parent ? all.filter((m) => m.parent === parent) : [];
4292
+ const cands = under2.length ? under2 : all;
4293
+ const most = Math.max(0, ...cands.map((m) => shared(p, m)));
4294
+ const hit = best(p, cands.filter((m) => shared(p, m) === most));
4295
+ if (hit) take(p.id, hit.id);
4296
+ }
4297
+ for (const p of staticParts) {
4298
+ if (pairs2.has(p.id)) continue;
4299
+ if (p.parent && !pairs2.has(p.parent)) continue;
4300
+ const parent = p.parent ? pairs2.get(p.parent) : void 0;
4301
+ const known = p.kind === "text" && !!textOf(p);
4302
+ const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
4303
+ const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
4304
+ if (hit) take(p.id, hit.id);
4305
+ }
4306
+ for (const p of staticParts) {
4307
+ if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
4308
+ const mp = pairs2.get(p.parent);
4309
+ const t = textOf(p);
4310
+ 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);
4311
+ }
4312
+ return pairs2;
4186
4313
  }
4187
- const SUGGESTION_TOO_LARGE = "Too large for a suggestion: split it, or ask a developer to open it directly.";
4188
- const utf8 = (text) => new TextEncoder().encode(text);
4189
- const hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
4190
- function padded(bytes) {
4191
- const len = bytes.length;
4192
- const total = Math.ceil((len + 9) / 64) * 64;
4193
- const buf = new Uint8Array(total);
4194
- buf.set(bytes);
4195
- buf[len] = 128;
4196
- const view = new DataView(buf.buffer);
4197
- view.setUint32(total - 8, Math.floor(len / 536870912), false);
4198
- view.setUint32(total - 4, len * 8 >>> 0, false);
4199
- return view;
4314
+ function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
4315
+ const pairs2 = pairParts(staticParts, measured, o);
4316
+ const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
4317
+ const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
4318
+ const near = (a, b) => {
4319
+ if (a === void 0 || b === void 0) return false;
4320
+ const na0 = num2(a);
4321
+ const nb0 = num2(b);
4322
+ if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
4323
+ if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
4324
+ const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
4325
+ const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
4326
+ return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
4327
+ };
4328
+ for (const sp of staticParts) {
4329
+ const mid = pairs2.get(sp.id);
4330
+ if (!mid) {
4331
+ out.unmatchedParts.push(sp.id);
4332
+ continue;
4333
+ }
4334
+ const sv = staticValues[sp.id] ?? {};
4335
+ const mv = mid ? measured.values[mid] ?? {} : {};
4336
+ const mr = mid ? measured.raw[mid] ?? {} : {};
4337
+ const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
4338
+ for (const f of fields) {
4339
+ if ((f === "width" || f === "height") && sv[f] === void 0) continue;
4340
+ const s = sv[f];
4341
+ const m = mv[f] !== void 0 ? mv[f] : mr[f];
4342
+ if (s === void 0) {
4343
+ if (m !== void 0 && m !== 0) out.measuredOnly++;
4344
+ continue;
4345
+ }
4346
+ if (m === void 0 && !(f in mr)) {
4347
+ if (s === 0 || s === null) continue;
4348
+ const r = typeof s === "string" && resolve ? resolve(s) : s;
4349
+ if (r === 0 || r === "0px" || r === null) continue;
4350
+ out.staticOnly++;
4351
+ out.diffs.push({ part: sp.id, field: f, static: s, same: false });
4352
+ continue;
4353
+ }
4354
+ out.compared++;
4355
+ let same2 = s === m;
4356
+ if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
4357
+ if (same2 && typeof s === "string") out.sameToken++;
4358
+ if (!same2 && resolve) {
4359
+ let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
4360
+ const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
4361
+ if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
4362
+ same2 = near(rs, mr[f]);
4363
+ }
4364
+ if (same2) out.same++;
4365
+ else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
4366
+ }
4367
+ }
4368
+ const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
4369
+ const box = measured.boxes.root;
4370
+ 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 } };
4371
+ return out;
4200
4372
  }
4201
- const K256 = [
4202
- 1116352408,
4203
- 1899447441,
4204
- 3049323471,
4205
- 3921009573,
4206
- 961987163,
4207
- 1508970993,
4208
- 2453635748,
4209
- 2870763221,
4210
- 3624381080,
4211
- 310598401,
4212
- 607225278,
4213
- 1426881987,
4214
- 1925078388,
4215
- 2162078206,
4216
- 2614888103,
4217
- 3248222580,
4218
- 3835390401,
4219
- 4022224774,
4220
- 264347078,
4221
- 604807628,
4222
- 770255983,
4223
- 1249150122,
4224
- 1555081692,
4225
- 1996064986,
4226
- 2554220882,
4227
- 2821834349,
4228
- 2952996808,
4229
- 3210313671,
4230
- 3336571891,
4231
- 3584528711,
4232
- 113926993,
4233
- 338241895,
4234
- 666307205,
4235
- 773529912,
4236
- 1294757372,
4237
- 1396182291,
4238
- 1695183700,
4239
- 1986661051,
4240
- 2177026350,
4241
- 2456956037,
4242
- 2730485921,
4243
- 2820302411,
4244
- 3259730800,
4245
- 3345764771,
4246
- 3516065817,
4247
- 3600352804,
4248
- 4094571909,
4249
- 275423344,
4250
- 430227734,
4251
- 506948616,
4252
- 659060556,
4253
- 883997877,
4254
- 958139571,
4255
- 1322822218,
4256
- 1537002063,
4257
- 1747873779,
4258
- 1955562222,
4259
- 2024104815,
4260
- 2227730452,
4261
- 2361852424,
4262
- 2428436474,
4263
- 2756734187,
4264
- 3204031479,
4265
- 3329325298
4373
+ function tokenVariablePath(name, prefix) {
4374
+ let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
4375
+ if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
4376
+ return parts.join("/");
4377
+ }
4378
+ function sharedTokenPrefix(names2) {
4379
+ if (names2.length < 20) return void 0;
4380
+ const counts = /* @__PURE__ */ new Map();
4381
+ for (const n2 of names2) {
4382
+ const first = n2.replace(/^--/, "").split("-")[0];
4383
+ counts.set(first, (counts.get(first) ?? 0) + 1);
4384
+ }
4385
+ const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
4386
+ return top && n / names2.length >= 0.6 ? top : void 0;
4387
+ }
4388
+ function markUsedTokens(tokens, stories) {
4389
+ const byName = new Map(tokens.properties.map((p) => [p.name, p]));
4390
+ const seen = /* @__PURE__ */ new Set();
4391
+ const use = (name) => {
4392
+ if (seen.has(name)) return;
4393
+ seen.add(name);
4394
+ const p = byName.get(name);
4395
+ if (!p) return;
4396
+ p.used = true;
4397
+ for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
4398
+ if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
4399
+ };
4400
+ for (const s of stories)
4401
+ for (const spec of [s.spec, ...s.instances ?? []])
4402
+ for (const part of Object.values(spec?.tokens ?? {}))
4403
+ for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
4404
+ const used = tokens.properties.filter((p) => p.used).length;
4405
+ tokens.counts.used = used;
4406
+ return used;
4407
+ }
4408
+ class MeasuredSpecsError extends Error {
4409
+ }
4410
+ const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4411
+ function parseMeasuredSpecsFile(input) {
4412
+ let data = input;
4413
+ if (typeof input === "string") {
4414
+ try {
4415
+ data = JSON.parse(input);
4416
+ } catch (e) {
4417
+ throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
4418
+ }
4419
+ }
4420
+ if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
4421
+ if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
4422
+ if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
4423
+ if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
4424
+ data.stories.forEach((s, i) => {
4425
+ if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
4426
+ const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
4427
+ for (const sp of specs) {
4428
+ 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`);
4429
+ 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`);
4430
+ }
4431
+ if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
4432
+ });
4433
+ if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
4434
+ if (data.tokens !== void 0) {
4435
+ const t = data.tokens;
4436
+ if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
4437
+ 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");
4438
+ }
4439
+ return data;
4440
+ }
4441
+ const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
4442
+ const XYZ_D65_TO_LINEAR_SRGB = [
4443
+ [3.2409699419045226, -1.537383177570094, -0.4986107602930034],
4444
+ [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
4445
+ [0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
4266
4446
  ];
4267
- function sha256Hex(text) {
4268
- const view = padded(utf8(text));
4269
- const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
4270
- const w = new Array(64);
4271
- const rotr = (x, n) => x >>> n | x << 32 - n;
4272
- for (let off = 0; off < view.byteLength; off += 64) {
4273
- for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
4274
- for (let i = 16; i < 64; i++) {
4275
- const a2 = w[i - 15];
4276
- const b2 = w[i - 2];
4277
- const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
4278
- const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
4279
- w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
4447
+ const D50_TO_D65 = [
4448
+ [0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
4449
+ [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
4450
+ [0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
4451
+ ];
4452
+ const LINEAR_P3_TO_XYZ_D65 = [
4453
+ [0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
4454
+ [0.2289745640697488, 0.6917385218365064, 0.079286914093745],
4455
+ [0, 0.04511338185890264, 1.043944368900976]
4456
+ ];
4457
+ const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
4458
+ const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
4459
+ const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
4460
+ function oklabToLinearSrgb(L, a, b) {
4461
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
4462
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
4463
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
4464
+ 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];
4465
+ }
4466
+ function labToXyzD50(L, a, b) {
4467
+ const e = 216 / 24389;
4468
+ const k = 24389 / 27;
4469
+ const fy = (L + 16) / 116;
4470
+ const fx = a / 500 + fy;
4471
+ const fz = fy - b / 200;
4472
+ const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
4473
+ const y = L > k * e ? fy ** 3 : L / k;
4474
+ const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
4475
+ return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
4476
+ }
4477
+ function num(s, full = 1) {
4478
+ if (s === void 0 || s === "none") return 0;
4479
+ if (s.endsWith("%")) return parseFloat(s) / 100 * full;
4480
+ if (/deg$/.test(s)) return parseFloat(s);
4481
+ if (/turn$/.test(s)) return parseFloat(s) * 360;
4482
+ if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
4483
+ return parseFloat(s);
4484
+ }
4485
+ function linearSrgb(fn, args) {
4486
+ switch (fn) {
4487
+ case "oklab":
4488
+ return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
4489
+ case "oklch": {
4490
+ const c = num(args[1], 0.4);
4491
+ const h = num(args[2]) * Math.PI / 180;
4492
+ return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
4280
4493
  }
4281
- let [a, b, c, d, e, f, g, hh] = h;
4282
- for (let i = 0; i < 64; i++) {
4283
- const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
4284
- const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
4285
- hh = g;
4286
- g = f;
4287
- f = e;
4288
- e = d + t1 | 0;
4289
- d = c;
4290
- c = b;
4291
- b = a;
4292
- a = t1 + t2 | 0;
4494
+ case "lab":
4495
+ return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
4496
+ case "lch": {
4497
+ const c = num(args[1], 150);
4498
+ const h = num(args[2]) * Math.PI / 180;
4499
+ return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
4500
+ }
4501
+ case "color": {
4502
+ const [space, ...c] = args;
4503
+ const v = [num(c[0]), num(c[1]), num(c[2])];
4504
+ if (space === "srgb") return v.map(toLinear);
4505
+ if (space === "srgb-linear") return v;
4506
+ if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
4507
+ if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
4508
+ if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
4509
+ return void 0;
4293
4510
  }
4294
- h[0] = h[0] + a | 0;
4295
- h[1] = h[1] + b | 0;
4296
- h[2] = h[2] + c | 0;
4297
- h[3] = h[3] + d | 0;
4298
- h[4] = h[4] + e | 0;
4299
- h[5] = h[5] + f | 0;
4300
- h[6] = h[6] + g | 0;
4301
- h[7] = h[7] + hh | 0;
4302
4511
  }
4303
- return hex(h);
4512
+ return void 0;
4304
4513
  }
4305
- function sha1Bytes(bytes) {
4306
- const view = padded(bytes);
4307
- const h = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
4308
- const w = new Array(80);
4309
- const rotl = (x, n) => x << n | x >>> 32 - n;
4310
- for (let off = 0; off < view.byteLength; off += 64) {
4311
- for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
4312
- for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
4313
- let [a, b, c, d, e] = h;
4314
- for (let i = 0; i < 80; i++) {
4315
- const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
4316
- const t = rotl(a, 5) + f + e + k + w[i] | 0;
4317
- e = d;
4318
- d = c;
4319
- c = rotl(b, 30);
4320
- b = a;
4321
- a = t;
4322
- }
4323
- h[0] = h[0] + a | 0;
4324
- h[1] = h[1] + b | 0;
4325
- h[2] = h[2] + c | 0;
4326
- h[3] = h[3] + d | 0;
4327
- h[4] = h[4] + e | 0;
4514
+ const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
4515
+ function cssColorToRgb(fn, inner) {
4516
+ const [main, alpha] = inner.split("/");
4517
+ const args = main.trim().split(/[\s,]+/).filter(Boolean);
4518
+ const lin = linearSrgb(fn.toLowerCase(), args);
4519
+ if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
4520
+ const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
4521
+ const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
4522
+ return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
4523
+ }
4524
+ function cssColorsToRgb(text) {
4525
+ if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
4526
+ return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
4527
+ }
4528
+ function withSrgbColors(value) {
4529
+ if (typeof value === "string") return cssColorsToRgb(value);
4530
+ if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
4531
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
4532
+ return value;
4533
+ }
4534
+ function parseBoxShadow(css) {
4535
+ if (!css || css.trim() === "none") return [];
4536
+ const out = [];
4537
+ for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
4538
+ const color = /rgba?\(([^)]*)\)/i.exec(one);
4539
+ const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
4540
+ const a = c[3] ?? 1;
4541
+ const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
4542
+ if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
4543
+ 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 } });
4544
+ }
4545
+ return out;
4546
+ }
4547
+ function parseTier(labels, body) {
4548
+ const found = [];
4549
+ for (const l of labels) {
4550
+ const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
4551
+ if (m) found.push(Number(m[1]));
4552
+ }
4553
+ for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
4554
+ for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
4555
+ return found.length ? Math.max(...found) : null;
4556
+ }
4557
+ function claudeStage(labels) {
4558
+ const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
4559
+ if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
4560
+ if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
4561
+ return null;
4562
+ }
4563
+ const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
4564
+ function pullState(p) {
4565
+ if (p.merged_at || p.merged) return "merged";
4566
+ if (p.state && p.state !== "open") return "closed";
4567
+ if (isDraft(p)) return "draft";
4568
+ return "open";
4569
+ }
4570
+ const PULL_STATE_PILL = {
4571
+ merged: { label: "Merged", cls: "" },
4572
+ closed: { label: "Closed", cls: "" },
4573
+ draft: { label: "Draft", cls: "" },
4574
+ open: { label: "Open", cls: "" }
4575
+ };
4576
+ function openPulls(pulls) {
4577
+ return pulls.filter((p) => {
4578
+ const s = pullState(p);
4579
+ return s === "open" || s === "draft";
4580
+ });
4581
+ }
4582
+ function isPlatformRefusal(s) {
4583
+ return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
4584
+ }
4585
+ const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
4586
+ function summarizeCi(runs, statuses = []) {
4587
+ const failed = [];
4588
+ const pending = [];
4589
+ let passed = 0;
4590
+ for (const r of runs) {
4591
+ if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
4592
+ else if (r.conclusion && OK.has(r.conclusion)) passed++;
4593
+ else failed.push({ name: r.name, url: r.html_url });
4594
+ }
4595
+ const seen = /* @__PURE__ */ new Set();
4596
+ for (const s of statuses) {
4597
+ if (seen.has(s.context)) continue;
4598
+ seen.add(s.context);
4599
+ if (isPlatformRefusal(s)) continue;
4600
+ if (s.state === "success") passed++;
4601
+ else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
4602
+ else failed.push({ name: s.context, url: s.target_url ?? void 0 });
4603
+ }
4604
+ const total = passed + failed.length + pending.length;
4605
+ const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
4606
+ return { state, total, passed, failed, pending };
4607
+ }
4608
+ const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
4609
+ function summarizeApprovals(reviews, owners, author, rules = {}) {
4610
+ const latest = /* @__PURE__ */ new Map();
4611
+ const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
4612
+ for (const r of sorted) {
4613
+ const login = r.user?.login;
4614
+ if (!login || login === author) continue;
4615
+ if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
4616
+ }
4617
+ const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
4618
+ const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
4619
+ const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
4620
+ const d = lower2(owners.design);
4621
+ const t = lower2(owners.tech);
4622
+ const a = lower2(approvers);
4623
+ let design;
4624
+ let tech;
4625
+ const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
4626
+ if (d.length || t.length) {
4627
+ design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
4628
+ const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
4629
+ const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
4630
+ tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
4631
+ } else {
4632
+ design = a.length >= 1;
4633
+ tech = a.length >= 2;
4634
+ }
4635
+ return { approvers, changesRequested, design, tech, owner };
4636
+ }
4637
+ function lookAgainReviewers(reviews, headSha, author) {
4638
+ if (!headSha) return [];
4639
+ const latest = /* @__PURE__ */ new Map();
4640
+ const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
4641
+ for (const r of sorted) {
4642
+ const login = r.user?.login;
4643
+ if (!login || login === author) continue;
4644
+ if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
4328
4645
  }
4329
- return hex(h);
4646
+ const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
4647
+ if (!asked.length) return [];
4648
+ return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
4330
4649
  }
4331
- function gitBlobSha(text) {
4332
- const body = utf8(text);
4333
- const head = utf8(`blob ${body.length}\0`);
4334
- const all = new Uint8Array(head.length + body.length);
4335
- all.set(head);
4336
- all.set(body, head.length);
4337
- return sha1Bytes(all);
4650
+ function withSuggestionApprovals(pr, suggestion, prTier) {
4651
+ if (!suggestion) return pr;
4652
+ const s = suggestion.approvals;
4653
+ if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
4654
+ const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
4655
+ if (!covered) return pr;
4656
+ const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
4657
+ return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
4338
4658
  }
4339
- function canonicalJson(value) {
4340
- return JSON.stringify(sortKeys(value));
4659
+ function withFigmaPushApprovals(pr, push) {
4660
+ if (!push || push.scope === "outside") return pr;
4661
+ const tech = pr.tech || push.scope === "token-values";
4662
+ return { ...pr, design: true, tech, owner: true };
4341
4663
  }
4342
- function sortKeys(v) {
4343
- if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
4344
- if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
4345
- const out = {};
4346
- for (const k of Object.keys(v).sort()) {
4347
- const x = v[k];
4348
- if (x !== void 0) out[k] = sortKeys(x);
4349
- }
4350
- return out;
4351
- }
4352
- return v;
4664
+ function withTokenValueApprovals(pr, valuesOnly) {
4665
+ if (!valuesOnly) return pr;
4666
+ return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
4353
4667
  }
4354
- function shaForm(p) {
4355
- if (!p.files) return p;
4356
- return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
4668
+ const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
4669
+ function canOverride(login, policy) {
4670
+ if (!login || !policy?.allowed.length) return false;
4671
+ return policy.allowed.map(normLogin).includes(normLogin(login));
4357
4672
  }
4358
- function suggestionPayloadSha(p) {
4359
- return sha256Hex(canonicalJson(shaForm(p)));
4673
+ function overrideHolders(policy, owners) {
4674
+ const allowed = policy?.allowed ?? [];
4675
+ const all = [...owners.design, ...owners.tech];
4676
+ if (!all.length || all.some((o) => o.includes("/"))) return allowed;
4677
+ const named = all.map(normLogin);
4678
+ return allowed.filter((l) => named.includes(normLogin(l)));
4360
4679
  }
4361
- const KIND_SUBJECT = {
4362
- "token-change": "Tokens",
4363
- docs: "Docs",
4364
- "component-spec": "Component",
4365
- brand: "Brand",
4366
- revert: "Revert",
4367
- config: "Settings"
4368
- };
4369
- function suggestionSubject(p) {
4370
- if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
4371
- if (p.kind === "docs" && p.docs?.length) {
4372
- const names2 = [...new Set(p.docs.map((d) => d.component))];
4373
- return names2.length === 1 ? names2[0] : KIND_SUBJECT.docs;
4680
+ const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
4681
+ function mergeGate(input) {
4682
+ const blocking = [];
4683
+ const missing = [];
4684
+ const approvalReasons = [];
4685
+ const lifecycle = pullState(input);
4686
+ if (lifecycle === "merged" || lifecycle === "closed") {
4687
+ 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) };
4374
4688
  }
4375
- return KIND_SUBJECT[p.kind];
4689
+ if (input.exploration) blocking.push("It is an exploration: say it is done exploring first, then developers take it.");
4690
+ if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
4691
+ if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
4692
+ if (input.ci.state === "pending") blocking.push("Tests are still running.");
4693
+ if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
4694
+ const tier = input.tier ?? 3;
4695
+ if (tier >= 2) {
4696
+ if (!input.approvals.design) {
4697
+ approvalReasons.push("It needs a design owner approval.");
4698
+ missing.push("design approval");
4699
+ }
4700
+ if (!input.approvals.tech && !input.approvals.valuesOnly) {
4701
+ approvalReasons.push("It needs a tech owner approval.");
4702
+ missing.push("tech approval");
4703
+ }
4704
+ } else if (!hasOwnerApproval(input.approvals)) {
4705
+ approvalReasons.push("It needs one owner approval.");
4706
+ missing.push("approval");
4707
+ }
4708
+ const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
4709
+ const shipper = !input.viewer || canShip(input.viewer, input.ship);
4710
+ const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
4711
+ const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
4712
+ const allowed = reasons.length === 0;
4713
+ const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
4714
+ return { allowed, reasons, overridable, missing, canShip: shipper };
4376
4715
  }
4377
- function suggestionIssueTitle(p) {
4378
- const names2 = [...(p.docs ?? []).map((d) => d.component), ...p.componentSpec?.component ? [p.componentSpec.component] : []];
4379
- return subjectTitle(suggestionSubject(p), p.title, names2);
4716
+ function branchVerdict(input) {
4717
+ const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
4718
+ const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
4719
+ const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
4720
+ const override = ownerOverridePolicy(input.override, input.owners);
4721
+ const state = input.state ?? "open";
4722
+ const draft = isDraft(input);
4723
+ const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
4724
+ 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;
4725
+ const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
4726
+ const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
4727
+ const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
4728
+ const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
4729
+ return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
4380
4730
  }
4381
- function suggestionLabels(p, opts = {}) {
4382
- return [
4383
- .../* @__PURE__ */ new Set([
4384
- SUGGESTION_LABEL,
4385
- `suggestion: ${p.kind}`,
4386
- `source: ${p.source}`,
4387
- `tier: ${p.tier.tier}`,
4388
- SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
4389
- ...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
4390
- ...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
4391
- ...p.links?.exploration ? [EXPLORATION_LABEL] : [],
4392
- ...opts.labels ?? []
4393
- ])
4394
- ];
4731
+ const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
4732
+ const OVERRIDE_HEADING = "**Bridge owner override**";
4733
+ function overrideReasonMin(policy) {
4734
+ return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
4395
4735
  }
4396
- function suggestionKindFromLabels(labels) {
4397
- for (const l of labels) {
4398
- const m = /^suggestion: (.+)$/.exec(l);
4399
- if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
4400
- }
4401
- return void 0;
4736
+ function overrideReasonProblem(reason, policy) {
4737
+ const min = overrideReasonMin(policy);
4738
+ const n = reason.trim().length;
4739
+ if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
4740
+ if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
4741
+ return null;
4402
4742
  }
4403
- function suggestionStateFromLabels(labels) {
4404
- return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
4743
+ function overrideComment(login, reason, missing) {
4744
+ const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
4745
+ return `${OVERRIDE_HEADING}
4746
+
4747
+ Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
4405
4748
  }
4406
- function suggestionIsTokenValues(p) {
4407
- if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
4408
- if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
4409
- return (p.files ?? []).every((f) => f.path.endsWith(".md"));
4749
+ function overrideRefusedComment(body, message) {
4750
+ return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
4410
4751
  }
4411
- function suggestionStatus(i) {
4412
- const prs = i.linkedPrs ?? [];
4413
- if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
4414
- if (prs.some((p) => p.state === "open") || i.applied) return "applying";
4415
- if (i.outdated) return "outdated";
4416
- return i.review ?? "needs-review";
4752
+ function parseOverrideComment(body) {
4753
+ const text = (body ?? "").trim();
4754
+ if (!text.startsWith(OVERRIDE_HEADING)) return null;
4755
+ const m = text.match(/Owner override by @([\w-]+): ([\s\S]*?)\. Missing: ([^.]*)\. (Merged from Bridge\.|Merge refused by GitHub)/);
4756
+ if (!m) return null;
4757
+ return { login: m[1], reason: m[2], missing: m[3].split(/,\s*/).filter(Boolean), merged: m[4].startsWith("Merged") };
4417
4758
  }
4418
- function withOutdated(status, outdated) {
4419
- return outdated && (status === "needs-review" || status === "changes-requested" || status === "approved") ? "outdated" : status;
4759
+ function allowedMergeMethods(repo) {
4760
+ const out = [];
4761
+ if (repo?.allow_squash_merge !== false) out.push("squash");
4762
+ if (repo?.allow_merge_commit !== false) out.push("merge");
4763
+ if (repo?.allow_rebase_merge !== false) out.push("rebase");
4764
+ return out;
4420
4765
  }
4421
- function suggestionStateLabel(status) {
4422
- if (status === "applied" || status === "declined") return SUGGESTION_FINAL_LABELS[status];
4423
- return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
4766
+ function hasWriteAccess(perms) {
4767
+ return Boolean(perms && (perms.admin || perms.maintain || perms.push));
4424
4768
  }
4425
- const SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
4426
- function suggestionLabelChanges(labels, status) {
4427
- if (status === "outdated") return { add: [], remove: [] };
4428
- const want = suggestionStateLabel(status);
4429
- return {
4430
- add: want && !labels.includes(want) ? [want] : [],
4431
- remove: SUGGESTION_STATUS_LABELS.filter((l) => l !== want && labels.includes(l))
4432
- };
4769
+ const REVIEW_STATUS_ORDER = {
4770
+ "ci-failing": 0,
4771
+ conflict: 1,
4772
+ "changes-requested": 2,
4773
+ "needs-design": 3,
4774
+ "needs-tech": 4,
4775
+ "needs-approval": 5,
4776
+ ready: 6,
4777
+ "ci-running": 7,
4778
+ checking: 8,
4779
+ unknown: 9,
4780
+ draft: 10,
4781
+ merged: 11,
4782
+ closed: 12
4783
+ };
4784
+ function reviewStatusKind(input) {
4785
+ const out = (kind) => kind;
4786
+ const lifecycle = pullState(input);
4787
+ if (lifecycle === "merged") return out("merged");
4788
+ if (lifecycle === "closed") return out("closed");
4789
+ if (lifecycle === "draft") return out("draft");
4790
+ if (input.ci?.state === "fail") return out("ci-failing");
4791
+ if (input.approvals?.changesRequested.length) return out("changes-requested");
4792
+ if (input.mergeable === false) return out("conflict");
4793
+ if (input.ci?.state === "pending") return out("ci-running");
4794
+ if (!input.ci || !input.approvals) return out("unknown");
4795
+ const tier = input.tier ?? 3;
4796
+ if (tier >= 2) {
4797
+ if (!input.approvals.design) return out("needs-design");
4798
+ if (!input.approvals.tech) return out("needs-tech");
4799
+ } else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
4800
+ if (input.mergeable === null) return out("checking");
4801
+ 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 });
4802
+ return gate.allowed ? out("ready") : out("unknown");
4803
+ }
4804
+ const REVIEW_STATUS_LABEL = {
4805
+ merged: "Merged",
4806
+ closed: "Closed",
4807
+ draft: "Draft",
4808
+ "ci-failing": "Blocked by tests",
4809
+ "changes-requested": "Changes requested",
4810
+ "ci-running": "Tests running",
4811
+ "needs-design": "Needs design approval",
4812
+ "needs-tech": "Needs tech approval",
4813
+ "needs-approval": "Needs approval",
4814
+ conflict: "Conflict with main",
4815
+ checking: "Checking for conflicts",
4816
+ ready: "Ready to merge",
4817
+ unknown: "Status unknown"
4818
+ };
4819
+ const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
4820
+ function reviewStatusLabel(kind, lookAgain) {
4821
+ return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
4822
+ }
4823
+ const CI_STATE_LABEL = {
4824
+ pass: "Tests pass",
4825
+ fail: "Tests fail",
4826
+ pending: "Tests running",
4827
+ none: "No automated checks"
4828
+ };
4829
+ const BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
4830
+ const isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
4831
+ function isAutomationPull(p) {
4832
+ return isReleasePull(p) || BOTS.has((p.user?.login ?? "").toLowerCase());
4433
4833
  }
4434
- const bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
4435
- const MARK = "bridge-suggestion";
4436
- const PART_SCHEMA = "bridge.suggestion-part/1";
4437
- const cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
4438
- const safeText = (s) => s.replace(/<!--/g, "&lt;!--");
4439
- const sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
4440
- const show$1 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
4441
- function table(head, rows, max) {
4442
- if (!rows.length) return [];
4443
- const shown = rows.slice(0, max);
4444
- return [
4445
- `| ${head.join(" | ")} |`,
4446
- `| ${head.map(() => "---").join(" | ")} |`,
4447
- ...shown.map((r) => `| ${r.map((c) => cell(c)).join(" | ")} |`),
4448
- ...rows.length > max ? ["", `and ${rows.length - max} more`] : []
4449
- ];
4834
+ function reviewQueue(pulls) {
4835
+ return openPulls(pulls).filter((p) => !isAutomationPull(p) && !isHeldExploration(p));
4450
4836
  }
4451
- function changesSection(p, max) {
4452
- const out = [];
4453
- if (p.tokens?.changes.length) {
4454
- const rows = p.tokens.changes.map((c) => [
4455
- `\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
4456
- c.beforeCss ?? show$1(c.before),
4457
- c.afterCss ?? show$1(c.after)
4458
- ]);
4459
- out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
4460
- } else if (p.tokens?.edits.length) {
4461
- const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show$1(e.value)]);
4462
- out.push(...table(["Token", "Edit", "Value"], rows, max), "");
4463
- }
4464
- if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
4465
- if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
4466
- if (p.files?.length) {
4467
- const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
4468
- out.push(...table(["File", "Change"], rows, max), "");
4469
- }
4470
- return out;
4837
+ function releasePull(pulls) {
4838
+ return openPulls(pulls).find((p) => isReleasePull(p)) ?? null;
4471
4839
  }
4472
- function checksSection(p, max) {
4473
- const c = p.checks;
4474
- const failing = c.contrast.filter((x) => !x.pass);
4475
- const rows = [...failing, ...c.contrast.filter((x) => x.pass)].map((x) => [x.pair, x.mode ?? "", x.before.toFixed(2), x.after.toFixed(2), String(x.min), x.pass ? "pass" : "**fails**"]);
4476
- return [
4477
- "## Checks when suggested",
4478
- "",
4479
- `- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
4480
- `- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
4481
- `- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
4482
- `- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
4483
- "",
4484
- ...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
4485
- ];
4840
+ function effectiveTier(labels, body, files) {
4841
+ const labelled = parseTier(labels, body);
4842
+ if (!files) return labelled;
4843
+ const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
4844
+ return Math.max(labelled ?? 0, computed);
4486
4845
  }
4487
- function linksSection(p) {
4488
- const l = p.links ?? {};
4489
- const out = [];
4490
- if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell(l.neededBy.title)}](${l.neededBy.url})`);
4491
- if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
4492
- const key = l.figma?.fileKey;
4493
- const nodes = (l.figma?.nodes ?? []).filter((n) => n && typeof n.nodeId === "string");
4494
- for (const n of nodes) {
4495
- const url = n.url ?? (key ? figmaNodeUrl(key, n.nodeId) : void 0);
4496
- const what = `${n.state === "before" ? "Before" : n.state === "after" ? "After" : "Node"}${n.name ? ` (${cell(n.name)})` : ""}`;
4497
- out.push(`- ${what}: ${url ? `[node ${n.nodeId}](${url})` : `node ${n.nodeId}`}`);
4498
- }
4499
- if (key && (nodes.length || l.figma?.nodeId)) {
4500
- const ids = nodes.length ? nodes.map((n) => n.nodeId) : [l.figma.nodeId];
4501
- out.push(
4502
- `- Claude Code reads this design straight from Figma: the Figma MCP server (\`get_design_context\` with file key \`${key}\` and node ${ids.map((i) => `\`${i}\``).join(", ")}) or the REST API (\`GET https://api.figma.com/v1/files/${key}/nodes?ids=${ids.map(encodeURIComponent).join(",")}\`). No image hosting is needed.`
4503
- );
4504
- }
4505
- if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
4506
- if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
4507
- for (const v of p.previews ?? []) out.push(`- ${cell(v.label)}: ${v.url}`);
4508
- return out.length ? ["## Previews and links", "", ...out, ""] : [];
4846
+ const norm$1 = (x) => x.trim().toLowerCase().replace(/^@/, "");
4847
+ function reopenEligibility(x) {
4848
+ if (x.state !== "closed" || !x.viewer) return { kind: "none" };
4849
+ const isAuthor = norm$1(x.viewer) === norm$1(x.author);
4850
+ const isOwner = x.owners.some((o) => norm$1(o) === norm$1(x.viewer));
4851
+ if (!isAuthor && !isOwner) return { kind: "none" };
4852
+ if (!x.canWrite && !isAuthor) return { kind: "none" };
4853
+ if (x.branch === "exists") return { kind: "reopen" };
4854
+ if (x.branch === "gone") return { kind: "gone" };
4855
+ return { kind: "none" };
4509
4856
  }
4510
- function humanPart(p, details, max, repo) {
4511
- return [
4512
- `## Suggested change: ${safeText(p.title.trim())}`,
4513
- "",
4514
- safeText(p.summary.trim()),
4515
- "",
4516
- ...changesSection(p, max),
4517
- ...details?.trim() ? [safeText(details.trim()), ""] : [],
4518
- ...checksSection(p, max),
4519
- ...linksSection(p),
4520
- "## How to apply",
4521
- "",
4522
- "- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
4523
- '- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
4524
- ` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
4525
- "",
4526
- "The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
4527
- "",
4528
- `<sub>Suggested from ${sourceLabel(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
4529
- ].join("\n");
4857
+ const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
4858
+ const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
4859
+ function requiredApprovals(tier, valuesOnly) {
4860
+ 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]);
4530
4861
  }
4531
- function fitHuman(p, details, repo) {
4532
- for (const max of [50, 20, 8, 3]) {
4533
- const h2 = humanPart(p, details, max, repo);
4534
- if (h2.length <= SUGGESTION_LIMITS.human) return h2;
4535
- }
4536
- const h = humanPart(p, void 0, 3, repo);
4537
- if (h.length <= SUGGESTION_LIMITS.human) return h;
4538
- const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
4539
- return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
4862
+ function pendingApprovals(tier, approvals) {
4863
+ if (tier == null || !approvals) return [];
4864
+ return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
4540
4865
  }
4541
- const pretty = (v) => JSON.stringify(sortKeys(v), null, 2);
4542
- const fit = (v) => {
4543
- const p = pretty(v);
4544
- return p.length <= SUGGESTION_LIMITS.inline ? p : canonicalJson(v);
4545
- };
4546
- const fenced = (json) => ["```json", json, "```"].join("\n");
4547
- function payloadBlock(json, sha, parts) {
4548
- return [
4549
- "<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
4550
- "",
4551
- `<!-- ${MARK}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
4552
- fenced(json),
4553
- "",
4554
- "</details>"
4555
- ].join("\n");
4866
+ const uniqLogins = (xs) => xs.filter((x, i) => xs.findIndex((y) => normLogin(y) === normLogin(x)) === i);
4867
+ function eligibleReviewers(role, p) {
4868
+ const raw = role === "design" ? p.owners.design : role === "tech" ? p.owners.tech : uniqLogins([...p.owners.design, ...p.owners.tech]);
4869
+ const listed = raw.map((x) => x.replace(/^@/, ""));
4870
+ const approvers = (p.approvers ?? []).map(normLogin);
4871
+ const logins = listed.filter((l) => normLogin(l) !== normLogin(p.author) && !approvers.includes(normLogin(l)));
4872
+ return { open: listed.length === 0, logins, listed };
4556
4873
  }
4557
- function partComment(i, n, json) {
4558
- return [
4559
- `<details><summary>Machine-readable change, part ${i} of ${n} (Bridge suggestion v1)</summary>`,
4560
- "",
4561
- `<!-- ${MARK}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
4562
- fenced(json),
4563
- "",
4564
- "</details>"
4565
- ].join("\n");
4874
+ function reviewersToAsk(x) {
4875
+ const roles = pendingApprovals(x.tier, x.approvals);
4876
+ const all = roles.flatMap((r) => eligibleReviewers(r, { owners: x.owners, author: x.author, approvers: x.approvals.approvers }).logins);
4877
+ return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
4566
4878
  }
4567
- function escapedEnd(s, from, budget) {
4568
- let used = 0;
4569
- let i = from;
4570
- while (i < s.length) {
4571
- const c = s.charCodeAt(i);
4572
- let w = 1;
4573
- let len = 1;
4574
- if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
4575
- else if (c < 32) len = 6;
4576
- else if (c >= 55296 && c <= 56319) {
4577
- const d = s.charCodeAt(i + 1);
4578
- if (d >= 56320 && d <= 57343) {
4579
- w = 2;
4580
- len = 2;
4581
- } else len = 6;
4582
- } else if (c >= 56320 && c <= 57343) len = 6;
4583
- if (used + len > budget) break;
4584
- used += len;
4585
- i += w;
4586
- }
4587
- return i;
4879
+ const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6}$/;
4880
+ function parseJiraKey(input) {
4881
+ const t = (input ?? "").trim().toUpperCase();
4882
+ if (!t) return null;
4883
+ if (!JIRA_KEY.test(t)) throw new Error("Write the Jira ticket as its key, like DS-42, or leave it empty.");
4884
+ return t;
4588
4885
  }
4589
- function composeSuggestionIssue(payload, opts = {}) {
4590
- const problems = suggestionShapeProblems(payload);
4591
- if (problems.length) throw new SuggestionError("invalid", problems.join(" "));
4592
- if (payload.checks.blocked) throw new SuggestionError("invalid", `This change cannot be suggested: ${payload.checks.blocked}`);
4593
- const full = shaForm(payload);
4594
- const sha = suggestionPayloadSha(full);
4595
- const human = fitHuman(full, opts.details, opts.repo);
4596
- const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
4597
- const inline = fit(full);
4598
- if (inline.length <= SUGGESTION_LIMITS.inline) {
4599
- return { ...base, body: `${human}
4600
-
4601
- ${payloadBlock(inline, sha, 0)}
4602
- `, parts: [] };
4603
- }
4604
- const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
4605
- const parts = [];
4606
- const firstPart = /* @__PURE__ */ new Map();
4607
- const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
4608
- const emptyOverhead = partJson([], 9, 9).length;
4609
- let cur = [];
4610
- let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
4611
- for (const { f, i } of moved) {
4612
- const text = f.content;
4613
- let at = 0;
4614
- while (at < text.length) {
4615
- const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
4616
- if (room - entryOverhead < 256) {
4617
- parts.push(cur);
4618
- cur = [];
4619
- room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
4620
- }
4621
- const end = escapedEnd(text, at, room - entryOverhead);
4622
- if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
4623
- if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
4624
- const chunk = { i, path: f.path, text: text.slice(at, end) };
4625
- cur.push(chunk);
4626
- room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
4627
- at = end;
4628
- }
4629
- }
4630
- if (cur.length) parts.push(cur);
4631
- if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
4632
- const skeleton = {
4633
- ...full,
4634
- files: (full.files ?? []).map((f, i) => {
4635
- if (!firstPart.has(i)) return f;
4636
- const { content: _content, ...rest } = f;
4637
- return { ...rest, part: firstPart.get(i) };
4638
- })
4639
- };
4640
- const skeletonJson = fit(skeleton);
4641
- if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
4642
- const n = parts.length;
4643
- const partBodies = parts.map((chunks, k) => {
4644
- const json = partJson(chunks, k + 1, n);
4645
- if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
4646
- return partComment(k + 1, n, json);
4647
- });
4648
- const body = `${human}
4649
-
4650
- ${payloadBlock(skeletonJson, sha, n)}
4651
- `;
4652
- if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
4653
- return { ...base, body, parts: partBodies };
4886
+ const isJiraKey = (input) => JIRA_KEY.test(input.trim().toUpperCase());
4887
+ const LEADING_KEY = /^\[?([A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6})\]?[:\s]\s*/;
4888
+ function jiraKeyOf(title) {
4889
+ return LEADING_KEY.exec(title.trim())?.[1] ?? null;
4654
4890
  }
4655
- function hasSuggestionBlock(body) {
4656
- return new RegExp(`^<!-- ${MARK}:v\\d+ sha256=`, "m").test(body ?? "");
4891
+ const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
4892
+ function titleWithJiraKey(title, key) {
4893
+ if (!key) return title;
4894
+ return `${key} ${withoutJiraKey(title)}`;
4657
4895
  }
4658
- function fencedAfter(text, from) {
4659
- const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
4660
- return m?.[1];
4896
+ function branchWithJiraKey(branch, key) {
4897
+ if (!key || branch.includes(key)) return branch;
4898
+ const i = branch.lastIndexOf("/");
4899
+ return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
4661
4900
  }
4662
- const fail = (reason, message) => ({ ok: false, reason, message });
4663
- function parseSuggestionIssue(body, opts = {}) {
4664
- const text = body ?? "";
4665
- const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
4666
- const marker = markers[markers.length - 1];
4667
- if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
4668
- if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
4669
- const json = fencedAfter(text, marker.index + marker[0].length);
4670
- if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
4671
- let raw;
4901
+ const SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
4902
+ const SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
4903
+ const EVENTS = ["approve", "request-changes", "comment"];
4904
+ const ROLE_TEXT = { design: "for design", tech: "for tech", approval: "" };
4905
+ function composeSuggestionReviewComment(r) {
4906
+ const who = r.by ? ` by @${r.by.replace(/^@/, "")}` : "";
4907
+ const role = r.role ? ROLE_TEXT[r.role] : "";
4908
+ const head = r.event === "approve" ? `Approved${role ? ` ${role}` : ""}${who} in Bridge.` : r.event === "request-changes" ? `Changes requested${who} in Bridge.` : `Comment${who} in Bridge.`;
4909
+ const quote = r.message?.trim() ? r.message.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n") : "";
4910
+ const record = { event: r.event, ...r.role ? { role: r.role } : {}, payloadSha: r.payloadSha, at: r.at };
4911
+ return [head, ...quote ? ["", quote] : [], "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_REVIEW_MARK} ${JSON.stringify(record)} -->`].join("\n");
4912
+ }
4913
+ function parseSuggestionReviewComment(body) {
4914
+ const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4915
+ if (!m) return null;
4672
4916
  try {
4673
- raw = JSON.parse(json);
4917
+ const r = JSON.parse(m[1]);
4918
+ if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
4919
+ return {
4920
+ event: r.event,
4921
+ ...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
4922
+ payloadSha: r.payloadSha,
4923
+ at: typeof r.at === "string" ? r.at : ""
4924
+ };
4674
4925
  } catch {
4675
- return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
4676
- }
4677
- const schema = raw?.schema;
4678
- if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
4679
- const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
4680
- return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
4926
+ return null;
4681
4927
  }
4682
- const problems = suggestionShapeProblems(raw);
4683
- if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
4684
- const payload = raw;
4685
- const markedSha = marker[2];
4686
- const expected = marker[3] ? Number(marker[3]) : 0;
4687
- let edited = false;
4688
- if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
4689
- const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
4690
- const found = /* @__PURE__ */ new Map();
4691
- const author = opts.author?.toLowerCase();
4692
- for (const c of opts.comments ?? []) {
4693
- if (author && c.author && c.author.toLowerCase() !== author) continue;
4694
- const m = new RegExp(`^<!-- ${MARK}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
4695
- if (!m) continue;
4696
- const i = Number(m[1]);
4697
- if (found.has(i)) continue;
4698
- const pj = fencedAfter(c.body, m.index + m[0].length);
4699
- if (pj === void 0) continue;
4700
- if (sha256Hex(pj) !== m[3]) edited = true;
4701
- try {
4702
- const part = JSON.parse(pj);
4703
- if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
4704
- found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
4705
- } catch {
4706
- continue;
4707
- }
4708
- }
4709
- for (let i = 1; i <= want; i++) {
4710
- if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
4711
- }
4712
- const files = (payload.files ?? []).map((f, idx) => {
4713
- if (f.part === void 0) return f;
4714
- let content = "";
4715
- for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
4716
- const { part: _part, ...rest } = f;
4717
- return { ...rest, content };
4718
- });
4719
- payload.files = files;
4928
+ }
4929
+ function composeSuggestionAppliedComment(r) {
4930
+ const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
4931
+ const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
4932
+ return [
4933
+ `Being applied ${where2}${r.by ? ` by @${r.by.replace(/^@/, "")}` : ""}.${r.forced ? " Applied before every approval was in." : ""}`,
4934
+ "",
4935
+ `<!-- ${SUGGESTION_APPLIED_MARK} ${JSON.stringify(record)} -->`
4936
+ ].join("\n");
4937
+ }
4938
+ function parseSuggestionAppliedComment(body) {
4939
+ const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
4940
+ if (!m) return null;
4941
+ try {
4942
+ const r = JSON.parse(m[1]);
4943
+ return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
4944
+ } catch {
4945
+ return null;
4720
4946
  }
4721
- const sha = suggestionPayloadSha(payload);
4722
- return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
4723
4947
  }
4724
- function suggestionShapeProblems(raw) {
4725
- const out = [];
4726
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
4727
- const p = raw;
4728
- if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
4729
- if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
4730
- if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
4731
- if (typeof p.summary !== "string") out.push('"summary" must be text.');
4732
- if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
4733
- if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
4734
- const base = p.base;
4735
- if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
4736
- const tier = p.tier;
4737
- if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
4738
- const checks = p.checks;
4739
- if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
4740
- out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
4741
- const tokens = p.tokens;
4742
- if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
4743
- if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
4744
- const spec = p.componentSpec;
4745
- if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
4746
- const nodes = p.links?.figma?.nodes;
4747
- if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
4748
- if (p.files !== void 0) {
4749
- if (!Array.isArray(p.files)) out.push('"files" must be a list.');
4750
- else {
4751
- const seen = /* @__PURE__ */ new Set();
4752
- for (const f of p.files) {
4753
- const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
4754
- if (!ok) {
4755
- out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
4756
- break;
4757
- }
4758
- if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
4759
- seen.add(f.path);
4760
- }
4761
- }
4948
+ const SUGGESTION_OVERRIDE_MARK = "bridge-suggestion-override";
4949
+ function composeSuggestionOverrideComment(r) {
4950
+ const audit = overrideComment(r.login, r.reason, r.missing).replace(/ Merged from Bridge\.$/, " Approved as owner from Bridge.");
4951
+ const record = { payloadSha: r.payloadSha, login: r.login.replace(/^@/, "").toLowerCase(), missing: r.missing };
4952
+ return [audit, "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_OVERRIDE_MARK} ${JSON.stringify(record)} -->`].join("\n");
4953
+ }
4954
+ function parseSuggestionOverrideComment(body) {
4955
+ const text = (body ?? "").trim();
4956
+ if (!text.startsWith(OVERRIDE_HEADING)) return null;
4957
+ const m = new RegExp(`<!-- ${SUGGESTION_OVERRIDE_MARK} (\\{[^\\n]*?\\}) -->`).exec(text);
4958
+ if (!m) return null;
4959
+ try {
4960
+ const r = JSON.parse(m[1]);
4961
+ if (typeof r.payloadSha !== "string" || typeof r.login !== "string") return null;
4962
+ const why = /Owner override by @[\w-]+: ([\s\S]*?)\. Missing:/.exec(text);
4963
+ return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
4964
+ } catch {
4965
+ return null;
4762
4966
  }
4763
- return out;
4764
4967
  }
4765
- function unsafePathProblem(path) {
4766
- if (typeof path !== "string" || !path.length) return "A path is empty.";
4767
- if (/[\0-\x1f\x7f]/.test(path)) return `"${path.replace(/[\0-\x1f\x7f]/g, "?")}" has control characters.`;
4768
- if (path.includes("\\")) return `"${path}" uses backslashes: paths use forward slashes.`;
4769
- if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.startsWith("~")) return `"${path}" is absolute: paths are relative to the repository root.`;
4770
- const segs = path.split("/");
4771
- if (segs.some((s) => s === "" || s === "." || s === "..")) return `"${path}" has an empty, "." or ".." part: paths stay inside the repository.`;
4772
- if (segs.some((s) => s.toLowerCase() === ".git")) return `"${path}" is inside .git.`;
4968
+ function suggestionOverrideProblem(x) {
4969
+ const lists = approverLists(x.gov.owners, x.gov.rights ?? null, x.gov.teams);
4970
+ const holders = overrideHolders(x.gov.override, lists).map(norm);
4971
+ 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.";
4972
+ if (norm(x.viewer) !== norm(x.author)) return "The owner override is for your own suggestion: approve this one the usual way.";
4973
+ const others = eligibleReviewers("approval", { owners: lists, author: x.author });
4974
+ if (others.open || others.logins.length) return "Someone else can review it: ask them instead.";
4773
4975
  return null;
4774
4976
  }
4775
- const under = (path, root) => {
4776
- const r = root.replace(/\/+$/, "");
4777
- return !r || r === "." || path === r || path.startsWith(`${r}/`);
4778
- };
4779
- function isGuardrailPath(path, guardrails) {
4780
- return guardrails.some((g) => g.endsWith("/") ? path.startsWith(g) || path === g.slice(0, -1) : path === g);
4781
- }
4782
- function suggestionAllowedRoots(kind, config) {
4783
- const tokenRoot2 = tokensPath(config) || ".";
4784
- const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
4785
- switch (kind) {
4786
- case "token-change":
4787
- return [tokenRoot2, ...changesets];
4788
- case "docs":
4789
- return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
4790
- case "component-spec":
4791
- return [".bridge/figma"];
4792
- case "brand":
4793
- return [tokenRoot2, ...changesets, CONFIG_FILE];
4794
- case "config":
4795
- return [CONFIG_FILE];
4796
- case "revert":
4797
- return void 0;
4977
+ function suggestionOverrideOf(comments, gov, opts) {
4978
+ if (!gov.override || !opts.author) return null;
4979
+ for (const c of [...comments].reverse()) {
4980
+ const r = parseSuggestionOverrideComment(c.body);
4981
+ if (!r || r.payloadSha !== opts.payloadSha || !c.author || norm(c.author) !== norm(r.login)) continue;
4982
+ if (suggestionOverrideProblem({ viewer: c.author, author: opts.author, gov })) continue;
4983
+ if (overrideReasonProblem(r.reason, gov.override)) continue;
4984
+ return r;
4798
4985
  }
4986
+ return null;
4799
4987
  }
4800
- function suggestionPathProblems(p, config) {
4801
- const out = [];
4802
- const roots = suggestionAllowedRoots(p.kind, config);
4803
- const guardrails = config.governance.guardrails;
4804
- const check = (path) => {
4805
- const bad = unsafePathProblem(path);
4806
- if (bad) return out.push(bad);
4807
- if (roots && !roots.some((r) => under(path, r))) return out.push(`"${path}" is outside what a ${p.kind} suggestion may change (${roots.join(", ")}).`);
4808
- const configAllowed = path === CONFIG_FILE && (p.kind === "config" || p.kind === "brand");
4809
- if (!configAllowed && isGuardrailPath(path, guardrails)) return out.push(`"${path}" is a guardrail file: change it in a pull request, not a suggestion.`);
4810
- return 0;
4811
- };
4812
- for (const f of p.files ?? []) check(f.path);
4813
- for (const d of p.docs ?? []) check(d.path);
4814
- if (p.componentSpec) check(p.componentSpec.specPath);
4815
- for (const e of p.tokens?.edits ?? []) {
4816
- if (e.file === void 0) continue;
4817
- const bad = unsafePathProblem(e.file);
4818
- if (bad) out.push(bad);
4819
- }
4820
- return [...new Set(out)];
4988
+ const norm = (l) => l.trim().replace(/^@/, "").toLowerCase();
4989
+ const COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
4990
+ function summarizeSuggestionApprovals(comments, gov, opts) {
4991
+ const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
4992
+ const owners = new Set([...lists.design, ...lists.tech].map(norm));
4993
+ const anyOwner = owners.size > 0;
4994
+ const reviews = [];
4995
+ const latestOld = /* @__PURE__ */ new Map();
4996
+ const ignored = /* @__PURE__ */ new Set();
4997
+ const ordered = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
4998
+ ordered.forEach(({ c }, i) => {
4999
+ const r = parseSuggestionReviewComment(c.body);
5000
+ if (!r || r.event === "comment" || !c.author) return;
5001
+ const login = norm(c.author);
5002
+ if (opts.author && login === norm(opts.author)) return;
5003
+ const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
5004
+ if (!counts) {
5005
+ ignored.add(c.author);
5006
+ return;
5007
+ }
5008
+ const at = String(i).padStart(6, "0");
5009
+ if (r.payloadSha !== opts.payloadSha) {
5010
+ latestOld.set(c.author, r.event);
5011
+ reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
5012
+ return;
5013
+ }
5014
+ latestOld.delete(c.author);
5015
+ reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
5016
+ });
5017
+ const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
5018
+ const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
5019
+ const out = { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
5020
+ const override = summary.changesRequested.length ? null : suggestionOverrideOf(comments, gov, { payloadSha: opts.payloadSha, author: opts.author });
5021
+ return override ? { ...out, design: true, tech: true, owner: true, override } : out;
5022
+ }
5023
+ function suggestionReviewState(tier, approvals) {
5024
+ if (approvals.changesRequested.length) return "changes-requested";
5025
+ return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
4821
5026
  }
4822
5027
  const SUGGESTION_APPLY_EXIT = {
4823
5028
  ready: 0,
@@ -4952,7 +5157,7 @@ async function planSuggestionApply(input) {
4952
5157
  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.`);
4953
5158
  if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
4954
5159
  }
4955
- const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
5160
+ const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth, override: config.governance.override ?? null };
4956
5161
  const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
4957
5162
  const review = suggestionReviewState(payload.tier.tier, approvals);
4958
5163
  const missing = pendingApprovals(payload.tier.tier, approvals);
@@ -5248,6 +5453,27 @@ async function reviewSuggestionOnGitHub(gh, n, input, gov) {
5248
5453
  if (labelWarning) warnings.push(labelWarning);
5249
5454
  return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
5250
5455
  }
5456
+ async function overrideSuggestionOnGitHub(gh, n, input, gov) {
5457
+ if (!gov.override) throw new GitHubError(422, "The review rules allow no owner override.");
5458
+ const before = await getSuggestionOnGitHub(gh, n, gov);
5459
+ if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be approved: ${before.parsed.message}`);
5460
+ if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed.`);
5461
+ const sha = before.parsed.sha;
5462
+ if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and approve the new version.`);
5463
+ const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
5464
+ const problem = suggestionOverrideProblem({ viewer: by, author: before.issue.author, gov }) ?? overrideReasonProblem(input.reason, gov.override);
5465
+ if (problem) throw new GitHubError(422, problem);
5466
+ if (before.approvals?.changesRequested.length) throw new GitHubError(422, `Changes were asked for on #${n}: the owner override covers missing approvals only.`);
5467
+ 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");
5468
+ const posted = await gh.createIssueComment(n, composeSuggestionOverrideComment({ login: by, reason: input.reason, missing, payloadSha: sha }));
5469
+ const after = await getSuggestionOnGitHub(gh, n, gov);
5470
+ const counts = Boolean(after.approvals?.override);
5471
+ const warnings = [];
5472
+ if (!counts) warnings.push("The owner override is posted but does not count: check the owners and the override in bridge.config.json.");
5473
+ const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, after.status);
5474
+ if (labelWarning) warnings.push(labelWarning);
5475
+ return { commentUrl: posted.url, payloadSha: sha, status: after.status, counts, warnings };
5476
+ }
5251
5477
  async function linkSuggestionPrOnGitHub(gh, n, link) {
5252
5478
  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.");
5253
5479
  const issue = await gh.getIssue(n);
@@ -5678,317 +5904,337 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
5678
5904
  return out;
5679
5905
  }
5680
5906
  export {
5681
- composeProposalBody as $,
5682
- explorationPrTitle as A,
5683
- explorationPrBody as B,
5684
- compareSpecToMeasured as C,
5907
+ overrideComment as $,
5908
+ explorationNote as A,
5909
+ explorationNotePath as B,
5910
+ explorationPrTitle as C,
5685
5911
  DRAFT_LINE_MAX as D,
5686
- EXPLORATION_PREFIX as E,
5687
- variantKeyOf as F,
5912
+ EXPLORATION_ARCHIVED_LABEL as E,
5913
+ explorationPrBody as F,
5688
5914
  GitHubError as G,
5689
- withoutAddedWrappers as H,
5690
- pairParts as I,
5691
- COLOR_FIELDS as J,
5692
- hashSpec as K,
5693
- overrideComment as L,
5694
- overrideRefusedComment as M,
5695
- suggestionStateFromLabels as N,
5696
- OVERRIDE_HEADING as O,
5915
+ suggestionStateFromLabels as H,
5916
+ suggestionStatus as I,
5917
+ composeRequestIssue as J,
5918
+ EXPLORATION_NOTES_DIR as K,
5919
+ isExplorationBranch as L,
5920
+ doneExploringPlan as M,
5921
+ withDoneExploring as N,
5922
+ explorationStartingPoint as O,
5697
5923
  PROSE_KEYS as P,
5698
- suggestionStatus as Q,
5924
+ explorationRefLabel as Q,
5699
5925
  REQUESTS_BRANCH as R,
5700
- SPEC_FIELDS as S,
5701
- composeRequestIssue as T,
5702
- EXPLORATION_NOTES_DIR as U,
5703
- isExplorationBranch as V,
5704
- diffLayered as W,
5705
- contrastDelta as X,
5706
- runLayeredContrast as Y,
5707
- layeredImpact as Z,
5708
- classifyLayered as _,
5926
+ compareSpecToMeasured as S,
5927
+ variantKeyOf as T,
5928
+ matchVariant as U,
5929
+ withoutAddedWrappers as V,
5930
+ pairParts as W,
5931
+ hashSpec as X,
5932
+ SPEC_FIELDS as Y,
5933
+ COLOR_FIELDS as Z,
5934
+ DRAFTED_BY_CLAUDE_LABEL as _,
5709
5935
  parseChangelog as a,
5710
- REVIEW_STATUS_ORDER as a$,
5711
- createSuggestionOnGitHub as a0,
5712
- gitBlobSha as a1,
5713
- SUGGESTION_SCHEMA as a2,
5714
- branchWithJiraKey as a3,
5715
- titleWithJiraKey as a4,
5716
- figmaSpecPath as a5,
5717
- formatFigmaSpec as a6,
5718
- figmaDraftBrief as a7,
5719
- parseFigmaOrigin as a8,
5720
- figmaPushStatus as a9,
5721
- requestIssueTitle as aA,
5722
- pullState as aB,
5723
- summarizeCi as aC,
5724
- summarizeApprovals as aD,
5725
- BULK_LIMIT as aE,
5726
- CI_STATE_LABEL as aF,
5727
- COMPONENT_SPECS_VERSION as aG,
5728
- CONFLICT_REASON as aH,
5729
- DEFAULT_COMPONENT_SPECS_FILE as aI,
5730
- DEFAULT_PROTECTED_BRANCHES as aJ,
5731
- DOCS_CHANGE_BUMP as aK,
5732
- DOCS_CLI_ORIGIN as aL,
5733
- DOCS_EDITOR_ORIGIN as aM,
5734
- DOCS_KEYS as aN,
5735
- DOCS_MCP_ORIGIN as aO,
5736
- EXPLORATION_CHECK as aP,
5737
- EXPLORATION_STATUS_LABEL as aQ,
5738
- EXPLORATION_TIER_LABEL as aR,
5739
- EXPLORATION_USE_CASES as aS,
5740
- FORMER_MEMBER as aT,
5741
- LOOK_AGAIN_STATUS_LABEL as aU,
5742
- MeasuredSpecsError as aV,
5743
- PULL_STATE_PILL as aW,
5744
- ProtectedBranchError as aX,
5745
- REDUCED_MOTION_NOTE as aY,
5746
- REQUEST_STATUS_LABEL as aZ,
5747
- REVIEW_STATUS_LABEL as a_,
5748
- parseFigmaSpec as aa,
5749
- FIGMA_SPEC_DIR as ab,
5750
- FROM_FIGMA_LABEL as ac,
5751
- withFigmaOrigin as ad,
5752
- WAITING_FOR_CLAUDE_LABEL as ae,
5753
- DRAFTED_BY_CLAUDE_LABEL as af,
5754
- planComponentPush as ag,
5755
- figmaOriginSection as ah,
5756
- figmaBranchTitle as ai,
5757
- figmaSpecSlug as aj,
5758
- figmaDraftBody as ak,
5759
- REQUEST_LABEL as al,
5760
- figmaNodeUrl as am,
5761
- parentDir as an,
5762
- parseComponentSpecs as ao,
5763
- readRepoTexts as ap,
5764
- normalizeRepoPath as aq,
5765
- isUnder as ar,
5766
- componentMotion as as,
5767
- GitHubClient as at,
5768
- effectiveTier as au,
5769
- parseRequestIssue as av,
5770
- requestStatus as aw,
5771
- listSuggestionsOnGitHub as ax,
5772
- getSuggestionOnGitHub as ay,
5773
- closingIssueNumbers as az,
5936
+ EXPLORATION_TIER_LABEL as a$,
5937
+ OVERRIDE_HEADING as a0,
5938
+ overrideRefusedComment as a1,
5939
+ diffLayered as a2,
5940
+ contrastDelta as a3,
5941
+ runLayeredContrast as a4,
5942
+ layeredImpact as a5,
5943
+ classifyLayered as a6,
5944
+ composeProposalBody as a7,
5945
+ createSuggestionOnGitHub as a8,
5946
+ gitBlobSha as a9,
5947
+ GitHubClient as aA,
5948
+ effectiveTier as aB,
5949
+ parseRequestIssue as aC,
5950
+ requestStatus as aD,
5951
+ listSuggestionsOnGitHub as aE,
5952
+ getSuggestionOnGitHub as aF,
5953
+ closingIssueNumbers as aG,
5954
+ requestIssueTitle as aH,
5955
+ pullState as aI,
5956
+ summarizeCi as aJ,
5957
+ summarizeApprovals as aK,
5958
+ BULK_LIMIT as aL,
5959
+ CI_STATE_LABEL as aM,
5960
+ COMPONENT_SPECS_VERSION as aN,
5961
+ CONFLICT_REASON as aO,
5962
+ DEFAULT_COMPONENT_SPECS_FILE as aP,
5963
+ DEFAULT_PROTECTED_BRANCHES as aQ,
5964
+ DOCS_CHANGE_BUMP as aR,
5965
+ DOCS_CLI_ORIGIN as aS,
5966
+ DOCS_EDITOR_ORIGIN as aT,
5967
+ DOCS_KEYS as aU,
5968
+ DOCS_MCP_ORIGIN as aV,
5969
+ EXPLORATION_CHECK as aW,
5970
+ EXPLORATION_READY_LABEL as aX,
5971
+ EXPLORATION_SLUG_MAX as aY,
5972
+ EXPLORATION_STARTING_POINT as aZ,
5973
+ EXPLORATION_STATUS_LABEL as a_,
5974
+ SUGGESTION_SCHEMA as aa,
5975
+ branchWithJiraKey as ab,
5976
+ titleWithJiraKey as ac,
5977
+ figmaSpecPath as ad,
5978
+ formatFigmaSpec as ae,
5979
+ figmaDraftBrief as af,
5980
+ parseFigmaOrigin as ag,
5981
+ figmaPushStatus as ah,
5982
+ parseFigmaSpec as ai,
5983
+ FIGMA_SPEC_DIR as aj,
5984
+ FROM_FIGMA_LABEL as ak,
5985
+ withFigmaOrigin as al,
5986
+ WAITING_FOR_CLAUDE_LABEL as am,
5987
+ planComponentPush as an,
5988
+ figmaOriginSection as ao,
5989
+ figmaBranchTitle as ap,
5990
+ figmaSpecSlug as aq,
5991
+ figmaDraftBody as ar,
5992
+ REQUEST_LABEL as as,
5993
+ figmaNodeUrl as at,
5994
+ parentDir as au,
5995
+ parseComponentSpecs as av,
5996
+ readRepoTexts as aw,
5997
+ normalizeRepoPath as ax,
5998
+ isUnder as ay,
5999
+ componentMotion as az,
5774
6000
  buildReport as b,
5775
- docsText as b$,
5776
- SUGGESTION_APPLIED_MARK as b0,
5777
- SUGGESTION_APPLY_EXIT as b1,
5778
- SUGGESTION_FINAL_LABELS as b2,
5779
- SUGGESTION_KINDS as b3,
5780
- SUGGESTION_LABEL as b4,
5781
- SUGGESTION_LIMITS as b5,
5782
- SUGGESTION_REVIEW_MARK as b6,
5783
- SUGGESTION_STATE_LABELS as b7,
5784
- SUGGESTION_STATUS_LABEL as b8,
5785
- SUGGESTION_STATUS_LABELS as b9,
5786
- cleanDocs as bA,
5787
- clearSharedEtags as bB,
5788
- commitFileOnGitHub as bC,
5789
- componentContrastPairs as bD,
5790
- componentsUsingMotionToken as bE,
5791
- composeChangeset as bF,
5792
- composeSuggestionAppliedComment as bG,
5793
- composeSuggestionIssue as bH,
5794
- composeSuggestionReviewComment as bI,
5795
- continueDocsProposalOnGitHub as bJ,
5796
- contrastDeltaTable as bK,
5797
- countChanges as bL,
5798
- coverageSummary as bM,
5799
- cssColorToRgb as bN,
5800
- cssColorsToRgb as bO,
5801
- cssModuleClasses as bP,
5802
- declineComment as bQ,
5803
- declineReasonProblems as bR,
5804
- declineRequestOnGitHub as bS,
5805
- declineSuggestionOnGitHub as bT,
5806
- defaultDocsFooter as bU,
5807
- docsAuthorLine as bV,
5808
- docsFileText as bW,
5809
- docsProposal as bX,
5810
- docsSectionShapes as bY,
5811
- docsSlug as bZ,
5812
- docsSuggestionPayload as b_,
5813
- SUGGESTION_TOO_LARGE as ba,
5814
- StorybookIndexError as bb,
5815
- SuggestionError as bc,
5816
- TOKEN_REJECTED_MESSAGE as bd,
5817
- TOKEN_SETTINGS_URL as be,
5818
- TRIGGER_LABELS as bf,
5819
- UNRELEASED as bg,
5820
- acceptDocsDraft as bh,
5821
- acceptDocsText as bi,
5822
- allowedMergeMethods as bj,
5823
- anatomyLabel as bk,
5824
- appliedStatus as bl,
5825
- approvedSuggestionNumber as bm,
5826
- approvedSuggestionOnGitHub as bn,
5827
- assertSafeBranch as bo,
5828
- assignRequestOnGitHub as bp,
5829
- assignVersions as bq,
5830
- base64ToUtf8 as br,
5831
- branchVerdict as bs,
5832
- bridgeMcpInstallCommand as bt,
5833
- canOverride as bu,
5834
- canonicalJson as bv,
5835
- changeTable as bw,
5836
- changesetBump as bx,
5837
- classifyAuth as by,
5838
- claudeStage as bz,
6001
+ cssModuleClasses as b$,
6002
+ EXPLORATION_USE_CASES as b0,
6003
+ FORMER_MEMBER as b1,
6004
+ LOOK_AGAIN_STATUS_LABEL as b2,
6005
+ MeasuredSpecsError as b3,
6006
+ PULL_STATE_PILL as b4,
6007
+ ProtectedBranchError as b5,
6008
+ REDUCED_MOTION_NOTE as b6,
6009
+ REQUEST_STATUS_LABEL as b7,
6010
+ REVIEW_STATUS_LABEL as b8,
6011
+ REVIEW_STATUS_ORDER as b9,
6012
+ assignRequestOnGitHub as bA,
6013
+ assignVersions as bB,
6014
+ base64ToUtf8 as bC,
6015
+ branchVerdict as bD,
6016
+ bridgeMcpInstallCommand as bE,
6017
+ canOverride as bF,
6018
+ canonicalJson as bG,
6019
+ changeTable as bH,
6020
+ changesetBump as bI,
6021
+ classifyAuth as bJ,
6022
+ claudeStage as bK,
6023
+ cleanDocs as bL,
6024
+ clearSharedEtags as bM,
6025
+ commitFileOnGitHub as bN,
6026
+ componentContrastPairs as bO,
6027
+ componentsUsingMotionToken as bP,
6028
+ composeChangeset as bQ,
6029
+ composeSuggestionAppliedComment as bR,
6030
+ composeSuggestionIssue as bS,
6031
+ composeSuggestionOverrideComment as bT,
6032
+ composeSuggestionReviewComment as bU,
6033
+ continueDocsProposalOnGitHub as bV,
6034
+ contrastDeltaTable as bW,
6035
+ countChanges as bX,
6036
+ coverageSummary as bY,
6037
+ cssColorToRgb as bZ,
6038
+ cssColorsToRgb as b_,
6039
+ SUGGESTION_APPLIED_MARK as ba,
6040
+ SUGGESTION_APPLY_EXIT as bb,
6041
+ SUGGESTION_FINAL_LABELS as bc,
6042
+ SUGGESTION_KINDS as bd,
6043
+ SUGGESTION_LABEL as be,
6044
+ SUGGESTION_LIMITS as bf,
6045
+ SUGGESTION_OVERRIDE_MARK as bg,
6046
+ SUGGESTION_REVIEW_MARK as bh,
6047
+ SUGGESTION_STATE_LABELS as bi,
6048
+ SUGGESTION_STATUS_LABEL as bj,
6049
+ SUGGESTION_STATUS_LABELS as bk,
6050
+ SUGGESTION_TOO_LARGE as bl,
6051
+ StorybookIndexError as bm,
6052
+ SuggestionError as bn,
6053
+ TOKEN_REJECTED_MESSAGE as bo,
6054
+ TOKEN_SETTINGS_URL as bp,
6055
+ TRIGGER_LABELS as bq,
6056
+ UNRELEASED as br,
6057
+ acceptDocsDraft as bs,
6058
+ acceptDocsText as bt,
6059
+ allowedMergeMethods as bu,
6060
+ anatomyLabel as bv,
6061
+ appliedStatus as bw,
6062
+ approvedSuggestionNumber as bx,
6063
+ approvedSuggestionOnGitHub as by,
6064
+ assertSafeBranch as bz,
5839
6065
  collectHistory as c,
5840
- parseOverrideComment as c$,
5841
- draftChangeLine as c0,
5842
- draftPushDescription as c1,
5843
- draftRequestText as c2,
5844
- eligibleReviewers as c3,
5845
- evalCalc as c4,
5846
- explorationBranch as c5,
5847
- explorationRules as c6,
5848
- explorationShareMessage as c7,
5849
- explorationSlugFromBranch as c8,
5850
- fetchStorybookIndex as c9,
5851
- jiraKeyOf as cA,
5852
- joinRepoPath as cB,
5853
- linkRequestToExplorationOnGitHub as cC,
5854
- linkSuggestionPrOnGitHub as cD,
5855
- linkedPullsOf as cE,
5856
- loadRepoFiles as cF,
5857
- lookAgainReviewers as cG,
5858
- lowerFirstWord as cH,
5859
- markSuggestionsAppliedOnGitHub as cI,
5860
- markUsedTokens as cJ,
5861
- memoryEtagStore as cK,
5862
- memorySource as cL,
5863
- mergeGate as cM,
5864
- missingPermissionMessage as cN,
5865
- missingProse as cO,
5866
- modeLabel as cP,
5867
- needsDocsReview as cQ,
5868
- normalizeStorybookUrl as cR,
5869
- openPulls as cS,
5870
- overrideHolders as cT,
5871
- overrideReasonMin as cU,
5872
- overrideReasonProblem as cV,
5873
- ownerOverridePolicy as cW,
5874
- parseBoxShadow as cX,
5875
- parseChangesetText as cY,
5876
- parseJiraKey as cZ,
5877
- parseMeasuredSpecsFile as c_,
5878
- figmaCell as ca,
5879
- fileModes as cb,
5880
- filterHistory as cc,
5881
- findOpenDocsProposal as cd,
5882
- findOpenDocsProposalOnGitHub as ce,
5883
- firstParagraph as cf,
5884
- formatDocsJson as cg,
5885
- friendlyMessage as ch,
5886
- githubSuggestionWorkspace as ci,
5887
- groupStorybookComponents as cj,
5888
- hasOwnerApproval as ck,
5889
- hasSuggestionBlock as cl,
5890
- hasWriteAccess as cm,
5891
- iconComponentName as cn,
5892
- installationFor as co,
5893
- isActiveExploration as cp,
5894
- isAutomationPull as cq,
5895
- isDocsProposalBranch as cr,
5896
- isDraft as cs,
5897
- isGuardrailPath as ct,
5898
- isJiraKey as cu,
5899
- isMissingPermission as cv,
5900
- isPlatformRefusal as cw,
5901
- isReleasePull as cx,
5902
- isTokenRejected as cy,
5903
- jiraIssueFields as cz,
6066
+ mergeGate as c$,
6067
+ declineComment as c0,
6068
+ declineReasonProblems as c1,
6069
+ declineRequestOnGitHub as c2,
6070
+ declineSuggestionOnGitHub as c3,
6071
+ defaultDocsFooter as c4,
6072
+ docsAuthorLine as c5,
6073
+ docsFileText as c6,
6074
+ docsProposal as c7,
6075
+ docsSectionShapes as c8,
6076
+ docsSlug as c9,
6077
+ iconComponentName as cA,
6078
+ installationFor as cB,
6079
+ isActiveExploration as cC,
6080
+ isAutomationPull as cD,
6081
+ isDocsProposalBranch as cE,
6082
+ isDraft as cF,
6083
+ isGuardrailPath as cG,
6084
+ isHandedOff as cH,
6085
+ isHeldExploration as cI,
6086
+ isJiraKey as cJ,
6087
+ isMissingPermission as cK,
6088
+ isPlatformRefusal as cL,
6089
+ isReleasePull as cM,
6090
+ isTokenRejected as cN,
6091
+ jiraIssueFields as cO,
6092
+ jiraKeyOf as cP,
6093
+ joinRepoPath as cQ,
6094
+ linkRequestToExplorationOnGitHub as cR,
6095
+ linkSuggestionPrOnGitHub as cS,
6096
+ linkedPullsOf as cT,
6097
+ loadRepoFiles as cU,
6098
+ lookAgainReviewers as cV,
6099
+ lowerFirstWord as cW,
6100
+ markSuggestionsAppliedOnGitHub as cX,
6101
+ markUsedTokens as cY,
6102
+ memoryEtagStore as cZ,
6103
+ memorySource as c_,
6104
+ docsSuggestionPayload as ca,
6105
+ docsText as cb,
6106
+ draftChangeLine as cc,
6107
+ draftPushDescription as cd,
6108
+ draftRequestText as ce,
6109
+ eligibleReviewers as cf,
6110
+ evalCalc as cg,
6111
+ explorationBranch as ch,
6112
+ explorationRules as ci,
6113
+ explorationShareMessage as cj,
6114
+ explorationSlugFromBranch as ck,
6115
+ explorationTakeCommands as cl,
6116
+ fetchStorybookIndex as cm,
6117
+ figmaCell as cn,
6118
+ fileModes as co,
6119
+ filterHistory as cp,
6120
+ findOpenDocsProposal as cq,
6121
+ findOpenDocsProposalOnGitHub as cr,
6122
+ firstParagraph as cs,
6123
+ formatDocsJson as ct,
6124
+ friendlyMessage as cu,
6125
+ githubSuggestionWorkspace as cv,
6126
+ groupStorybookComponents as cw,
6127
+ hasOwnerApproval as cx,
6128
+ hasSuggestionBlock as cy,
6129
+ hasWriteAccess as cz,
5904
6130
  applyTokenEdits as d,
5905
- summarizeSuggestionApprovals as d$,
5906
- parseRepo as d0,
5907
- parseStorybookIndex as d1,
5908
- parseStructureChange as d2,
5909
- parseSuggestionAppliedComment as d3,
5910
- parseSuggestionIssue as d4,
5911
- parseSuggestionReviewComment as d5,
5912
- parseTier as d6,
5913
- patchDocsJson as d7,
5914
- pendingApprovals as d8,
5915
- permissionLabel as d9,
5916
- sha256Hex as dA,
5917
- sharedTokenPrefix as dB,
5918
- shortVariant as dC,
5919
- shortVariants as dD,
5920
- specHash as dE,
5921
- specsUsage as dF,
5922
- stalenessStatus as dG,
5923
- storybookIframeUrl as dH,
5924
- storybookIndexUrls as dI,
5925
- storybookManagerUrl as dJ,
5926
- subjectTitle as dK,
5927
- suggestionAllowedRoots as dL,
5928
- suggestionAppliedComment as dM,
5929
- suggestionApplyTable as dN,
5930
- suggestionIsTokenValues as dO,
5931
- suggestionIssueTitle as dP,
5932
- suggestionKindFromLabels as dQ,
5933
- suggestionLabelChanges as dR,
5934
- suggestionLabels as dS,
5935
- suggestionPathProblems as dT,
5936
- suggestionPayloadSha as dU,
5937
- suggestionReviewState as dV,
5938
- suggestionShapeProblems as dW,
5939
- suggestionStalenessOnGitHub as dX,
5940
- suggestionStateLabel as dY,
5941
- suggestionSubject as dZ,
5942
- suggestionTouches as d_,
5943
- planRestore as da,
5944
- planRevert as db,
5945
- planSuggestionApply as dc,
5946
- planToJson as dd,
5947
- prNumberFromSubject as de,
5948
- prefillDocs as df,
5949
- proposeDocsOnGitHub as dg,
5950
- propsTable as dh,
5951
- pushChangeRows as di,
5952
- rateLimitMessage as dj,
5953
- releasePull as dk,
5954
- reopenEligibility as dl,
5955
- requestBrief as dm,
5956
- requestImagePath as dn,
5957
- requesterFirstName as dp,
5958
- requesterLine as dq,
5959
- requesterOf as dr,
5960
- requiredApprovals as ds,
5961
- requiredPermission as dt,
5962
- reviewQueue as du,
5963
- reviewStatusKind as dv,
5964
- reviewStatusLabel as dw,
5965
- reviewSuggestionOnGitHub as dx,
5966
- reviewersToAsk as dy,
5967
- sectionsNeedingReview as dz,
6131
+ storybookManagerUrl as d$,
6132
+ missingPermissionMessage as d0,
6133
+ missingProse as d1,
6134
+ modeLabel as d2,
6135
+ needsDocsReview as d3,
6136
+ normalizeStorybookUrl as d4,
6137
+ openPulls as d5,
6138
+ overrideHolders as d6,
6139
+ overrideReasonMin as d7,
6140
+ overrideReasonProblem as d8,
6141
+ overrideSuggestionOnGitHub as d9,
6142
+ propsTable as dA,
6143
+ pushChangeRows as dB,
6144
+ rateLimitMessage as dC,
6145
+ releasePull as dD,
6146
+ reopenEligibility as dE,
6147
+ requestBrief as dF,
6148
+ requestImagePath as dG,
6149
+ requesterFirstName as dH,
6150
+ requesterLine as dI,
6151
+ requesterOf as dJ,
6152
+ requiredApprovals as dK,
6153
+ requiredPermission as dL,
6154
+ reviewQueue as dM,
6155
+ reviewStatusKind as dN,
6156
+ reviewStatusLabel as dO,
6157
+ reviewSuggestionOnGitHub as dP,
6158
+ reviewersToAsk as dQ,
6159
+ sectionsNeedingReview as dR,
6160
+ sha256Hex as dS,
6161
+ sharedTokenPrefix as dT,
6162
+ shortVariant as dU,
6163
+ shortVariants as dV,
6164
+ specHash as dW,
6165
+ specsUsage as dX,
6166
+ stalenessStatus as dY,
6167
+ storybookIframeUrl as dZ,
6168
+ storybookIndexUrls as d_,
6169
+ ownerOverridePolicy as da,
6170
+ parseBoxShadow as db,
6171
+ parseChangesetText as dc,
6172
+ parseExplorationRef as dd,
6173
+ parseJiraKey as de,
6174
+ parseMeasuredSpecsFile as df,
6175
+ parseOverrideComment as dg,
6176
+ parseRepo as dh,
6177
+ parseStorybookIndex as di,
6178
+ parseStructureChange as dj,
6179
+ parseSuggestionAppliedComment as dk,
6180
+ parseSuggestionIssue as dl,
6181
+ parseSuggestionOverrideComment as dm,
6182
+ parseSuggestionReviewComment as dn,
6183
+ parseTier as dp,
6184
+ patchDocsJson as dq,
6185
+ pendingApprovals as dr,
6186
+ permissionLabel as ds,
6187
+ planRestore as dt,
6188
+ planRevert as du,
6189
+ planSuggestionApply as dv,
6190
+ planToJson as dw,
6191
+ prNumberFromSubject as dx,
6192
+ prefillDocs as dy,
6193
+ proposeDocsOnGitHub as dz,
5968
6194
  buildHistoryEntry as e,
5969
- syncSuggestionStateLabel as e0,
5970
- titleOf as e1,
5971
- toChangeSummary as e2,
5972
- tokenCssVar as e3,
5973
- tokenFingerprint as e4,
5974
- tokenHistory as e5,
5975
- tokenVariablePath as e6,
5976
- unsafePathProblem as e7,
5977
- utf8ToBase64 as e8,
5978
- validateComponentSpec as e9,
5979
- validateDocs as ea,
5980
- validateIntegrations as eb,
5981
- variantName as ec,
5982
- versionTimeline as ed,
5983
- webStorageEtagStore as ee,
5984
- withAuthorLine as ef,
5985
- withFigmaPushApprovals as eg,
5986
- withOutdated as eh,
5987
- withRequestExploration as ei,
5988
- withSrgbColors as ej,
5989
- withSuggestionApprovals as ek,
5990
- withTokenValueApprovals as el,
5991
- withoutJiraKey as em,
6195
+ subjectTitle as e0,
6196
+ suggestionAllowedRoots as e1,
6197
+ suggestionAppliedComment as e2,
6198
+ suggestionApplyTable as e3,
6199
+ suggestionIsTokenValues as e4,
6200
+ suggestionIssueTitle as e5,
6201
+ suggestionKindFromLabels as e6,
6202
+ suggestionLabelChanges as e7,
6203
+ suggestionLabels as e8,
6204
+ suggestionOverrideOf as e9,
6205
+ withFigmaPushApprovals as eA,
6206
+ withOutdated as eB,
6207
+ withRequestExploration as eC,
6208
+ withSrgbColors as eD,
6209
+ withSuggestionApprovals as eE,
6210
+ withTokenValueApprovals as eF,
6211
+ withoutJiraKey as eG,
6212
+ suggestionOverrideProblem as ea,
6213
+ suggestionPathProblems as eb,
6214
+ suggestionPayloadSha as ec,
6215
+ suggestionReviewState as ed,
6216
+ suggestionShapeProblems as ee,
6217
+ suggestionStalenessOnGitHub as ef,
6218
+ suggestionStateLabel as eg,
6219
+ suggestionSubject as eh,
6220
+ suggestionTouches as ei,
6221
+ summarizeSuggestionApprovals as ej,
6222
+ syncSuggestionStateLabel as ek,
6223
+ titleOf as el,
6224
+ toChangeSummary as em,
6225
+ tokenCssVar as en,
6226
+ tokenFingerprint as eo,
6227
+ tokenHistory as ep,
6228
+ tokenVariablePath as eq,
6229
+ unsafePathProblem as er,
6230
+ utf8ToBase64 as es,
6231
+ validateComponentSpec as et,
6232
+ validateDocs as eu,
6233
+ validateIntegrations as ev,
6234
+ variantName as ew,
6235
+ versionTimeline as ex,
6236
+ webStorageEtagStore as ey,
6237
+ withAuthorLine as ez,
5992
6238
  coerceValue as f,
5993
6239
  checkLayeredPair as g,
5994
6240
  formatChangeset as h,
@@ -5998,17 +6244,17 @@ export {
5998
6244
  REQUEST_KINDS as l,
5999
6245
  mergeReleases as m,
6000
6246
  parseExplorationBody as n,
6001
- explorationSlug as o,
6247
+ EXPLORATION_PREFIX as o,
6002
6248
  packageDirCandidates as p,
6003
- explorationStatus as q,
6249
+ explorationSlug as q,
6004
6250
  releasesFromTags as r,
6005
6251
  sanitizeDraftLine as s,
6006
- explorationTryIt as t,
6007
- isExplorationPull as u,
6008
- promotionPlan as v,
6009
- EXPLORATION_LABEL as w,
6010
- explorationProblems as x,
6011
- explorationNote as y,
6012
- explorationNotePath as z
6252
+ isArchivedExploration as t,
6253
+ explorationStatus as u,
6254
+ explorationTryIt as v,
6255
+ isExplorationPull as w,
6256
+ promotionPlan as x,
6257
+ EXPLORATION_LABEL as y,
6258
+ explorationProblems as z
6013
6259
  };
6014
- //# sourceMappingURL=repo-source-m0Tc603o.js.map
6260
+ //# sourceMappingURL=repo-source-CEBBBtm1.js.map