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