@bridge-ds/core 0.22.0 → 0.24.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 { 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-JL5tmL6u.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, _ as previewUrl, M as resolveBranchPreviews, aj as slugify, ai as readPreviewComment, V as CONFIG_FILE, N as loadConfig, am as ConfigError, af as DEFAULT_TOKEN_PATHS, al as globToRegExp, 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, 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-JL5tmL6u.js";
2
2
  function changesetBump(changes) {
3
3
  return tokenBump(changes) ?? "patch";
4
4
  }
@@ -361,193 +361,518 @@ function assertSafeBranch(branch, rules = {}) {
361
361
  throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
362
362
  }
363
363
  }
364
- class GitHubError extends Error {
365
- constructor(status, message, detail, info = {}) {
366
- super(message);
367
- this.status = status;
368
- this.detail = detail;
369
- this.name = "GitHubError";
370
- this.auth = info.auth;
371
- this.permission = info.permission;
372
- this.resetsAt = info.resetsAt;
373
- }
374
- status;
375
- detail;
376
- /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
377
- auth;
378
- /** With auth "missing-permission": the permission the call needed, when known. */
379
- permission;
380
- /** For a rate limit (status 429): when the limit resets, when known. */
381
- resetsAt;
382
- }
383
- const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
384
- const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
385
- const PERMISSION_LABELS = {
386
- issues: "Issues",
387
- pull_requests: "Pull requests",
388
- contents: "Contents",
389
- checks: "Checks",
390
- statuses: "Commit statuses",
391
- metadata: "Metadata",
392
- actions: "Actions",
393
- deployments: "Deployments",
394
- workflows: "Workflows",
395
- administration: "Administration"
364
+ const EXPLORATION_LABEL = "exploration";
365
+ const EXPLORATION_PREFIX = "explore/";
366
+ const EXPLORATION_NOTES_DIR = ".bridge/explorations";
367
+ const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
368
+ const EXPLORATION_CHECK = "Exploration not merged as is";
369
+ const EXPLORATION_READY_LABEL = "ready-for-devs";
370
+ const EXPLORATION_ARCHIVED_LABEL = "archived";
371
+ const EXPLORATION_USE_CASES = [
372
+ { 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." },
373
+ { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
374
+ { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
375
+ { 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." },
376
+ { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
377
+ ];
378
+ const EXPLORATION_STATUS_LABEL = {
379
+ active: "Running",
380
+ ready: "Waiting for code",
381
+ building: "Being built",
382
+ shipped: "Shipped",
383
+ promoted: "Promoted to review",
384
+ closed: "Closed"
396
385
  };
397
- function permissionLabel(permission) {
398
- return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
399
- }
400
- const TOKEN_SETTINGS_URL = "https://github.com/settings/personal-access-tokens";
401
- const TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
402
- function missingPermissionMessage(permission, action) {
403
- const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
404
- if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
405
- return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
406
- }
407
- function requiredPermission(path, acceptedHeader) {
408
- if (acceptedHeader) {
409
- const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
410
- if (first.length) return first[0];
411
- }
412
- const p = path.replace(/[?#].*$/, "");
413
- if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
414
- if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
415
- if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
416
- if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
417
- if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
418
- if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
419
- if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
420
- if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
421
- return void 0;
422
- }
423
- const PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
424
- function classifyAuth(status, detail, opts = {}) {
425
- if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
426
- if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
427
- if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
428
- return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
429
- }
430
- return {};
386
+ const MARK$2 = "bridge-exploration";
387
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
388
+ const EXPLORATION_SLUG_MAX = 60;
389
+ function explorationSlug(name) {
390
+ const s = name.trim().replace(/^explore\//i, "");
391
+ if (!/[a-z0-9]/i.test(s)) return "";
392
+ return slugify(s, EXPLORATION_SLUG_MAX);
431
393
  }
432
- function memoryEtagStore(max = 500) {
433
- const map = /* @__PURE__ */ new Map();
434
- return {
435
- get(key) {
436
- const e = map.get(key);
437
- if (e) {
438
- map.delete(key);
439
- map.set(key, e);
440
- }
441
- return e;
442
- },
443
- set(key, entry) {
444
- map.delete(key);
445
- map.set(key, entry);
446
- while (map.size > max) map.delete(map.keys().next().value);
447
- },
448
- clear: () => map.clear()
449
- };
394
+ function explorationBranch(nameOrBranch) {
395
+ return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
450
396
  }
451
- function webStorageEtagStore(storage, opts = {}) {
452
- const prefix = opts.prefix ?? "bridge.gh.etag:";
453
- const maxBytes = opts.maxEntryBytes ?? 512e3;
454
- return {
455
- get(key) {
456
- try {
457
- const raw = storage.getItem(prefix + key);
458
- if (!raw) return void 0;
459
- const e = JSON.parse(raw);
460
- return typeof e?.etag === "string" ? e : void 0;
461
- } catch {
462
- return void 0;
463
- }
464
- },
465
- set(key, entry) {
466
- try {
467
- const raw = JSON.stringify(entry);
468
- if (raw.length > maxBytes) return;
469
- storage.setItem(prefix + key, raw);
470
- } catch {
471
- }
472
- },
473
- clear() {
474
- try {
475
- const keys = [];
476
- for (let i = 0; i < storage.length; i++) {
477
- const k = storage.key(i);
478
- if (k?.startsWith(prefix)) keys.push(k);
479
- }
480
- for (const k of keys) storage.removeItem(k);
481
- } catch {
482
- }
483
- }
484
- };
397
+ function isExplorationBranch(ref) {
398
+ const r = (ref ?? "").replace(/^refs\/heads\//, "");
399
+ return r.startsWith(EXPLORATION_PREFIX) && SLUG_RE.test(r.slice(EXPLORATION_PREFIX.length));
485
400
  }
486
- const sharedEtags = memoryEtagStore();
487
- function clearSharedEtags() {
488
- sharedEtags.clear();
401
+ function explorationSlugFromBranch(ref) {
402
+ return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
489
403
  }
490
- function tokenFingerprint(token) {
491
- if (!token) return "anon";
492
- let a = 2166136261;
493
- let b = 16777619 ^ token.length;
494
- for (let i = 0; i < token.length; i++) {
495
- const c = token.charCodeAt(i);
496
- a = Math.imul(a ^ c, 16777619) >>> 0;
497
- b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
404
+ const REPO_NAME = /^[\w.-]+\/[\w.-]+$/;
405
+ const KEY_NAME = /^[a-z0-9][a-z0-9-]*$/i;
406
+ function parseExplorationRef(input) {
407
+ if (typeof input === "number") return { ref: input };
408
+ const s = input.trim();
409
+ const link = /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/i.exec(s);
410
+ if (link) return { repo: `${link[1]}/${link[2]}`, ref: Number(link[3]) };
411
+ if (/^#?\d+$/.test(s)) return { ref: Number(s.replace(/^#/, "")) };
412
+ const hash = /^(.+)#(\d+)$/.exec(s);
413
+ if (hash) {
414
+ const where2 = hash[1].trim();
415
+ if (REPO_NAME.test(where2)) return { repo: where2, ref: Number(hash[2]) };
416
+ if (KEY_NAME.test(where2)) return { product: where2.toLowerCase(), ref: Number(hash[2]) };
498
417
  }
499
- return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
500
- }
501
- function installationFor(list, owner) {
502
- const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
503
- if (!inst) return { reach: "no" };
504
- const selection = inst.repositorySelection ?? inst.repository_selection;
505
- return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
506
- }
507
- const MAX_PAGES = 10;
508
- function rateLimitMessage(x) {
509
- const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
510
- if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
511
- const hint = x.tokenHint ?? "add a GitHub token";
512
- return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
418
+ const named = /^([a-z0-9][a-z0-9-]*):(.+)$/i.exec(s);
419
+ if (named) return { product: named[1].toLowerCase(), ref: /^#?\d+$/.test(named[2].trim()) ? Number(named[2].trim().replace(/^#/, "")) : named[2].trim() };
420
+ return { ref: s };
513
421
  }
514
- const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
515
- const hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
516
- const isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
517
- const API = "https://api.github.com";
518
- const ACCEPT_JSON = "application/vnd.github+json";
519
- const ACCEPT_RAW = "application/vnd.github.raw";
520
- const RAW = "https://raw.githubusercontent.com";
521
- function parseRepo(repo) {
522
- const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
523
- if (!m) throw new Error(`"${repo}" is not a repository. Use the form owner/name.`);
524
- return { owner: m[1], name: m[2] };
422
+ const explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
423
+ const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
424
+ function explorationProblems(e) {
425
+ const out = [];
426
+ const name = e.name.trim();
427
+ const slug = e.slug?.trim() || explorationSlug(name);
428
+ if (!name) out.push('Name the exploration, for example "Denser data tables".');
429
+ else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
430
+ 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.");
431
+ return out;
525
432
  }
526
- function utf8ToBase64(text) {
527
- const bytes = new TextEncoder().encode(text);
528
- let bin = "";
529
- for (const b of bytes) bin += String.fromCharCode(b);
530
- return btoa(bin);
433
+ const useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
434
+ function explorationNote(e) {
435
+ const slug = e.slug || explorationSlug(e.name);
436
+ const uc = useCaseLabel(e.useCase);
437
+ return [
438
+ `# Exploration: ${e.name.trim()}`,
439
+ "",
440
+ ...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
441
+ `- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
442
+ ...uc ? [`- Use case: ${uc}`] : [],
443
+ ...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
444
+ "",
445
+ 'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
446
+ ""
447
+ ].join("\n");
531
448
  }
532
- function base64ToUtf8(b64) {
533
- const bin = atob(b64.replace(/\s/g, ""));
534
- const bytes = new Uint8Array(bin.length);
535
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
536
- return new TextDecoder("utf-8").decode(bytes);
449
+ function explorationPrTitle(e) {
450
+ const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
451
+ return `Exploration: ${n}`;
537
452
  }
538
- const copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
539
- const encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
540
- class GitHubClient {
541
- constructor(repo, token, fetchImpl = (url, init) => fetch(url, init), retry = DEFAULT_RETRY, options = {}) {
542
- this.token = token;
543
- this.fetchImpl = fetchImpl;
544
- this.retry = retry;
545
- this.options = options;
546
- const r = parseRepo(repo);
547
- this.owner = r.owner;
548
- this.name = r.name;
549
- this.api = (options.apiUrl ?? API).replace(/\/$/, "");
550
- this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
453
+ function explorationPrBody(e, ctx) {
454
+ const slug = e.slug || explorationSlug(e.name);
455
+ const branch = `${EXPLORATION_PREFIX}${slug}`;
456
+ const uc = useCaseLabel(e.useCase);
457
+ const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
458
+ const lines = [
459
+ `## Exploration: ${e.name.trim()}`,
460
+ "",
461
+ `> 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.`,
462
+ "",
463
+ ...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
464
+ ...uc ? [`**Use case.** ${uc}`, ""] : [],
465
+ "### What is experimental",
466
+ "",
467
+ ...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."],
468
+ ...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
469
+ ""
470
+ ];
471
+ if (ctx.inProduct) {
472
+ const p = ctx.inProduct;
473
+ lines.push(
474
+ "### Work on it",
475
+ "",
476
+ `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.`,
477
+ "",
478
+ `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.`,
479
+ "",
480
+ "### Previews",
481
+ ""
482
+ );
483
+ const url = p.preview ? previewUrl(p.preview, { branch, ...ctx.pr ? { pr: ctx.pr } : {} }) : null;
484
+ if (p.preview) lines.push("| App | Preview |", "| --- | --- |", `| ${p.name} | ${url ?? "Link in the Vercel comment on this pull request"} |`);
485
+ else lines.push(`The preview deploys of ${p.name}, if it has them, build this branch like any other.`);
486
+ } else {
487
+ lines.push(
488
+ "### Try it in a product staging branch",
489
+ "",
490
+ '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.'
491
+ );
492
+ lines.push("", "### Previews", "");
493
+ const links = resolveBranchPreviews(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
494
+ if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url ?? "Link in the Vercel comment on this pull request"} |`));
495
+ else lines.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
496
+ }
497
+ lines.push(
498
+ "",
499
+ "### When it is done",
500
+ "",
501
+ `- **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}\`.`,
502
+ `- **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."}`,
503
+ "",
504
+ explorationBlock({ ...e, slug })
505
+ );
506
+ return lines.join("\n");
507
+ }
508
+ function explorationBlock(e) {
509
+ const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
510
+ const data = {
511
+ name: e.name.trim(),
512
+ purpose: (e.purpose ?? "").trim(),
513
+ slug: e.slug || explorationSlug(e.name),
514
+ ...e.useCase ? { useCase: e.useCase } : {},
515
+ ...experimental.length ? { experimental } : {},
516
+ ...e.author ? { author: e.author } : {},
517
+ ...e.startedAt ? { startedAt: e.startedAt } : {},
518
+ ...e.source ? { source: e.source } : {},
519
+ ...e.agent ? { agent: e.agent } : {},
520
+ ...e.doneAt ? { doneAt: e.doneAt } : {},
521
+ ...e.doneSha ? { doneSha: e.doneSha } : {}
522
+ };
523
+ return `<!-- ${MARK$2} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
524
+ }
525
+ function parseExplorationBody(body) {
526
+ if (!body) return void 0;
527
+ const m = new RegExp(`<!-- ${MARK$2} (\\{[\\s\\S]*?\\}) -->`).exec(body);
528
+ if (!m) return void 0;
529
+ try {
530
+ const e = JSON.parse(m[1]);
531
+ if (!e || typeof e.name !== "string") return void 0;
532
+ return typeof e.purpose === "string" ? e : e.purpose === void 0 ? { ...e, purpose: "" } : void 0;
533
+ } catch {
534
+ return void 0;
535
+ }
536
+ }
537
+ const labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.name ?? "");
538
+ function isExplorationPull(p) {
539
+ return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
540
+ }
541
+ function isUnlabeledExploration(p) {
542
+ if (p.state !== "open" || p.draft !== true) return false;
543
+ const names2 = labelNames(p.labels ?? []);
544
+ if (names2.includes(EXPLORATION_LABEL) || names2.includes(EXPLORATION_ARCHIVED_LABEL) || names2.includes(EXPLORATION_READY_LABEL)) return false;
545
+ const block = parseExplorationBody(p.body);
546
+ if (block?.doneAt) return false;
547
+ return isExplorationBranch(p.headRef) || Boolean(block);
548
+ }
549
+ function isHandedOff(p) {
550
+ return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
551
+ }
552
+ function explorationStatus(p) {
553
+ const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
554
+ if (p.state !== "open" && !merged) return "closed";
555
+ if (p.state === "open" && (labelNames(p.labels).includes(EXPLORATION_LABEL) || isUnlabeledExploration(p))) return "active";
556
+ if (!isHandedOff(p)) return "promoted";
557
+ if (merged) return "shipped";
558
+ if (p.draft === false) return "building";
559
+ const doneSha = parseExplorationBody(p.body)?.doneSha;
560
+ if (doneSha && p.headSha && p.headSha !== doneSha) return "building";
561
+ return "ready";
562
+ }
563
+ function isGuardedExploration(p) {
564
+ if (p.state !== void 0 && p.state !== "open") return false;
565
+ if (labelNames(p.labels ?? []).includes(EXPLORATION_LABEL)) return true;
566
+ return isUnlabeledExploration({ state: "open", draft: p.draft, labels: p.labels, headRef: p.headRef ?? p.head?.ref, body: p.body });
567
+ }
568
+ const isExplorationCheck = (name) => (name ?? "").trim() === EXPLORATION_CHECK;
569
+ const isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
570
+ const isArchivedExploration = (labels) => labelNames(labels ?? []).includes(EXPLORATION_ARCHIVED_LABEL);
571
+ function isHeldExploration(p) {
572
+ if (p.state !== void 0 && p.state !== "open") return false;
573
+ const names2 = labelNames(p.labels ?? []);
574
+ if (names2.includes(EXPLORATION_LABEL) || names2.includes(EXPLORATION_ARCHIVED_LABEL)) return true;
575
+ if (isUnlabeledExploration({ state: "open", draft: p.draft, labels: p.labels, headRef: p.headRef ?? p.head?.ref, body: p.body })) return true;
576
+ return names2.includes(EXPLORATION_READY_LABEL) && p.draft !== false;
577
+ }
578
+ const explorationTakeCommands = (branch) => ["git fetch origin", `git checkout ${branch}`];
579
+ const CONTINUE = "Continue on this branch: its pull request already exists, so Bridge follows your pushes, and the merge reads as shipped.";
580
+ const EXPLORATION_STARTING_POINT = `A starting point, not ready to ship: nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
581
+ function explorationStartingPoint(checks) {
582
+ switch (checks) {
583
+ case "success":
584
+ return `A starting point: its checks pass, but nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
585
+ case "failure":
586
+ return `A starting point, not ready to ship: some checks fail on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
587
+ case "pending":
588
+ return `A starting point, not ready to ship: its checks are still running and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
589
+ case "none":
590
+ return `A starting point, not ready to ship: no checks ran on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
591
+ default:
592
+ return EXPLORATION_STARTING_POINT;
593
+ }
594
+ }
595
+ function doneExploringPlan(input) {
596
+ const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
597
+ return {
598
+ removeLabels: [EXPLORATION_LABEL],
599
+ addLabels: [EXPLORATION_READY_LABEL],
600
+ removeFiles,
601
+ body: withDoneExploring(input.body, { branch: input.branch, doneAt: (input.date ?? /* @__PURE__ */ new Date()).toISOString() }),
602
+ take: explorationTakeCommands(input.branch),
603
+ markReady: false
604
+ };
605
+ }
606
+ const DEV_SECTION = "### For developers";
607
+ function withDoneExploring(body, done) {
608
+ const e = parseExplorationBody(body);
609
+ const [fetch2, checkout] = explorationTakeCommands(done.branch);
610
+ const section = [
611
+ DEV_SECTION,
612
+ "",
613
+ `**Done exploring on ${done.doneAt.slice(0, 10)}.** ${explorationStartingPoint(done.checks)}`,
614
+ "",
615
+ "```sh",
616
+ fetch2,
617
+ checkout,
618
+ "```",
619
+ "",
620
+ "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."
621
+ ].join("\n");
622
+ const block = new RegExp(`<!-- ${MARK$2} \\{[\\s\\S]*?\\} -->`);
623
+ const kept = (body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/### For developers[\s\S]*?(?=### |<!-- |$)/, "");
624
+ if (!e || !block.test(kept)) return `${kept.trim()}${kept.trim() ? "\n\n" : ""}${section}`;
625
+ const at = kept.search(block);
626
+ const nextBlock = explorationBlock({ ...e, doneAt: done.doneAt, ...done.doneSha ? { doneSha: done.doneSha } : {} });
627
+ const after = kept.slice(at).replace(block, () => nextBlock).trim();
628
+ return `${kept.slice(0, at).trim()}
629
+
630
+ ${section}
631
+
632
+ ${after}`.trim();
633
+ }
634
+ function promotionPlan(input) {
635
+ const exploration = parseExplorationBody(input.body);
636
+ const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
637
+ const title = jira ? input.title.slice(jira.length).trim() : input.title;
638
+ const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
639
+ const files = input.files.filter((f) => !f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`));
640
+ const risk = classifyRisk({ files, ...input.config ? { config: input.config } : {} });
641
+ const tokens = files.some((f) => f.path.endsWith(".tokens.json"));
642
+ const addLabels = [`tier: ${risk.tier}`, ...risk.tier === 3 && tokens ? ["tokens"] : []];
643
+ const date = (input.date ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
644
+ const existing = new Set(input.body ? [...input.body.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi)].map((m) => Number(m[1])) : []);
645
+ const closes = [...new Set(input.requests ?? [])].filter((n) => !existing.has(n));
646
+ const kept = (input.body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/^## Exploration: /m, "## ").trim();
647
+ const body = [
648
+ `**Promoted from an exploration on ${date}.** Review it like any change: the tier below comes from the files it touches.`,
649
+ "",
650
+ kept || `## ${title.replace(/^Exploration:\s*/i, "")}`,
651
+ "",
652
+ `**Risk tier:** ${risk.tier}.`,
653
+ ...risk.reasons.slice(0, 8).map((r) => `- ${r}`),
654
+ ...closes.length ? ["", ...closes.map((n) => `Closes #${n}`)] : []
655
+ ].join("\n");
656
+ const t = title.replace(/^Exploration:\s*/i, "").trim() || exploration?.name || title;
657
+ return {
658
+ tier: risk.tier,
659
+ reasons: risk.reasons,
660
+ removeLabels: [EXPLORATION_LABEL],
661
+ addLabels,
662
+ removeFiles,
663
+ body,
664
+ title: `${jira ? `${jira} ` : ""}${t.charAt(0).toUpperCase()}${t.slice(1)}`,
665
+ markReady: true
666
+ };
667
+ }
668
+ function explorationTryIt(input) {
669
+ const c = readPreviewComment(input.comment);
670
+ if (!c) return { state: "building" };
671
+ if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
672
+ if (!c.command || c.source === "none") return { state: "failed" };
673
+ return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
674
+ }
675
+ function explorationShareMessage(input) {
676
+ const name = input.name.trim().replace(/"/g, "'");
677
+ const head = input.command ? `Exploration "${name}" (${input.branch}): try it in a staging branch of your product with: ${input.command}.` : `Exploration "${name}" (${input.branch}): its test build is being prepared, the install command for a staging branch of your product will be on the pull request in a few minutes.`;
678
+ const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
679
+ const local = !previews.length && input.local ? `Try it locally: ${input.local}, then start Storybook or the app with its dev command.` : "";
680
+ return [head, previews.length ? `Previews: ${previews.join(", ")}.` : local, `Pull request: ${input.url}.`].filter(Boolean).join(" ");
681
+ }
682
+ function explorationRules(branch) {
683
+ return [
684
+ `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
685
+ `- 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.`,
686
+ `- 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.`
687
+ ];
688
+ }
689
+ class GitHubError extends Error {
690
+ constructor(status, message, detail, info = {}) {
691
+ super(message);
692
+ this.status = status;
693
+ this.detail = detail;
694
+ this.name = "GitHubError";
695
+ this.auth = info.auth;
696
+ this.permission = info.permission;
697
+ this.resetsAt = info.resetsAt;
698
+ }
699
+ status;
700
+ detail;
701
+ /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
702
+ auth;
703
+ /** With auth "missing-permission": the permission the call needed, when known. */
704
+ permission;
705
+ /** For a rate limit (status 429): when the limit resets, when known. */
706
+ resetsAt;
707
+ }
708
+ const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
709
+ const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
710
+ const PERMISSION_LABELS = {
711
+ issues: "Issues",
712
+ pull_requests: "Pull requests",
713
+ contents: "Contents",
714
+ checks: "Checks",
715
+ statuses: "Commit statuses",
716
+ metadata: "Metadata",
717
+ actions: "Actions",
718
+ deployments: "Deployments",
719
+ workflows: "Workflows",
720
+ administration: "Administration"
721
+ };
722
+ function permissionLabel(permission) {
723
+ return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
724
+ }
725
+ const TOKEN_SETTINGS_URL = "https://github.com/settings/personal-access-tokens";
726
+ const TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
727
+ function missingPermissionMessage(permission, action) {
728
+ const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
729
+ if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
730
+ return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
731
+ }
732
+ function requiredPermission(path, acceptedHeader) {
733
+ if (acceptedHeader) {
734
+ const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
735
+ if (first.length) return first[0];
736
+ }
737
+ const p = path.replace(/[?#].*$/, "");
738
+ if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
739
+ if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
740
+ if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
741
+ if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
742
+ if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
743
+ if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
744
+ if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
745
+ if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
746
+ return void 0;
747
+ }
748
+ const PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
749
+ function classifyAuth(status, detail, opts = {}) {
750
+ if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
751
+ if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
752
+ if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
753
+ return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
754
+ }
755
+ return {};
756
+ }
757
+ function memoryEtagStore(max = 500) {
758
+ const map = /* @__PURE__ */ new Map();
759
+ return {
760
+ get(key) {
761
+ const e = map.get(key);
762
+ if (e) {
763
+ map.delete(key);
764
+ map.set(key, e);
765
+ }
766
+ return e;
767
+ },
768
+ set(key, entry) {
769
+ map.delete(key);
770
+ map.set(key, entry);
771
+ while (map.size > max) map.delete(map.keys().next().value);
772
+ },
773
+ clear: () => map.clear()
774
+ };
775
+ }
776
+ function webStorageEtagStore(storage, opts = {}) {
777
+ const prefix = opts.prefix ?? "bridge.gh.etag:";
778
+ const maxBytes = opts.maxEntryBytes ?? 512e3;
779
+ return {
780
+ get(key) {
781
+ try {
782
+ const raw = storage.getItem(prefix + key);
783
+ if (!raw) return void 0;
784
+ const e = JSON.parse(raw);
785
+ return typeof e?.etag === "string" ? e : void 0;
786
+ } catch {
787
+ return void 0;
788
+ }
789
+ },
790
+ set(key, entry) {
791
+ try {
792
+ const raw = JSON.stringify(entry);
793
+ if (raw.length > maxBytes) return;
794
+ storage.setItem(prefix + key, raw);
795
+ } catch {
796
+ }
797
+ },
798
+ clear() {
799
+ try {
800
+ const keys = [];
801
+ for (let i = 0; i < storage.length; i++) {
802
+ const k = storage.key(i);
803
+ if (k?.startsWith(prefix)) keys.push(k);
804
+ }
805
+ for (const k of keys) storage.removeItem(k);
806
+ } catch {
807
+ }
808
+ }
809
+ };
810
+ }
811
+ const sharedEtags = memoryEtagStore();
812
+ function clearSharedEtags() {
813
+ sharedEtags.clear();
814
+ }
815
+ function tokenFingerprint(token) {
816
+ if (!token) return "anon";
817
+ let a = 2166136261;
818
+ let b = 16777619 ^ token.length;
819
+ for (let i = 0; i < token.length; i++) {
820
+ const c = token.charCodeAt(i);
821
+ a = Math.imul(a ^ c, 16777619) >>> 0;
822
+ b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
823
+ }
824
+ return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
825
+ }
826
+ function installationFor(list, owner) {
827
+ const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
828
+ if (!inst) return { reach: "no" };
829
+ const selection = inst.repositorySelection ?? inst.repository_selection;
830
+ return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
831
+ }
832
+ const MAX_PAGES = 10;
833
+ function rateLimitMessage(x) {
834
+ const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
835
+ if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
836
+ const hint = x.tokenHint ?? "add a GitHub token";
837
+ return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
838
+ }
839
+ const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
840
+ const hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
841
+ const isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
842
+ const API = "https://api.github.com";
843
+ const ACCEPT_JSON = "application/vnd.github+json";
844
+ const ACCEPT_RAW = "application/vnd.github.raw";
845
+ const RAW = "https://raw.githubusercontent.com";
846
+ function parseRepo(repo) {
847
+ const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
848
+ if (!m) throw new Error(`"${repo}" is not a repository. Use the form owner/name.`);
849
+ return { owner: m[1], name: m[2] };
850
+ }
851
+ function utf8ToBase64(text) {
852
+ const bytes = new TextEncoder().encode(text);
853
+ let bin = "";
854
+ for (const b of bytes) bin += String.fromCharCode(b);
855
+ return btoa(bin);
856
+ }
857
+ function base64ToUtf8(b64) {
858
+ const bin = atob(b64.replace(/\s/g, ""));
859
+ const bytes = new Uint8Array(bin.length);
860
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
861
+ return new TextDecoder("utf-8").decode(bytes);
862
+ }
863
+ const copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
864
+ const encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
865
+ class GitHubClient {
866
+ constructor(repo, token, fetchImpl = (url, init) => fetch(url, init), retry = DEFAULT_RETRY, options = {}) {
867
+ this.token = token;
868
+ this.fetchImpl = fetchImpl;
869
+ this.retry = retry;
870
+ this.options = options;
871
+ const r = parseRepo(repo);
872
+ this.owner = r.owner;
873
+ this.name = r.name;
874
+ this.api = (options.apiUrl ?? API).replace(/\/$/, "");
875
+ this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
551
876
  this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
552
877
  this.etagScope = options.etagScope ?? tokenFingerprint(token);
553
878
  }
@@ -1209,8 +1534,11 @@ class GitHubClient {
1209
1534
  const r = await this.request("GET", `${this.repoPath}/pulls?state=${state}&per_page=50&sort=updated&direction=desc`);
1210
1535
  return r.map(toSummary);
1211
1536
  }
1212
- /** Commit statuses and check runs of a commit, folded into one state. */
1213
- async getChecks(sha) {
1537
+ /**
1538
+ * Commit statuses and check runs of a commit, folded into one state. `exploration`: the commit heads an exploration
1539
+ * (explorations isGuardedExploration), so its guard check, failing by design, is left out (as review-gate summarizeCi).
1540
+ */
1541
+ async getChecks(sha, opts = {}) {
1214
1542
  const [combined, runs] = await Promise.all([
1215
1543
  this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/status`).catch(() => ({ statuses: [] })),
1216
1544
  this.listCheckRuns(sha).catch(() => [])
@@ -1218,7 +1546,7 @@ class GitHubClient {
1218
1546
  const checks = [
1219
1547
  ...runs.map((c) => ({ name: c.name, status: c.status, conclusion: c.conclusion })),
1220
1548
  ...combined.statuses.map((s) => ({ name: s.context, status: s.state === "pending" ? "in_progress" : "completed", conclusion: s.state === "pending" ? null : s.state }))
1221
- ];
1549
+ ].filter((c) => !(opts.exploration && isExplorationCheck(c.name)));
1222
1550
  const failing = checks.some((c) => c.conclusion && !["success", "neutral", "skipped"].includes(c.conclusion));
1223
1551
  const pending = checks.some((c) => c.status !== "completed");
1224
1552
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
@@ -1302,1498 +1630,1188 @@ class GitHubClient {
1302
1630
  */
1303
1631
  async getBranchProtection(branch) {
1304
1632
  try {
1305
- return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
1306
- } catch (err) {
1307
- if (!(err instanceof GitHubError)) throw err;
1308
- if (err.status === 404) return null;
1309
- if (err.status === 403 || err.status === 401) {
1310
- const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
1311
- return b?.protected === false ? null : void 0;
1312
- }
1313
- throw err;
1314
- }
1315
- }
1316
- /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
1317
- async createCheckRun(input) {
1318
- const r = await this.request("POST", `${this.repoPath}/check-runs`, {
1319
- name: input.name,
1320
- head_sha: input.headSha,
1321
- ...input.status ? { status: input.status } : {},
1322
- ...input.conclusion ? { conclusion: input.conclusion } : {},
1323
- ...input.output ? { output: input.output } : {}
1324
- });
1325
- return { id: r.id, name: r.name, status: r.status, conclusion: r.conclusion, url: r.html_url };
1326
- }
1327
- async removeLabel(issueNumber, label) {
1328
- await this.request("DELETE", `${this.repoPath}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`);
1329
- }
1330
- // --- branch actions shared by Studio, the MCP server and the CLI (branch-actions.ts) ---
1331
- /** The account the token belongs to (GET /user). Needs a token. */
1332
- async getViewer() {
1333
- this.assertToken("Reading who you are on GitHub");
1334
- const r = await this.request("GET", "/user");
1335
- return { login: r.login };
1336
- }
1337
- /** The repository's merge settings and the viewer's permissions (unknown settings count as allowed). */
1338
- async getMergeSettings() {
1339
- return this.request("GET", this.repoPath);
1340
- }
1341
- /**
1342
- * Merges a pull request, only at `sha` (GitHub refuses when the branch moved meanwhile). The repository's branch
1343
- * rules still apply: GitHub refuses a merge they block, whatever Bridge says.
1344
- */
1345
- async mergePullRequest(n, input) {
1346
- this.assertToken("Merging a branch");
1347
- const r = await this.request("PUT", `${this.repoPath}/pulls/${n}/merge`, {
1348
- merge_method: input.method,
1349
- sha: input.sha,
1350
- ...input.commitMessage !== void 0 && input.method !== "rebase" ? { commit_message: input.commitMessage } : {}
1351
- });
1352
- return { sha: r?.sha ?? "", merged: r?.merged ?? true };
1353
- }
1354
- /** GitHub's "Update branch": merges the base into the pull request, only when its head is still `expectedHeadSha`. */
1355
- async updatePullRequestBranch(n, expectedHeadSha) {
1356
- this.assertToken("Updating a branch from main");
1357
- await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
1358
- }
1359
- /**
1360
- * Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
1361
- * `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
1362
- * branches and, with a branch prefix, a `base` without it.
1363
- */
1364
- async mergeIntoBranch(base, head, message) {
1365
- this.assertToken("Merging a branch into another");
1366
- this.assertWritableBranch(base);
1367
- const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
1368
- return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
1369
- }
1370
- /** Posts a comment on an issue or pull request and returns its id and link. */
1371
- async createIssueComment(n, body) {
1372
- this.assertToken("Commenting");
1373
- const r = await this.request("POST", `${this.repoPath}/issues/${n}/comments`, { body });
1374
- return { id: r.id, url: r.html_url };
1375
- }
1376
- /** Replaces the text of a comment. */
1377
- async updateIssueComment(id, body) {
1378
- this.assertToken("Editing a comment");
1379
- await this.request("PATCH", `${this.repoPath}/issues/comments/${id}`, { body });
1380
- }
1381
- /** Submits a review on a given commit (the head the reviewer saw); the body may be empty for an approval. */
1382
- async submitReview(n, input) {
1383
- this.assertToken("Reviewing a branch");
1384
- const r = await this.request("POST", `${this.repoPath}/pulls/${n}/reviews`, {
1385
- event: input.event,
1386
- ...input.body ? { body: input.body } : {},
1387
- ...input.commitId ? { commit_id: input.commitId } : {}
1388
- });
1389
- return { id: r.id, user: r.user?.login, state: r.state, body: r.body ?? "" };
1390
- }
1391
- /** Line comments of a pull request (review threads), oldest first. */
1392
- async listReviewComments(n) {
1393
- const r = await this.request(
1394
- "GET",
1395
- `${this.repoPath}/pulls/${n}/comments?per_page=100`
1396
- );
1397
- return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", path: c.path, line: c.line ?? null, inReplyTo: c.in_reply_to_id ?? null, url: c.html_url, createdAt: c.created_at }));
1398
- }
1399
- /** Asks these people to review a pull request. */
1400
- async requestReviewers(n, logins) {
1401
- this.assertToken("Asking for a review");
1402
- await this.request("POST", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
1403
- }
1404
- /** Takes these people off the requested reviewers. */
1405
- async removeRequestedReviewers(n, logins) {
1406
- this.assertToken("Changing the requested reviewers");
1407
- await this.request("DELETE", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
1408
- }
1409
- /** Replies in the thread of a line comment. */
1410
- async replyToReviewComment(n, commentId, body) {
1411
- this.assertToken("Replying to a comment");
1412
- const r = await this.request("POST", `${this.repoPath}/pulls/${n}/comments/${commentId}/replies`, { body });
1413
- return { id: r.id, url: r.html_url };
1414
- }
1415
- /**
1416
- * What `head` adds on top of `base`, with what library sync needs: the shared start (merge base), the base
1417
- * commit and its tree, how far behind, and the files with their previous names.
1418
- */
1419
- async compareDetailed(base, head) {
1420
- const r = await this.request("GET", `${this.repoPath}/compare/${encodeRef(base)}...${encodeRef(head)}`);
1421
- return {
1422
- behindBy: r.behind_by,
1423
- aheadBy: r.ahead_by,
1424
- mergeBase: r.merge_base_commit?.sha ?? null,
1425
- baseSha: r.base_commit?.sha ?? null,
1426
- baseTree: r.base_commit?.commit?.tree?.sha ?? null,
1427
- files: (r.files ?? []).map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {} }))
1428
- };
1429
- }
1430
- /** A tree on top of `baseTree` from entries with their own file mode (blob sha, inline content, or sha null to delete). */
1431
- async createTreeEntries(baseTree, entries) {
1432
- const r = await this.request("POST", `${this.repoPath}/git/trees`, { base_tree: baseTree, tree: entries.map((e) => ({ ...e, type: "blob" })) }, true);
1433
- return r.sha;
1434
- }
1435
- }
1436
- function toCommitSummary(c) {
1437
- return {
1438
- sha: c.sha,
1439
- parents: (c.parents ?? []).map((p) => p.sha),
1440
- date: c.commit?.committer?.date ?? c.commit?.author?.date ?? "",
1441
- author: c.author?.login ?? c.commit?.author?.name ?? "unknown",
1442
- message: c.commit?.message ?? ""
1443
- };
1444
- }
1445
- function toSummary(r) {
1446
- return {
1447
- number: r.number,
1448
- title: r.title,
1449
- state: r.merged_at || r.merged ? "merged" : r.state,
1450
- draft: Boolean(r.draft),
1451
- author: r.user?.login ?? "unknown",
1452
- headRef: r.head?.ref,
1453
- headSha: r.head?.sha,
1454
- baseRef: r.base?.ref,
1455
- labels: (r.labels ?? []).map((l) => l.name),
1456
- url: r.html_url,
1457
- updatedAt: r.updated_at,
1458
- ...typeof r.body === "string" ? { body: r.body } : {},
1459
- ...r.created_at ? { createdAt: r.created_at } : {},
1460
- ...r.merged_at ? { mergedAt: r.merged_at } : {},
1461
- ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {}
1462
- };
1463
- }
1464
- function friendlyMessage(status, detail, info = {}) {
1465
- if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
1466
- if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
1467
- switch (status) {
1468
- case 401:
1469
- return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
1470
- case 403:
1471
- if (/rate limit/i.test(detail)) return "GitHub is limiting requests right now. Wait a minute, then try again.";
1472
- if (/archived/i.test(detail)) return "This repository is archived and read-only.";
1473
- return 'The token does not have permission for this. It needs "Contents: read and write" and "Pull requests: read and write" on this repository.';
1474
- case 404:
1475
- return "Repository, branch or file not found, or the token cannot see this repository.";
1476
- case 409:
1477
- return "GitHub reported a conflict (the branch moved while saving). Scan again, then retry.";
1478
- case 429:
1479
- return "GitHub is limiting requests right now. Wait a minute, then try again.";
1480
- case 422:
1481
- return `GitHub refused the request${detail ? `: ${detail}` : "."}`;
1482
- default:
1483
- return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
1484
- }
1485
- }
1486
- async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
1487
- const treeSha = await client.getCommitTree(commitSha);
1488
- const { entries, truncated } = await client.getTree(treeSha, true);
1489
- if (truncated) throw new Error("The repository tree is too large to read in one request.");
1490
- const warnings = [];
1491
- let config;
1492
- let configText;
1493
- const configEntry = entries.find((e) => e.type === "blob" && e.path === CONFIG_FILE);
1494
- if (configEntry) {
1495
- try {
1496
- configText = await client.getBlobText(configEntry.sha);
1497
- config = loadConfig(configText);
1633
+ return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
1498
1634
  } catch (err) {
1499
- warnings.push(err instanceof ConfigError ? `${err.message}. Using ${fallbackTokensPath} instead.` : `${CONFIG_FILE} could not be read.`);
1635
+ if (!(err instanceof GitHubError)) throw err;
1636
+ if (err.status === 404) return null;
1637
+ if (err.status === 403 || err.status === 401) {
1638
+ const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
1639
+ return b?.protected === false ? null : void 0;
1640
+ }
1641
+ throw err;
1500
1642
  }
1501
1643
  }
1502
- const tokensPath2 = (config?.tokens.root ?? fallbackTokensPath).replace(/^\/+|\/+$/g, "");
1503
- const checksFile = config?.tokens.contrastChecks ?? DEFAULT_TOKEN_PATHS.contrastChecks;
1504
- const prefix = tokensPath2 + "/";
1505
- const wanted = entries.filter(
1506
- (e) => e.type === "blob" && e.path.startsWith(prefix) && (e.path.endsWith(".tokens.json") || e.path === prefix + checksFile)
1507
- );
1508
- if (!wanted.some((e) => e.path.endsWith(".tokens.json"))) throw new Error(`No *.tokens.json files found under ${tokensPath2} at ${commitSha.slice(0, 7)}.`);
1509
- const files = {};
1510
- let contrastChecks;
1511
- const queue = [...wanted];
1512
- const worker = async () => {
1513
- for (let e = queue.shift(); e; e = queue.shift()) {
1514
- const text = await client.getBlobText(e.sha);
1515
- const rel = e.path.slice(prefix.length);
1516
- if (rel === checksFile) contrastChecks = text;
1517
- else files[rel] = text;
1518
- }
1519
- };
1520
- await Promise.all([worker(), worker(), worker(), worker()]);
1521
- return { commitSha, treeSha, files, contrastChecks, config, ...config ? { configText } : {}, tokensPath: tokensPath2, warnings };
1522
- }
1523
- function withRulesetUrl(r) {
1524
- const href = r._links?.html?.href;
1525
- return href && !r.html_url ? { ...r, html_url: href } : r;
1526
- }
1527
- function toIssue(r) {
1528
- return {
1529
- number: r.number,
1530
- title: r.title,
1531
- body: r.body ?? "",
1532
- url: r.html_url,
1533
- state: r.state,
1534
- stateReason: r.state_reason ?? null,
1535
- labels: r.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
1536
- author: r.user?.login ?? "",
1537
- createdAt: r.created_at,
1538
- updatedAt: r.updated_at,
1539
- ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {},
1540
- ...r.closed_at ? { closedAt: r.closed_at } : {}
1541
- };
1542
- }
1543
- const FORMER_MEMBER = "A former member";
1544
- const requesterFirstName = (name) => name.trim() === FORMER_MEMBER ? FORMER_MEMBER : name.trim().split(/\s+/)[0] ?? name;
1545
- const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
1546
- const LINE = /(?:^|\n)[ \t>*_-]*(?:<sub>)?\s*Requested (?:from [^\n<]{1,40}? )?by ([^\n<]{1,120}?)(?:\s+with Bridge)?\.?\s*(?:<\/sub>)?[ \t]*(?=\n|$)/i;
1547
- const SUGGESTED = new RegExp(LINE.source.replace("Requested (?:from", "Suggested (?:from"), "i");
1548
- function requesterOf(body) {
1549
- const m = LINE.exec(body ?? "") ?? SUGGESTED.exec(body ?? "");
1550
- if (!m) return null;
1551
- const who = m[1].trim().replace(/\.$/, "").replace(/\s+through Bridge$/i, "").replace(/\s*\([^()\s@]+@[^()\s]+\)$/, "");
1552
- const both = /^(.+?)\s*\(@([^)\s]+)\)$/.exec(who);
1553
- if (both) return LOGIN.test(both[2]) ? { name: both[1].trim(), login: both[2] } : null;
1554
- if (who.startsWith("@")) return LOGIN.test(who.slice(1)) ? { name: null, login: who.slice(1) } : null;
1555
- return /^[\p{L}][\p{L}'’. -]{0,60}$/u.test(who) && who.split(/\s+/).length <= 5 ? { name: who, login: null } : null;
1556
- }
1557
- function requesterLine(r) {
1558
- const name = r.name?.trim();
1559
- const login = r.login?.replace(/^@/, "").trim();
1560
- if (name && login) return `Requested by ${name} (@${login}).`;
1561
- if (login) return `Requested by @${login}.`;
1562
- if (name) return `Requested by ${name}.`;
1563
- throw new Error("A requester needs a name or a login.");
1564
- }
1565
- function requesterName(body) {
1566
- const r = requesterOf(body);
1567
- return r ? r.name ?? r.login : null;
1568
- }
1569
- const BULK_LIMIT = 60;
1570
- const UNRELEASED = "Unreleased";
1571
- const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
1572
- function prNumberFromSubject(subject) {
1573
- for (const re of SUBJECT_PR) {
1574
- const m = re.exec(subject);
1575
- if (m) return Number(m[1]);
1644
+ /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
1645
+ async createCheckRun(input) {
1646
+ const r = await this.request("POST", `${this.repoPath}/check-runs`, {
1647
+ name: input.name,
1648
+ head_sha: input.headSha,
1649
+ ...input.status ? { status: input.status } : {},
1650
+ ...input.conclusion ? { conclusion: input.conclusion } : {},
1651
+ ...input.output ? { output: input.output } : {}
1652
+ });
1653
+ return { id: r.id, name: r.name, status: r.status, conclusion: r.conclusion, url: r.html_url };
1576
1654
  }
1577
- return void 0;
1578
- }
1579
- function parseChangesetText(path, text) {
1580
- const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
1581
- if (!m) return null;
1582
- const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
1583
- const order = ["major", "minor", "patch"];
1584
- const bump = order.find((b) => bumps.some((x) => x.bump === b));
1585
- const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
1586
- return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
1587
- }
1588
- function firstParagraph(text) {
1589
- if (!text) return "";
1590
- const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
1591
- for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
1592
- const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
1593
- const s = lines.join(" ").replace(/\s+/g, " ").trim();
1594
- if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
1655
+ async removeLabel(issueNumber, label) {
1656
+ await this.request("DELETE", `${this.repoPath}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`);
1595
1657
  }
1596
- return "";
1597
- }
1598
- function countChanges(changes) {
1599
- const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
1600
- for (const x of changes) c[x.kind]++;
1601
- return c;
1602
- }
1603
- const shortSha = (sha) => sha.slice(0, 7);
1604
- function titleOf(commit) {
1605
- if (commit.pr?.title) return commit.pr.title;
1606
- if (/^Merge pull request #\d+/.test(commit.subject)) {
1607
- const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
1608
- if (first) return first.trim();
1658
+ // --- branch actions shared by Studio, the MCP server and the CLI (branch-actions.ts) ---
1659
+ /** The account the token belongs to (GET /user). Needs a token. */
1660
+ async getViewer() {
1661
+ this.assertToken("Reading who you are on GitHub");
1662
+ const r = await this.request("GET", "/user");
1663
+ return { login: r.login };
1609
1664
  }
1610
- return commit.subject;
1611
- }
1612
- function buildHistoryEntry(input) {
1613
- const { config, commit } = input;
1614
- const beforeSet = new LayeredTokenSet(config, input.before);
1615
- const afterSet = new LayeredTokenSet(config, input.after);
1616
- const changes = diffLayered(beforeSet, afterSet);
1617
- if (!changes.length) return null;
1618
- const tier = classifyLayered(changes, [], config);
1619
- const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
1620
- const pkg = config.changesets?.package;
1621
- const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
1622
- const title = titleOf(commit);
1623
- const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
1624
- const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
1625
- const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
1626
- const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
1627
- const bump = changesetBump2 ?? tier.bump;
1628
- return {
1629
- id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
1630
- sha: commit.sha,
1631
- ...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
1632
- date: commit.date,
1633
- // Who asked, as Bridge recorded it in the pull request ("Requested by Priya Shah ... through Bridge."), before the
1634
- // account that opened it (the Bridge app for an email member, an agent): History names the person.
1635
- author: requesterName(commit.pr?.body) ?? commit.pr?.author ?? commit.author,
1636
- title,
1637
- why,
1638
- ...pr ? { pr } : {},
1639
- changes,
1640
- counts: countChanges(changes),
1641
- tier: tier.tier,
1642
- bump,
1643
- breaking: bump === "major",
1644
- reasons: tier.reasons,
1645
- changesets: relevant,
1646
- ...changes.length > BULK_LIMIT ? { bulk: true } : {},
1647
- ...input.demo ? { demo: true } : {}
1648
- };
1649
- }
1650
- async function collectHistory(config, backend, opts) {
1651
- const root = config.tokens.root.replace(/\/$/, "");
1652
- const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
1653
- const seen = /* @__PURE__ */ new Set();
1654
- const picked = [];
1655
- for (const c of commits) {
1656
- const n = c.pr?.number;
1657
- if (n !== void 0) {
1658
- if (seen.has(n)) continue;
1659
- seen.add(n);
1660
- }
1661
- picked.push(c);
1665
+ /** The repository's merge settings and the viewer's permissions (unknown settings count as allowed). */
1666
+ async getMergeSettings() {
1667
+ return this.request("GET", this.repoPath);
1662
1668
  }
1663
- const entries = [];
1664
- let done = 0;
1665
- for (const c of picked) {
1666
- const parent = c.parents[0];
1667
- const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
1668
- let changesets = [];
1669
- if (backend.changesetPaths && backend.readFile && config.changesets) {
1670
- const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
1671
- const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
1672
- changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
1673
- }
1674
- const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
1675
- if (e) entries.push(e);
1676
- opts.onProgress?.(++done, picked.length);
1669
+ /**
1670
+ * Merges a pull request, only at `sha` (GitHub refuses when the branch moved meanwhile). The repository's branch
1671
+ * rules still apply: GitHub refuses a merge they block, whatever Bridge says.
1672
+ */
1673
+ async mergePullRequest(n, input) {
1674
+ this.assertToken("Merging a branch");
1675
+ const r = await this.request("PUT", `${this.repoPath}/pulls/${n}/merge`, {
1676
+ merge_method: input.method,
1677
+ sha: input.sha,
1678
+ ...input.commitMessage !== void 0 && input.method !== "rebase" ? { commit_message: input.commitMessage } : {}
1679
+ });
1680
+ return { sha: r?.sha ?? "", merged: r?.merged ?? true };
1677
1681
  }
1678
- return entries;
1679
- }
1680
- function parseChangelog(text) {
1681
- const out = [];
1682
- const parts = text.split(/^##\s+/m).slice(1);
1683
- for (const p of parts) {
1684
- const [head, ...rest] = p.split(/\r?\n/);
1685
- const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
1686
- if (!version) continue;
1687
- const body = rest.join("\n");
1688
- const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
1689
- const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
1690
- const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
1691
- out.push({ version, ...date ? { date } : {}, shas, prs });
1682
+ /** GitHub's "Update branch": merges the base into the pull request, only when its head is still `expectedHeadSha`. */
1683
+ async updatePullRequestBranch(n, expectedHeadSha) {
1684
+ this.assertToken("Updating a branch from main");
1685
+ await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
1692
1686
  }
1693
- return out;
1694
- }
1695
- function releasesFromTags(tags, pkg) {
1696
- const out = [];
1697
- for (const t of tags) {
1698
- const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
1699
- if (!m) continue;
1700
- if (pkg && m[1] && m[1] !== pkg) continue;
1701
- out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
1687
+ /**
1688
+ * Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
1689
+ * `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
1690
+ * branches and, with a branch prefix, a `base` without it.
1691
+ */
1692
+ async mergeIntoBranch(base, head, message) {
1693
+ this.assertToken("Merging a branch into another");
1694
+ this.assertWritableBranch(base);
1695
+ const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
1696
+ return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
1697
+ }
1698
+ /** Posts a comment on an issue or pull request and returns its id and link. */
1699
+ async createIssueComment(n, body) {
1700
+ this.assertToken("Commenting");
1701
+ const r = await this.request("POST", `${this.repoPath}/issues/${n}/comments`, { body });
1702
+ return { id: r.id, url: r.html_url };
1702
1703
  }
1703
- return out;
1704
- }
1705
- function assignVersions(entries, releases) {
1706
- const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
1707
- return entries.map((e) => {
1708
- const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
1709
- const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
1710
- return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
1711
- });
1712
- }
1713
- const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
1714
- function versionTimeline(entries, releases = []) {
1715
- const groups = /* @__PURE__ */ new Map();
1716
- for (const e of entries) {
1717
- const v = e.version ?? UNRELEASED;
1718
- let g = groups.get(v);
1719
- if (!g) {
1720
- const rel = releases.find((r) => r.version === v);
1721
- g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
1722
- groups.set(v, g);
1723
- }
1724
- g.entries.push(e.id);
1725
- if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
1726
- g.tokenChanges += e.counts.total;
1727
- g.breaking ||= e.breaking;
1728
- if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
1729
- if (!g.authors.includes(e.author)) g.authors.push(e.author);
1730
- if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
1704
+ /** Replaces the text of a comment. */
1705
+ async updateIssueComment(id, body) {
1706
+ this.assertToken("Editing a comment");
1707
+ await this.request("PATCH", `${this.repoPath}/issues/comments/${id}`, { body });
1731
1708
  }
1732
- return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
1733
- }
1734
- const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
1735
- function filterHistory(entries, f = {}) {
1736
- const q = f.query?.trim().toLowerCase();
1737
- return entries.filter(
1738
- (e) => (!f.token || e.changes.some((c) => matchesToken(c.path, f.token) || c.from && matchesToken(c.from, f.token))) && (!f.author || e.author.toLowerCase() === f.author.toLowerCase()) && (!f.tier || e.tier === f.tier) && (!f.version || (e.version ?? UNRELEASED) === f.version) && (f.breaking === void 0 || e.breaking === f.breaking) && (!f.since || Date.parse(e.date) >= Date.parse(f.since)) && (!f.until || Date.parse(e.date) <= Date.parse(f.until) + 86399999) && (!q || `${e.title} ${e.why} ${e.pr ? `#${e.pr.number}` : ""} ${e.sha}`.toLowerCase().includes(q))
1739
- );
1740
- }
1741
- function fileModes(config, file) {
1742
- for (const layer of config.tokens.layers) {
1743
- const keys = [];
1744
- const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
1745
- keys.push(k);
1746
- return "";
1709
+ /** Submits a review on a given commit (the head the reviewer saw); the body may be empty for an approval. */
1710
+ async submitReview(n, input) {
1711
+ this.assertToken("Reviewing a branch");
1712
+ const r = await this.request("POST", `${this.repoPath}/pulls/${n}/reviews`, {
1713
+ event: input.event,
1714
+ ...input.body ? { body: input.body } : {},
1715
+ ...input.commitId ? { commit_id: input.commitId } : {}
1747
1716
  });
1748
- const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
1749
- const m = new RegExp(source).exec(file);
1750
- if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
1717
+ return { id: r.id, user: r.user?.login, state: r.state, body: r.body ?? "" };
1751
1718
  }
1752
- return {};
1753
- }
1754
- function modeLabel(modes) {
1755
- const v = Object.values(modes).filter(Boolean);
1756
- if (!v.length) return "All modes";
1757
- const s = v.join(" ");
1758
- return s.charAt(0).toUpperCase() + s.slice(1);
1759
- }
1760
- function tokenHistory(entries, path, config) {
1761
- const names2 = /* @__PURE__ */ new Set([path]);
1762
- const out = [];
1763
- const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
1764
- for (const e of sorted) {
1765
- const hits = e.changes.filter((c) => names2.has(c.path));
1766
- for (const c of hits) {
1767
- const modes = config ? fileModes(config, c.file) : {};
1768
- out.push({
1769
- entryId: e.id,
1770
- sha: e.sha,
1771
- date: e.date,
1772
- author: e.author,
1773
- title: e.title,
1774
- why: e.why,
1775
- ...e.pr ? { pr: e.pr } : {},
1776
- ...e.version ? { version: e.version } : {},
1777
- ...e.demo ? { demo: true } : {},
1778
- kind: c.kind,
1779
- path: c.path,
1780
- ...c.from ? { from: c.from } : {},
1781
- file: c.file,
1782
- modes,
1783
- modeLabel: modeLabel(modes),
1784
- fields: c.fields,
1785
- ...c.before !== void 0 ? { before: c.before } : {},
1786
- ...c.after !== void 0 ? { after: c.after } : {},
1787
- ...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
1788
- ...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
1789
- ...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
1790
- ...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
1791
- });
1792
- }
1793
- for (const c of hits) if (c.kind === "renamed" && c.from) names2.add(c.from);
1719
+ /** Line comments of a pull request (review threads), oldest first. */
1720
+ async listReviewComments(n) {
1721
+ const r = await this.request(
1722
+ "GET",
1723
+ `${this.repoPath}/pulls/${n}/comments?per_page=100`
1724
+ );
1725
+ return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", path: c.path, line: c.line ?? null, inReplyTo: c.in_reply_to_id ?? null, url: c.html_url, createdAt: c.created_at }));
1794
1726
  }
1795
- return out;
1796
- }
1797
- const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1798
- const keyOf = (path, file) => `${path}@${file}`;
1799
- const show$2 = (v) => typeof v === "string" ? v : JSON.stringify(v);
1800
- function defIn(set, path, file) {
1801
- return set.defs.find((d) => d.path === path && d.file === file);
1802
- }
1803
- function sharesCombo(set, a, b) {
1804
- return a === b || set.combos().some((c) => {
1805
- const files = set.layerFiles(c).files;
1806
- return files.includes(a) && files.includes(b);
1807
- });
1808
- }
1809
- function laterTouching(later, path, file, from) {
1810
- return later.filter((e) => e.changes.some((c) => c.file === file && (c.path === path || c.from === path || from !== void 0 && (c.path === from || c.from === from)))).sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).map((e) => ({ id: e.id, title: e.title, date: e.date, author: e.author, ...e.pr ? { pr: e.pr.number } : {} }));
1811
- }
1812
- function planRevert(input) {
1813
- const { current, entry } = input;
1814
- const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
1815
- const res = input.resolutions ?? {};
1816
- const planned = [];
1817
- const conflicts = [];
1818
- const skipped = [];
1819
- const config = current.config;
1820
- const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
1821
- for (const c of entry.changes) {
1822
- const key = keyOf(c.path, c.file);
1823
- const label = modeLabel(fileModes(config, c.file));
1824
- const original = { ...c.before !== void 0 ? { before: c.before } : {}, ...c.after !== void 0 ? { after: c.after } : {}, ...c.beforeCss ? { beforeCss: c.beforeCss } : {}, ...c.afterCss ? { afterCss: c.afterCss } : {} };
1825
- const conflict = (reason, message, cur, editIfForced, summary) => {
1826
- const resolution = res[key] ?? "keep-current";
1827
- conflicts.push({ key, path: c.path, file: c.file, kind: c.kind, reason, message, original, ...cur ? { current: cur } : {}, later: laterTouching(later, c.path, c.file, c.from), resolution });
1828
- if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
1829
- };
1830
- const now = defIn(current, c.path, c.file);
1831
- switch (c.kind) {
1832
- case "changed": {
1833
- if (!now) {
1834
- conflict("removed-later", `${c.path} was removed from ${c.file} after this change. Restoring it is a new token decision, not a revert.`, void 0, { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} }, `Add ${c.path} back with ${show$2(c.before)} (${label})`);
1835
- break;
1836
- }
1837
- const valueField = c.fields.includes("value");
1838
- const descField = c.fields.includes("description");
1839
- const cur = { value: now.value, css: css(c.path, c.file) };
1840
- const valueAt = (v) => same$1(now.value, v);
1841
- const descNow = now.description ?? "";
1842
- if (!valueField && !descField) {
1843
- skipped.push({ key, path: c.path, file: c.file, reason: "Only the type or extensions changed: edit those by hand." });
1844
- break;
1845
- }
1846
- if ((!valueField || valueAt(c.before)) && (!descField || descNow === (c.descriptionBefore ?? ""))) {
1847
- skipped.push({ key, path: c.path, file: c.file, reason: "Already back to the value from before this change." });
1848
- break;
1849
- }
1850
- const valueMoved = valueField && !valueAt(c.after) && !valueAt(c.before);
1851
- const descMoved = descField && descNow !== (c.descriptionAfter ?? "") && descNow !== (c.descriptionBefore ?? "");
1852
- const edit = {
1853
- op: "set",
1854
- path: c.path,
1855
- file: c.file,
1856
- ...valueField ? { value: c.before } : {},
1857
- ...descField ? { description: c.descriptionBefore ?? "" } : {}
1858
- };
1859
- const summary = valueField ? `Set ${c.path} back to ${show$2(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
1860
- if (valueMoved || descMoved) {
1861
- conflict("changed-later", `${c.path} changed again after this change: it is now ${cur.css || show$2(cur.value)}, not ${c.afterCss || show$2(c.after)}.`, cur, edit, summary);
1862
- break;
1863
- }
1864
- planned.push({ key, path: c.path, file: c.file, summary, edit });
1865
- break;
1866
- }
1867
- case "added": {
1868
- if (!now) {
1869
- skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
1870
- break;
1871
- }
1872
- const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
1873
- const edit = { op: "remove", path: c.path, file: c.file };
1874
- const summary = `Remove ${c.path} (${label})`;
1875
- if (!same$1(now.value, c.after)) {
1876
- conflict("changed-later", `${c.path} changed after it was added: it is now ${css(c.path, c.file) || show$2(now.value)}.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
1877
- break;
1878
- }
1879
- if (users.length) {
1880
- conflict("still-referenced", `${users.length} token(s) alias ${c.path} now (${users.slice(0, 4).map((u) => u.path).join(", ")}${users.length > 4 ? ", ..." : ""}). Removing it would break them.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
1881
- break;
1882
- }
1883
- planned.push({ key, path: c.path, file: c.file, summary, edit });
1884
- break;
1885
- }
1886
- case "removed": {
1887
- const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
1888
- const summary = `Add ${c.path} back with ${show$2(c.before)} (${label})`;
1889
- if (now) {
1890
- if (same$1(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
1891
- else conflict("recreated-later", `${c.path} was added again later with another value (${css(c.path, c.file) || show$2(now.value)}).`, { value: now.value, css: css(c.path, c.file) });
1892
- break;
1893
- }
1894
- planned.push({ key, path: c.path, file: c.file, summary, edit });
1895
- break;
1896
- }
1897
- case "renamed": {
1898
- const from = c.from;
1899
- const back = defIn(current, from, c.file);
1900
- if (back && !now) {
1901
- skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
1902
- break;
1903
- }
1904
- if (!now) {
1905
- conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
1906
- break;
1907
- }
1908
- if (back) {
1909
- conflict("recreated-later", `${from} exists again in ${c.file}, so ${c.path} cannot take its old name back.`, { value: back.value, css: css(from, c.file) });
1910
- break;
1911
- }
1912
- if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
1913
- skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
1914
- break;
1915
- }
1916
- planned.push({ key, path: c.path, file: c.file, summary: `Rename ${c.path} back to ${from} and repoint its aliases`, edit: { op: "rename", path: c.path, newPath: from } });
1917
- break;
1918
- }
1919
- }
1727
+ /** Asks these people to review a pull request. */
1728
+ async requestReviewers(n, logins) {
1729
+ this.assertToken("Asking for a review");
1730
+ await this.request("POST", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
1731
+ }
1732
+ /** Takes these people off the requested reviewers. */
1733
+ async removeRequestedReviewers(n, logins) {
1734
+ this.assertToken("Changing the requested reviewers");
1735
+ await this.request("DELETE", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
1736
+ }
1737
+ /** Replies in the thread of a line comment. */
1738
+ async replyToReviewComment(n, commentId, body) {
1739
+ this.assertToken("Replying to a comment");
1740
+ const r = await this.request("POST", `${this.repoPath}/pulls/${n}/comments/${commentId}/replies`, { body });
1741
+ return { id: r.id, url: r.html_url };
1742
+ }
1743
+ /**
1744
+ * What `head` adds on top of `base`, with what library sync needs: the shared start (merge base), the base
1745
+ * commit and its tree, how far behind, and the files with their previous names.
1746
+ */
1747
+ async compareDetailed(base, head) {
1748
+ const r = await this.request("GET", `${this.repoPath}/compare/${encodeRef(base)}...${encodeRef(head)}`);
1749
+ return {
1750
+ behindBy: r.behind_by,
1751
+ aheadBy: r.ahead_by,
1752
+ mergeBase: r.merge_base_commit?.sha ?? null,
1753
+ baseSha: r.base_commit?.sha ?? null,
1754
+ baseTree: r.base_commit?.commit?.tree?.sha ?? null,
1755
+ files: (r.files ?? []).map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {} }))
1756
+ };
1920
1757
  }
1921
- const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
1922
- const title = `Revert: ${entry.title}`;
1923
- const lines = [
1924
- `Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
1925
- "",
1926
- `Original reason: ${entry.why}`,
1927
- "",
1928
- "Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
1929
- ];
1930
- if (conflicts.length) {
1931
- lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
1932
- for (const k of conflicts) lines.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
1758
+ /** A tree on top of `baseTree` from entries with their own file mode (blob sha, inline content, or sha null to delete). */
1759
+ async createTreeEntries(baseTree, entries) {
1760
+ const r = await this.request("POST", `${this.repoPath}/git/trees`, { base_tree: baseTree, tree: entries.map((e) => ({ ...e, type: "blob" })) }, true);
1761
+ return r.sha;
1933
1762
  }
1934
- if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
1935
- return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
1936
1763
  }
1937
- function planRestore(input) {
1938
- const { current, path } = input;
1939
- const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
1940
- if (!files.length) throw new Error(`Token "${path}" does not exist on the base branch. Restoring a removed token is a revert of the change that removed it.`);
1941
- const file = input.file ?? (files.length === 1 ? files[0] : void 0);
1942
- if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
1943
- if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
1944
- const label = modeLabel(fileModes(current.config, file));
1945
- const from = input.label ? ` from ${input.label}` : "";
1946
- const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show$2(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
1947
- const title = `Restore ${path} (${label})${from}`;
1948
- const item = input.item;
1949
- const summary = [
1950
- `Restores \`${path}\` in ${label} to ${show$2(input.value)}${from}.`,
1951
- ...item ? ["", `That value came from ${item.pr ? item.pr.url ? `[#${item.pr.number}](${item.pr.url})` : `#${item.pr.number}` : `commit ${item.sha.slice(0, 7)}`} ("${item.title}"), ${item.date.slice(0, 10)}, by ${item.author}.`] : []
1952
- ].join("\n");
1953
- return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
1764
+ function toCommitSummary(c) {
1765
+ return {
1766
+ sha: c.sha,
1767
+ parents: (c.parents ?? []).map((p) => p.sha),
1768
+ date: c.commit?.committer?.date ?? c.commit?.author?.date ?? "",
1769
+ author: c.author?.login ?? c.commit?.author?.name ?? "unknown",
1770
+ message: c.commit?.message ?? ""
1771
+ };
1954
1772
  }
1955
- const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
1956
- function finishPlan(p) {
1957
- const { current } = p;
1958
- const config = current.config;
1959
- const edits = p.planned.map((x) => x.edit);
1960
- const notes = [];
1961
- const empty = classifyLayered([], [], config);
1962
- const base = {
1963
- kind: p.kind,
1964
- title: p.title,
1965
- prTitle: p.prTitle,
1966
- summary: p.summary,
1967
- ...p.source ? { source: { id: p.source.id, sha: p.source.sha, title: p.source.title, ...p.source.pr ? { pr: p.source.pr } : {}, date: p.source.date, author: p.source.author } } : {},
1968
- planned: p.planned,
1969
- edits,
1970
- conflicts: p.conflicts,
1971
- skipped: p.skipped,
1972
- branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
1773
+ function toSummary(r) {
1774
+ return {
1775
+ number: r.number,
1776
+ title: r.title,
1777
+ state: r.merged_at || r.merged ? "merged" : r.state,
1778
+ draft: Boolean(r.draft),
1779
+ author: r.user?.login ?? "unknown",
1780
+ headRef: r.head?.ref,
1781
+ headSha: r.head?.sha,
1782
+ baseRef: r.base?.ref,
1783
+ labels: (r.labels ?? []).map((l) => l.name),
1784
+ url: r.html_url,
1785
+ updatedAt: r.updated_at,
1786
+ ...typeof r.body === "string" ? { body: r.body } : {},
1787
+ ...r.created_at ? { createdAt: r.created_at } : {},
1788
+ ...r.merged_at ? { mergedAt: r.merged_at } : {},
1789
+ ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {}
1973
1790
  };
1974
- const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
1975
- if (!edits.length) return nothing(p.conflicts.length ? 'Every change conflicts with a later one. Choose "restore the old value" on a conflict to include it.' : "Nothing left to undo: the base branch already has the old values.");
1976
- let result;
1977
- try {
1978
- result = applyTokenEdits(current, edits);
1979
- } catch (e) {
1980
- return { ...nothing(e.message), status: "blocked" };
1791
+ }
1792
+ function friendlyMessage(status, detail, info = {}) {
1793
+ if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
1794
+ if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
1795
+ switch (status) {
1796
+ case 401:
1797
+ return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
1798
+ case 403:
1799
+ if (/rate limit/i.test(detail)) return "GitHub is limiting requests right now. Wait a minute, then try again.";
1800
+ if (/archived/i.test(detail)) return "This repository is archived and read-only.";
1801
+ return 'The token does not have permission for this. It needs "Contents: read and write" and "Pull requests: read and write" on this repository.';
1802
+ case 404:
1803
+ return "Repository, branch or file not found, or the token cannot see this repository.";
1804
+ case 409:
1805
+ return "GitHub reported a conflict (the branch moved while saving). Scan again, then retry.";
1806
+ case 429:
1807
+ return "GitHub is limiting requests right now. Wait a minute, then try again.";
1808
+ case 422:
1809
+ return `GitHub refused the request${detail ? `: ${detail}` : "."}`;
1810
+ default:
1811
+ return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
1981
1812
  }
1982
- notes.push(...result.notes);
1983
- const after = new LayeredTokenSet(config, result.files);
1984
- const changes = diffLayered(current, after);
1985
- if (!changes.length) return nothing("The edits leave every token as it is now.");
1986
- const prev = new Set(validateLayered(current).map(issueKey));
1987
- const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
1988
- const pairs2 = p.pairs ?? [];
1989
- const modes = config.contrast?.modes;
1990
- const contrast = pairs2.length ? contrastDelta(runLayeredContrast(current, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
1991
- const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
1992
- const tier = classifyLayered(changes, [], config);
1993
- const impact = layeredImpact(current, after, changes);
1994
- const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
1995
- const changeset = config.changesets ? { path: `${config.changesets.dir.replace(/\/$/, "")}/bridge-${slugify(p.title, 48)}.md`, content: formatChangeset(config.changesets.package, tier.bump, p.title.replace(/\.?$/, ".")) } : void 0;
1996
- const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
1997
- const commitMessage = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
1998
-
1999
- ${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
2000
- const blocked = newErrors.length || newFailures.length;
2001
- return {
2002
- ...base,
2003
- status: blocked ? "blocked" : "ready",
2004
- ...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
2005
- changes,
2006
- contrast,
2007
- newFailures,
2008
- newErrors,
2009
- tier,
2010
- files,
2011
- ...changeset ? { changeset } : {},
2012
- commitMessage,
2013
- prBody,
2014
- notes
1813
+ }
1814
+ async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
1815
+ const treeSha = await client.getCommitTree(commitSha);
1816
+ const { entries, truncated } = await client.getTree(treeSha, true);
1817
+ if (truncated) throw new Error("The repository tree is too large to read in one request.");
1818
+ const warnings = [];
1819
+ let config;
1820
+ let configText;
1821
+ const configEntry = entries.find((e) => e.type === "blob" && e.path === CONFIG_FILE);
1822
+ if (configEntry) {
1823
+ try {
1824
+ configText = await client.getBlobText(configEntry.sha);
1825
+ config = loadConfig(configText);
1826
+ } catch (err) {
1827
+ warnings.push(err instanceof ConfigError ? `${err.message}. Using ${fallbackTokensPath} instead.` : `${CONFIG_FILE} could not be read.`);
1828
+ }
1829
+ }
1830
+ const tokensPath2 = (config?.tokens.root ?? fallbackTokensPath).replace(/^\/+|\/+$/g, "");
1831
+ const checksFile = config?.tokens.contrastChecks ?? DEFAULT_TOKEN_PATHS.contrastChecks;
1832
+ const prefix = tokensPath2 + "/";
1833
+ const wanted = entries.filter(
1834
+ (e) => e.type === "blob" && e.path.startsWith(prefix) && (e.path.endsWith(".tokens.json") || e.path === prefix + checksFile)
1835
+ );
1836
+ if (!wanted.some((e) => e.path.endsWith(".tokens.json"))) throw new Error(`No *.tokens.json files found under ${tokensPath2} at ${commitSha.slice(0, 7)}.`);
1837
+ const files = {};
1838
+ let contrastChecks;
1839
+ const queue = [...wanted];
1840
+ const worker = async () => {
1841
+ for (let e = queue.shift(); e; e = queue.shift()) {
1842
+ const text = await client.getBlobText(e.sha);
1843
+ const rel = e.path.slice(prefix.length);
1844
+ if (rel === checksFile) contrastChecks = text;
1845
+ else files[rel] = text;
1846
+ }
2015
1847
  };
1848
+ await Promise.all([worker(), worker(), worker(), worker()]);
1849
+ return { commitSha, treeSha, files, contrastChecks, config, ...config ? { configText } : {}, tokensPath: tokensPath2, warnings };
2016
1850
  }
2017
- function planToJson(plan) {
1851
+ function withRulesetUrl(r) {
1852
+ const href = r._links?.html?.href;
1853
+ return href && !r.html_url ? { ...r, html_url: href } : r;
1854
+ }
1855
+ function toIssue(r) {
2018
1856
  return {
2019
- bridge: "change-plan",
2020
- kind: plan.kind,
2021
- status: plan.status,
2022
- title: plan.prTitle,
2023
- branch: plan.branch,
2024
- tier: plan.tier.tier,
2025
- bump: plan.tier.bump,
2026
- edits: plan.edits,
2027
- conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
2028
- files: plan.files,
2029
- commitMessage: plan.commitMessage,
2030
- prBody: plan.prBody
1857
+ number: r.number,
1858
+ title: r.title,
1859
+ body: r.body ?? "",
1860
+ url: r.html_url,
1861
+ state: r.state,
1862
+ stateReason: r.state_reason ?? null,
1863
+ labels: r.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
1864
+ author: r.user?.login ?? "",
1865
+ createdAt: r.created_at,
1866
+ updatedAt: r.updated_at,
1867
+ ...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {},
1868
+ ...r.closed_at ? { closedAt: r.closed_at } : {}
2031
1869
  };
2032
1870
  }
2033
- function packageDirCandidates(config) {
2034
- const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
2035
- const out = [];
2036
- for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
2037
- return out;
1871
+ const FORMER_MEMBER = "A former member";
1872
+ const requesterFirstName = (name) => name.trim() === FORMER_MEMBER ? FORMER_MEMBER : name.trim().split(/\s+/)[0] ?? name;
1873
+ const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
1874
+ const LINE = /(?:^|\n)[ \t>*_-]*(?:<sub>)?\s*Requested (?:from [^\n<]{1,40}? )?by ([^\n<]{1,120}?)(?:\s+with Bridge)?\.?\s*(?:<\/sub>)?[ \t]*(?=\n|$)/i;
1875
+ const SUGGESTED = new RegExp(LINE.source.replace("Requested (?:from", "Suggested (?:from"), "i");
1876
+ function requesterOf(body) {
1877
+ const m = LINE.exec(body ?? "") ?? SUGGESTED.exec(body ?? "");
1878
+ if (!m) return null;
1879
+ const who = m[1].trim().replace(/\.$/, "").replace(/\s+through Bridge$/i, "").replace(/\s*\([^()\s@]+@[^()\s]+\)$/, "");
1880
+ const both = /^(.+?)\s*\(@([^)\s]+)\)$/.exec(who);
1881
+ if (both) return LOGIN.test(both[2]) ? { name: both[1].trim(), login: both[2] } : null;
1882
+ if (who.startsWith("@")) return LOGIN.test(who.slice(1)) ? { name: null, login: who.slice(1) } : null;
1883
+ return /^[\p{L}][\p{L}'’. -]{0,60}$/u.test(who) && who.split(/\s+/).length <= 5 ? { name: who, login: null } : null;
2038
1884
  }
2039
- function mergeReleases(changelog, tags) {
2040
- const byVersion = /* @__PURE__ */ new Map();
2041
- for (const r of [...changelog, ...tags]) {
2042
- const prev = byVersion.get(r.version);
2043
- byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
2044
- }
2045
- return [...byVersion.values()];
1885
+ function requesterLine(r) {
1886
+ const name = r.name?.trim();
1887
+ const login = r.login?.replace(/^@/, "").trim();
1888
+ if (name && login) return `Requested by ${name} (@${login}).`;
1889
+ if (login) return `Requested by @${login}.`;
1890
+ if (name) return `Requested by ${name}.`;
1891
+ throw new Error("A requester needs a name or a login.");
2046
1892
  }
2047
- function buildReport(args) {
2048
- const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2049
- return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
1893
+ function requesterName(body) {
1894
+ const r = requesterOf(body);
1895
+ return r ? r.name ?? r.login : null;
1896
+ }
1897
+ const BULK_LIMIT = 60;
1898
+ const UNRELEASED = "Unreleased";
1899
+ const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
1900
+ function prNumberFromSubject(subject) {
1901
+ for (const re of SUBJECT_PR) {
1902
+ const m = re.exec(subject);
1903
+ if (m) return Number(m[1]);
1904
+ }
1905
+ return void 0;
2050
1906
  }
2051
- const DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
2052
- const PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
2053
- const ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
2054
- function anatomyLabel(className) {
2055
- if (ACRONYMS.has(className)) return `Size ${className}`;
2056
- const words = className.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().toLowerCase();
2057
- if (!words) return className;
2058
- if (words === "root") return "Container";
2059
- return words.charAt(0).toUpperCase() + words.slice(1);
1907
+ function parseChangesetText(path, text) {
1908
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
1909
+ if (!m) return null;
1910
+ const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
1911
+ const order = ["major", "minor", "patch"];
1912
+ const bump = order.find((b) => bumps.some((x) => x.bump === b));
1913
+ const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
1914
+ return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
2060
1915
  }
2061
- function cssModuleClasses(css) {
2062
- const clean = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
2063
- const out = [];
2064
- for (const m of clean.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) {
2065
- const before = clean[m.index - 1] ?? " ";
2066
- if (/[\w)]/.test(before)) continue;
2067
- if (!out.includes(m[1])) out.push(m[1]);
1916
+ function firstParagraph(text) {
1917
+ if (!text) return "";
1918
+ const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
1919
+ for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
1920
+ const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
1921
+ const s = lines.join(" ").replace(/\s+/g, " ").trim();
1922
+ if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
2068
1923
  }
2069
- return out;
1924
+ return "";
2070
1925
  }
2071
- const cell$2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
2072
- function propsTable(name, props, inherits) {
2073
- if (!props.length) return inherits ? `${name} has no props of its own. It accepts every native <${inherits}> attribute.` : "";
2074
- const rows = props.map((p) => `| \`${p.name}\`${p.required ? " (required)" : ""} | \`${cell$2(p.type)}\` | ${p.defaultValue ? `\`${cell$2(p.defaultValue)}\`` : ""} | ${cell$2(p.description ?? "")} |`);
2075
- const tail = inherits ? `
2076
-
2077
- Also accepts every native <${inherits}> attribute.` : "";
2078
- return ["| Prop | Type | Default | Description |", "| --- | --- | --- | --- |", ...rows].join("\n") + tail;
1926
+ function countChanges(changes) {
1927
+ const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
1928
+ for (const x of changes) c[x.kind]++;
1929
+ return c;
2079
1930
  }
2080
- const isEmpty = (v) => v === void 0 || v === null || typeof v === "string" && !v.trim() || Array.isArray(v) && v.length === 0;
2081
- function missingProse(d) {
2082
- return PROSE_KEYS.filter((k) => isEmpty(d?.[k]));
1931
+ const shortSha = (sha) => sha.slice(0, 7);
1932
+ function titleOf(commit) {
1933
+ if (commit.pr?.title) return commit.pr.title;
1934
+ if (/^Merge pull request #\d+/.test(commit.subject)) {
1935
+ const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
1936
+ if (first) return first.trim();
1937
+ }
1938
+ return commit.subject;
2083
1939
  }
2084
- function prefillDocs(input) {
2085
- const existing = input.existing ?? null;
2086
- const anatomy = {};
2087
- for (const c of input.cssClasses) anatomy[c] = existing?.anatomy?.[c] ?? anatomyLabel(c);
2088
- const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p) => `${p.foreground} on ${p.background}: ${p.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p.min}:1 (${p.ratio >= p.min ? "passes" : "fails"}).`);
2089
- const missing = missingProse(existing);
2090
- const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
2091
- const draft = {
2092
- summary: existing?.summary ?? input.description ?? "",
2093
- ...existing?.category ? { category: existing.category } : {},
2094
- ...existing?.status ? { status: existing.status } : {},
2095
- ...existing?.preview ? { preview: existing.preview } : {},
2096
- whenToUse: existing?.whenToUse ?? [],
2097
- whenNotToUse: existing?.whenNotToUse ?? [],
2098
- dos: existing?.dos ?? [],
2099
- donts: existing?.donts ?? [],
2100
- accessibility: existing?.accessibility ?? [],
2101
- contentGuidelines: existing?.contentGuidelines ?? [],
2102
- anatomy,
2103
- ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
2104
- ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
2105
- ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
2106
- };
1940
+ function buildHistoryEntry(input) {
1941
+ const { config, commit } = input;
1942
+ const beforeSet = new LayeredTokenSet(config, input.before);
1943
+ const afterSet = new LayeredTokenSet(config, input.after);
1944
+ const changes = diffLayered(beforeSet, afterSet);
1945
+ if (!changes.length) return null;
1946
+ const tier = classifyLayered(changes, [], config);
1947
+ const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
1948
+ const pkg = config.changesets?.package;
1949
+ const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
1950
+ const title = titleOf(commit);
1951
+ const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
1952
+ const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
1953
+ const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
1954
+ const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
1955
+ const bump = changesetBump2 ?? tier.bump;
2107
1956
  return {
2108
- component: input.name,
2109
- propsTable: propsTable(input.name, input.props, input.inherits),
2110
- tokens: [...new Set(input.tokens)].sort(),
2111
- inheritedTokens: input.inheritedTokens ?? {},
2112
- anatomy,
2113
- contrastNotes,
2114
- changelogLine,
2115
- missing,
2116
- draft
1957
+ id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
1958
+ sha: commit.sha,
1959
+ ...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
1960
+ date: commit.date,
1961
+ // Who asked, as Bridge recorded it in the pull request ("Requested by Priya Shah ... through Bridge."), before the
1962
+ // account that opened it (the Bridge app for an email member, an agent): History names the person.
1963
+ author: requesterName(commit.pr?.body) ?? commit.pr?.author ?? commit.author,
1964
+ title,
1965
+ why,
1966
+ ...pr ? { pr } : {},
1967
+ changes,
1968
+ counts: countChanges(changes),
1969
+ tier: tier.tier,
1970
+ bump,
1971
+ breaking: bump === "major",
1972
+ reasons: tier.reasons,
1973
+ changesets: relevant,
1974
+ ...changes.length > BULK_LIMIT ? { bulk: true } : {},
1975
+ ...input.demo ? { demo: true } : {}
2117
1976
  };
2118
1977
  }
2119
- const SECTION_SHAPE = {
2120
- summary: '"summary": "one or two sentences"',
2121
- whenToUse: '"whenToUse": ["..."]',
2122
- whenNotToUse: '"whenNotToUse": ["..."]',
2123
- dos: '"dos": [{ "title": "...", "text": "..." }]',
2124
- donts: '"donts": [{ "title": "...", "text": "..." }]',
2125
- accessibility: '"accessibility": ["..."]',
2126
- contentGuidelines: '"contentGuidelines": ["..."]'
2127
- };
2128
- const docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
2129
- function docsPrompt(prefill, conventions = {}, options = {}) {
2130
- const facts = [
2131
- `Component: ${prefill.component}`,
2132
- prefill.propsTable ? `Props (parsed from TypeScript):
2133
- ${prefill.propsTable}` : "Props: none of its own.",
2134
- `Tokens its stylesheet reads: ${prefill.tokens.join(", ") || "none"}`,
2135
- ...Object.entries(prefill.inheritedTokens).map(([from, list]) => `Tokens it reads through ${from}'s stylesheet: ${list.length}`),
2136
- `Anatomy (CSS module classes): ${Object.entries(prefill.anatomy).map(([k, v]) => `${k} = ${v}`).join("; ") || "none"}`,
2137
- prefill.contrastNotes.length ? `Contrast checks:
2138
- - ${prefill.contrastNotes.join("\n- ")}` : "",
2139
- `Changelog line: ${prefill.changelogLine}`
2140
- ].filter(Boolean);
2141
- const block = (title, text) => text?.trim() ? `<${title}>
2142
- ${text.trim()}
2143
- </${title}>` : "";
2144
- const keys = options.keys;
2145
- const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
2146
- const write = keys ? keys.filter((k) => !check.includes(k)) : [];
2147
- const task = keys ? [
2148
- ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
2149
- ...check.length ? [
2150
- `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
2151
- "Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
2152
- "Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
2153
- ] : [],
2154
- ...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
2155
- ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
2156
- const shape = keys ? [
2157
- `- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
2158
- `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
2159
- ] : [
2160
- `- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
2161
- "- Answer with the full docs.json as one JSON object, nothing else."
2162
- ];
2163
- return [
2164
- ...task,
2165
- "",
2166
- "Rules:",
2167
- "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
2168
- "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
2169
- '- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
2170
- ...shape,
2171
- "",
2172
- "Facts computed from the code:",
2173
- facts.join("\n\n"),
2174
- "",
2175
- `Current draft:
2176
- ${JSON.stringify(prefill.draft, null, 2)}`,
2177
- "",
2178
- block("source", conventions.source),
2179
- block("styles", conventions.styles),
2180
- block("agents-md", conventions.agents),
2181
- block("voice", conventions.voice),
2182
- block("example-docs-json", conventions.example)
2183
- ].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
2184
- }
2185
- function markClaudeDraft(d, sections) {
2186
- if (!sections) {
2187
- const { draftedSections: _all, ...rest } = d;
2188
- return { ...rest, draftedBy: "claude", needsReview: true };
1978
+ async function collectHistory(config, backend, opts) {
1979
+ const root = config.tokens.root.replace(/\/$/, "");
1980
+ const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
1981
+ const seen = /* @__PURE__ */ new Set();
1982
+ const picked = [];
1983
+ for (const c of commits) {
1984
+ const n = c.pr?.number;
1985
+ if (n !== void 0) {
1986
+ if (seen.has(n)) continue;
1987
+ seen.add(n);
1988
+ }
1989
+ picked.push(c);
2189
1990
  }
2190
- const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
2191
- const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections.includes(k));
2192
- return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
2193
- }
2194
- function sectionsNeedingReview(d) {
2195
- if (!d || d.needsReview !== true) return [];
2196
- return d.draftedSections ? PROSE_KEYS.filter((k) => d.draftedSections.includes(k)) : [...PROSE_KEYS];
2197
- }
2198
- const needsDocsReview = (d) => Boolean(d && d.needsReview === true);
2199
- function acceptDocsDraft(d) {
2200
- const { draftedSections: _done, ...rest } = d;
2201
- return { ...rest, needsReview: false };
1991
+ const entries = [];
1992
+ let done = 0;
1993
+ for (const c of picked) {
1994
+ const parent = c.parents[0];
1995
+ const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
1996
+ let changesets = [];
1997
+ if (backend.changesetPaths && backend.readFile && config.changesets) {
1998
+ const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
1999
+ const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
2000
+ changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
2001
+ }
2002
+ const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
2003
+ if (e) entries.push(e);
2004
+ opts.onProgress?.(++done, picked.length);
2005
+ }
2006
+ return entries;
2202
2007
  }
2203
- function formatDocsJson(d) {
2204
- const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
2205
- const out = {};
2206
- for (const k of order) if (d[k] !== void 0) out[k] = d[k];
2207
- for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
2208
- return `${JSON.stringify(out, null, 2)}
2209
- `;
2008
+ function parseChangelog(text) {
2009
+ const out = [];
2010
+ const parts = text.split(/^##\s+/m).slice(1);
2011
+ for (const p of parts) {
2012
+ const [head, ...rest] = p.split(/\r?\n/);
2013
+ const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
2014
+ if (!version) continue;
2015
+ const body = rest.join("\n");
2016
+ const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
2017
+ const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
2018
+ const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
2019
+ out.push({ version, ...date ? { date } : {}, shas, prs });
2020
+ }
2021
+ return out;
2210
2022
  }
2211
- function patchNode(old, next) {
2212
- if (plainEqual(toPlain(old), next)) return old;
2213
- if (old.kind === "object" && next && typeof next === "object" && !Array.isArray(next)) {
2214
- const n = next;
2215
- const kept = old.entries.filter((e) => n[e.key] !== void 0).map((e) => ({ key: e.key, value: patchNode(e.value, n[e.key]) }));
2216
- const added = Object.keys(n).filter((k) => n[k] !== void 0 && !getEntry(old, k)).map((k) => ({ key: k, value: fromPlain(n[k], void 0, old.inline) }));
2217
- return { kind: "object", inline: old.inline, entries: [...kept, ...added] };
2023
+ function releasesFromTags(tags, pkg) {
2024
+ const out = [];
2025
+ for (const t of tags) {
2026
+ const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
2027
+ if (!m) continue;
2028
+ if (pkg && m[1] && m[1] !== pkg) continue;
2029
+ out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
2218
2030
  }
2219
- if (old.kind === "array" && Array.isArray(next)) {
2220
- return { kind: "array", inline: old.inline, items: next.map((v, i) => old.items[i] ? patchNode(old.items[i], v) : fromPlain(v, old.items[0], old.inline)) };
2031
+ return out;
2032
+ }
2033
+ function assignVersions(entries, releases) {
2034
+ const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
2035
+ return entries.map((e) => {
2036
+ const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
2037
+ const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
2038
+ return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
2039
+ });
2040
+ }
2041
+ const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
2042
+ function versionTimeline(entries, releases = []) {
2043
+ const groups = /* @__PURE__ */ new Map();
2044
+ for (const e of entries) {
2045
+ const v = e.version ?? UNRELEASED;
2046
+ let g = groups.get(v);
2047
+ if (!g) {
2048
+ const rel = releases.find((r) => r.version === v);
2049
+ g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
2050
+ groups.set(v, g);
2051
+ }
2052
+ g.entries.push(e.id);
2053
+ if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
2054
+ g.tokenChanges += e.counts.total;
2055
+ g.breaking ||= e.breaking;
2056
+ if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
2057
+ if (!g.authors.includes(e.author)) g.authors.push(e.author);
2058
+ if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
2221
2059
  }
2222
- return fromPlain(next, old);
2060
+ return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
2223
2061
  }
2224
- function patchDocsJson(original, next) {
2225
- const plain = JSON.parse(formatDocsJson(next));
2226
- if (!original) return formatDocsJson(next);
2227
- try {
2228
- const doc = parseJson(original);
2229
- if (doc.root.kind !== "object") return formatDocsJson(next);
2230
- return serializeJson({ ...doc, root: patchNode(doc.root, plain) });
2231
- } catch {
2232
- return formatDocsJson(next);
2062
+ const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
2063
+ function filterHistory(entries, f = {}) {
2064
+ const q = f.query?.trim().toLowerCase();
2065
+ return entries.filter(
2066
+ (e) => (!f.token || e.changes.some((c) => matchesToken(c.path, f.token) || c.from && matchesToken(c.from, f.token))) && (!f.author || e.author.toLowerCase() === f.author.toLowerCase()) && (!f.tier || e.tier === f.tier) && (!f.version || (e.version ?? UNRELEASED) === f.version) && (f.breaking === void 0 || e.breaking === f.breaking) && (!f.since || Date.parse(e.date) >= Date.parse(f.since)) && (!f.until || Date.parse(e.date) <= Date.parse(f.until) + 86399999) && (!q || `${e.title} ${e.why} ${e.pr ? `#${e.pr.number}` : ""} ${e.sha}`.toLowerCase().includes(q))
2067
+ );
2068
+ }
2069
+ function fileModes(config, file) {
2070
+ for (const layer of config.tokens.layers) {
2071
+ const keys = [];
2072
+ const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
2073
+ keys.push(k);
2074
+ return "";
2075
+ });
2076
+ const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
2077
+ const m = new RegExp(source).exec(file);
2078
+ if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
2233
2079
  }
2080
+ return {};
2234
2081
  }
2235
- function acceptDocsText(text) {
2236
- return patchDocsJson(text, acceptDocsDraft(JSON.parse(text)));
2082
+ function modeLabel(modes) {
2083
+ const v = Object.values(modes).filter(Boolean);
2084
+ if (!v.length) return "All modes";
2085
+ const s = v.join(" ");
2086
+ return s.charAt(0).toUpperCase() + s.slice(1);
2237
2087
  }
2238
- const DOCS_CHANGE_BUMP = "patch";
2239
- function cleanDraftText(text) {
2240
- return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2088
+ function tokenHistory(entries, path, config) {
2089
+ const names2 = /* @__PURE__ */ new Set([path]);
2090
+ const out = [];
2091
+ const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2092
+ for (const e of sorted) {
2093
+ const hits = e.changes.filter((c) => names2.has(c.path));
2094
+ for (const c of hits) {
2095
+ const modes = config ? fileModes(config, c.file) : {};
2096
+ out.push({
2097
+ entryId: e.id,
2098
+ sha: e.sha,
2099
+ date: e.date,
2100
+ author: e.author,
2101
+ title: e.title,
2102
+ why: e.why,
2103
+ ...e.pr ? { pr: e.pr } : {},
2104
+ ...e.version ? { version: e.version } : {},
2105
+ ...e.demo ? { demo: true } : {},
2106
+ kind: c.kind,
2107
+ path: c.path,
2108
+ ...c.from ? { from: c.from } : {},
2109
+ file: c.file,
2110
+ modes,
2111
+ modeLabel: modeLabel(modes),
2112
+ fields: c.fields,
2113
+ ...c.before !== void 0 ? { before: c.before } : {},
2114
+ ...c.after !== void 0 ? { after: c.after } : {},
2115
+ ...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
2116
+ ...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
2117
+ ...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
2118
+ ...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
2119
+ });
2120
+ }
2121
+ for (const c of hits) if (c.kind === "renamed" && c.from) names2.add(c.from);
2122
+ }
2123
+ return out;
2241
2124
  }
2242
- const values = (s) => (s ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
2243
- const article = (word) => /^[aeiou]/i.test(word) ? "an" : "a";
2244
- const andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2245
- const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
2246
- const capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2247
- const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
2248
- function propShape(s) {
2249
- if (!s) return void 0;
2250
- const m = /^(.*?), default (.*)$/.exec(s);
2251
- return m ? { type: m[1], def: m[2] } : { type: s, def: "" };
2125
+ const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2126
+ const keyOf = (path, file) => `${path}@${file}`;
2127
+ const show$2 = (v) => typeof v === "string" ? v : JSON.stringify(v);
2128
+ function defIn(set, path, file) {
2129
+ return set.defs.find((d) => d.path === path && d.file === file);
2252
2130
  }
2253
- const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
2254
- function shortVariant(key) {
2255
- const pairs2 = key.split(", ").map((p) => {
2256
- const at = p.indexOf("=");
2257
- return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
2131
+ function sharesCombo(set, a, b) {
2132
+ return a === b || set.combos().some((c) => {
2133
+ const files = set.layerFiles(c).files;
2134
+ return files.includes(a) && files.includes(b);
2258
2135
  });
2259
- const kept = pairs2.filter((p) => p.v && !/^default$/i.test(p.v));
2260
- if (!kept.length) return key;
2261
- const rank = (k) => {
2262
- const i = VARIANT_FIRST.indexOf(k.toLowerCase());
2263
- return i < 0 ? VARIANT_FIRST.length : i;
2264
- };
2265
- return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank(a.k) - rank(b.k) || a.i - b.i).map((p) => p.v).join(" ");
2266
- }
2267
- const shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
2268
- function parseStructureChange(c) {
2269
- const m = /^(New|Removed|Moved) layer "(.*)"(?: \((.*)\))?$/.exec(c.field);
2270
- if (!m) return void 0;
2271
- const op = m[1] === "New" ? "added" : m[1] === "Removed" ? "removed" : "moved";
2272
- const where2 = `in ${shortVariants(m[3])}`;
2273
- if (op === "moved") return { op, name: m[2], where: where2, at: c.after, from: c.before };
2274
- const shape = (op === "added" ? c.after : c.before) ?? "";
2275
- const sm = /^(\S+)(?: (.*?))?, ((?:after|before) ".*"|alone)$/.exec(shape);
2276
- return { op, name: m[2], where: where2, ...sm ? { type: sm[1], ...sm[2] ? { main: sm[2] } : {}, at: sm[3] } : {} };
2277
2136
  }
2278
- function structurePhrase(r) {
2279
- const type = r.type ? `${r.type} ` : "";
2280
- const main = r.main ? ` (${r.main})` : "";
2281
- const at = r.at && r.at !== "alone" ? ` ${r.at}` : "";
2282
- if (r.op === "added") return `New ${type}layer "${r.name}"${main}${at} ${r.where}`;
2283
- if (r.op === "removed") return `Removed ${type}layer "${r.name}"${main}${at} ${r.where}`;
2284
- return `Moved layer "${r.name}" ${r.at ?? ""}${r.from ? ` (was ${r.from})` : ""} ${r.where}`.replace(/\s+/g, " ");
2137
+ function laterTouching(later, path, file, from) {
2138
+ return later.filter((e) => e.changes.some((c) => c.file === file && (c.path === path || c.from === path || from !== void 0 && (c.path === from || c.from === from)))).sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).map((e) => ({ id: e.id, title: e.title, date: e.date, author: e.author, ...e.pr ? { pr: e.pr.number } : {} }));
2285
2139
  }
2286
- const bare = (at) => (at ?? "").replace(/"/g, "");
2287
- function firstChange(component, kind, c, all) {
2288
- switch (kind) {
2289
- case "new-component": {
2290
- const axes = all.filter((x) => x.field.startsWith("Variant "));
2291
- const props = all.filter((x) => x.field.startsWith("Prop "));
2292
- const parts = [axes.length ? plural(axes.length, "variant axis", "variant axes") : "", props.length ? plural(props.length, "prop") : ""].filter(Boolean);
2293
- return {
2294
- title: `Add the ${component} component`,
2295
- summary: `${component} is not in code yet.${parts.length ? ` The Figma component has ${andList(parts)}.` : ""}`
2296
- };
2297
- }
2298
- case "new-variant": {
2299
- if (!c) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2300
- if (c.field === "Variants") {
2301
- const gone = (c.before ?? "").split("; ").filter(Boolean);
2302
- const born = (c.after ?? "").split("; ").filter(Boolean);
2303
- if (gone.length === 1 && !born.length) return { title: `Remove the ${shortVariant(gone[0])} variant`, summary: `${component} no longer has the ${shortVariant(gone[0])} variant (${gone[0]}).` };
2304
- if (born.length === 1 && !gone.length) return { title: `Add the ${shortVariant(born[0])} variant`, summary: `${component} gets a new variant, ${shortVariant(born[0])} (${born[0]}).` };
2305
- const parts = [born.length ? `gets ${plural(born.length, "new variant")}: ${andList(born.map(shortVariant))}` : "", gone.length ? `no longer has ${andList(gone.map(shortVariant))}` : ""].filter(Boolean);
2306
- return { title: `Update the ${component} variants`, summary: `${component} ${parts.join(", and ")}.` };
2140
+ function planRevert(input) {
2141
+ const { current, entry } = input;
2142
+ const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
2143
+ const res = input.resolutions ?? {};
2144
+ const planned = [];
2145
+ const conflicts = [];
2146
+ const skipped = [];
2147
+ const config = current.config;
2148
+ const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
2149
+ for (const c of entry.changes) {
2150
+ const key = keyOf(c.path, c.file);
2151
+ const label = modeLabel(fileModes(config, c.file));
2152
+ const original = { ...c.before !== void 0 ? { before: c.before } : {}, ...c.after !== void 0 ? { after: c.after } : {}, ...c.beforeCss ? { beforeCss: c.beforeCss } : {}, ...c.afterCss ? { afterCss: c.afterCss } : {} };
2153
+ const conflict = (reason, message, cur, editIfForced, summary) => {
2154
+ const resolution = res[key] ?? "keep-current";
2155
+ conflicts.push({ key, path: c.path, file: c.file, kind: c.kind, reason, message, original, ...cur ? { current: cur } : {}, later: laterTouching(later, c.path, c.file, c.from), resolution });
2156
+ if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
2157
+ };
2158
+ const now = defIn(current, c.path, c.file);
2159
+ switch (c.kind) {
2160
+ case "changed": {
2161
+ if (!now) {
2162
+ conflict("removed-later", `${c.path} was removed from ${c.file} after this change. Restoring it is a new token decision, not a revert.`, void 0, { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} }, `Add ${c.path} back with ${show$2(c.before)} (${label})`);
2163
+ break;
2164
+ }
2165
+ const valueField = c.fields.includes("value");
2166
+ const descField = c.fields.includes("description");
2167
+ const cur = { value: now.value, css: css(c.path, c.file) };
2168
+ const valueAt = (v) => same$1(now.value, v);
2169
+ const descNow = now.description ?? "";
2170
+ if (!valueField && !descField) {
2171
+ skipped.push({ key, path: c.path, file: c.file, reason: "Only the type or extensions changed: edit those by hand." });
2172
+ break;
2173
+ }
2174
+ if ((!valueField || valueAt(c.before)) && (!descField || descNow === (c.descriptionBefore ?? ""))) {
2175
+ skipped.push({ key, path: c.path, file: c.file, reason: "Already back to the value from before this change." });
2176
+ break;
2177
+ }
2178
+ const valueMoved = valueField && !valueAt(c.after) && !valueAt(c.before);
2179
+ const descMoved = descField && descNow !== (c.descriptionAfter ?? "") && descNow !== (c.descriptionBefore ?? "");
2180
+ const edit = {
2181
+ op: "set",
2182
+ path: c.path,
2183
+ file: c.file,
2184
+ ...valueField ? { value: c.before } : {},
2185
+ ...descField ? { description: c.descriptionBefore ?? "" } : {}
2186
+ };
2187
+ const summary = valueField ? `Set ${c.path} back to ${show$2(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
2188
+ if (valueMoved || descMoved) {
2189
+ conflict("changed-later", `${c.path} changed again after this change: it is now ${cur.css || show$2(cur.value)}, not ${c.afterCss || show$2(c.after)}.`, cur, edit, summary);
2190
+ break;
2191
+ }
2192
+ planned.push({ key, path: c.path, file: c.file, summary, edit });
2193
+ break;
2194
+ }
2195
+ case "added": {
2196
+ if (!now) {
2197
+ skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
2198
+ break;
2199
+ }
2200
+ const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
2201
+ const edit = { op: "remove", path: c.path, file: c.file };
2202
+ const summary = `Remove ${c.path} (${label})`;
2203
+ if (!same$1(now.value, c.after)) {
2204
+ conflict("changed-later", `${c.path} changed after it was added: it is now ${css(c.path, c.file) || show$2(now.value)}.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
2205
+ break;
2206
+ }
2207
+ if (users.length) {
2208
+ conflict("still-referenced", `${users.length} token(s) alias ${c.path} now (${users.slice(0, 4).map((u) => u.path).join(", ")}${users.length > 4 ? ", ..." : ""}). Removing it would break them.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
2209
+ break;
2210
+ }
2211
+ planned.push({ key, path: c.path, file: c.file, summary, edit });
2212
+ break;
2307
2213
  }
2308
- const axis = c.field.replace(/^Variant /, "");
2309
- const before = values(c.before);
2310
- const after = values(c.after);
2311
- const added = after.filter((x) => !before.includes(x));
2312
- const removed = before.filter((x) => !after.includes(x));
2313
- const word = axis.toLowerCase();
2314
- if (!before.length) return { title: `Add a ${axis} variant axis`, summary: `${component} gets a new variant axis, ${axis}, with ${after.length === 1 ? "the value" : "the values"} ${andList(after)}.` };
2315
- if (!after.length) return { title: `Remove the ${axis} variant axis`, summary: `${component} no longer has the ${axis} variant axis (${andList(before)}).` };
2316
- if (added.length === 1 && !removed.length) return { title: `Add ${article(added[0])} ${added[0]} ${word}`, summary: `${component} gets a new ${axis} value, ${added[0]}.` };
2317
- if (added.length && !removed.length) return { title: `Add ${andList(added)} to ${axis}`, summary: `${component} gets ${plural(added.length, `new ${axis} value`)}: ${andList(added)}.` };
2318
- if (removed.length === 1 && !added.length) return { title: `Remove the ${removed[0]} ${word}`, summary: `${component} no longer has the ${axis} value ${removed[0]}.` };
2319
- if (removed.length && !added.length) return { title: `Remove ${andList(removed)} from ${axis}`, summary: `${component} no longer has the ${axis} values ${andList(removed)}.` };
2320
- return { title: `Change the ${axis} values`, summary: `${axis} on ${component} goes from ${andList(before)} to ${andList(after)}.` };
2321
- }
2322
- case "prop-change": {
2323
- if (!c) return { title: `Change a ${component} prop`, summary: `A prop of ${component} changed in Figma.` };
2324
- const name = c.field.replace(/^Prop /, "");
2325
- const b = propShape(c.before);
2326
- const a = propShape(c.after);
2327
- if (!b && a) return { title: `Add the ${name} prop`, summary: `${component} gets a new ${a.type} prop, ${name}${a.def ? `, default ${a.def}` : ""}.` };
2328
- if (b && !a) return { title: `Remove the ${name} prop`, summary: `${component} no longer has the ${name} prop.` };
2329
- if (b && a && b.type === a.type && b.def !== a.def) return { title: `Change the ${name} default to ${a.def || "empty"}`, summary: `The default of ${name} on ${component} goes from ${b.def || "empty"} to ${a.def || "empty"}.` };
2330
- return { title: `Change the ${name} prop`, summary: `${name} on ${component} goes from ${c.before ?? "none"} to ${c.after ?? "none"}.` };
2331
- }
2332
- case "token-binding": {
2333
- if (!c) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2334
- const m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2335
- const part = lower(m[1]);
2336
- const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2337
- const after = c.after && c.after !== "none" ? c.after : "";
2338
- const before = c.before && c.before !== "none" ? c.before : "";
2339
- if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where2} no longer uses ${before || "a token"}.` };
2340
- return {
2341
- title: `Use ${after} for the ${part}`,
2342
- summary: `The ${part} of ${component}${where2} ${before ? `moves from ${before} to ${after}` : `now uses ${after}`}.`
2343
- };
2344
- }
2345
- case "structure": {
2346
- const r = c ? parseStructureChange(c) : void 0;
2347
- if (!r) return { title: "Change the layers to match Figma", summary: `The layers of ${component} changed in Figma.` };
2348
- if (r.op === "added") {
2349
- const at = r.at && r.at !== "alone" ? ` ${bare(r.at)}` : "";
2350
- return { title: `Add a layer${at}`, summary: `${component} gets ${article(r.type ?? "layer")} ${r.type ?? "layer"}${r.type ? " layer" : ""}, ${r.name}${r.main ? ` (${r.main})` : ""}${at} ${r.where}.` };
2214
+ case "removed": {
2215
+ const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
2216
+ const summary = `Add ${c.path} back with ${show$2(c.before)} (${label})`;
2217
+ if (now) {
2218
+ if (same$1(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
2219
+ else conflict("recreated-later", `${c.path} was added again later with another value (${css(c.path, c.file) || show$2(now.value)}).`, { value: now.value, css: css(c.path, c.file) });
2220
+ break;
2221
+ }
2222
+ planned.push({ key, path: c.path, file: c.file, summary, edit });
2223
+ break;
2224
+ }
2225
+ case "renamed": {
2226
+ const from = c.from;
2227
+ const back = defIn(current, from, c.file);
2228
+ if (back && !now) {
2229
+ skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
2230
+ break;
2231
+ }
2232
+ if (!now) {
2233
+ conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
2234
+ break;
2235
+ }
2236
+ if (back) {
2237
+ conflict("recreated-later", `${from} exists again in ${c.file}, so ${c.path} cannot take its old name back.`, { value: back.value, css: css(from, c.file) });
2238
+ break;
2239
+ }
2240
+ if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
2241
+ skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
2242
+ break;
2243
+ }
2244
+ planned.push({ key, path: c.path, file: c.file, summary: `Rename ${c.path} back to ${from} and repoint its aliases`, edit: { op: "rename", path: c.path, newPath: from } });
2245
+ break;
2351
2246
  }
2352
- if (r.op === "removed") return { title: `Remove the ${r.name} layer`, summary: `${component} no longer has the ${r.name} layer ${r.where}.` };
2353
- return { title: `Move the ${r.name} layer`, summary: `The ${r.name} layer of ${component} moves ${bare(r.at) || "elsewhere"}${r.from ? ` (it was ${bare(r.from)})` : ""} ${r.where}.` };
2354
2247
  }
2355
- case "visual":
2356
- return {
2357
- title: "Update the look to match Figma",
2358
- summary: `${component} looks different in Figma${c?.field.startsWith("Look of ") && c.field !== "Look of every variant" ? ` in ${c.field.slice(8)}` : ""}, and no variable edit explains it.`
2359
- };
2360
- default:
2361
- return { title: "", summary: "" };
2362
2248
  }
2249
+ const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
2250
+ const title = `Revert: ${entry.title}`;
2251
+ const lines = [
2252
+ `Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
2253
+ "",
2254
+ `Original reason: ${entry.why}`,
2255
+ "",
2256
+ "Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
2257
+ ];
2258
+ if (conflicts.length) {
2259
+ lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
2260
+ for (const k of conflicts) lines.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
2261
+ }
2262
+ if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
2263
+ return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
2363
2264
  }
2364
- function draftRequestText(component, kind, changes) {
2365
- const name = component.trim() || "the component";
2366
- if (!changes.length && kind !== "new-component") return { title: "", summary: "" };
2367
- const d = firstChange(name, kind, changes[0], changes);
2368
- const rest = kind === "new-component" ? 0 : changes.length - 1;
2369
- const summary = rest > 0 ? `${d.summary} ${capital(plural(rest, "other change"))} ${rest === 1 ? "is" : "are"} listed below.` : d.summary;
2370
- return { title: cleanDraftText(d.title), summary: cleanDraftText(summary) };
2371
- }
2372
- const DRAFT_LINE_MAX = 120;
2373
- function clipText(text, max) {
2374
- const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
2375
- if (t.length <= max) return t;
2376
- const cut = t.slice(0, max + 1);
2377
- const at = cut.lastIndexOf(" ");
2378
- return (at > max * 0.5 ? cut.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
2379
- }
2380
- function sanitizeDraftLine(raw) {
2381
- if (typeof raw !== "string") return "";
2382
- return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
2265
+ function planRestore(input) {
2266
+ const { current, path } = input;
2267
+ const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
2268
+ if (!files.length) throw new Error(`Token "${path}" does not exist on the base branch. Restoring a removed token is a revert of the change that removed it.`);
2269
+ const file = input.file ?? (files.length === 1 ? files[0] : void 0);
2270
+ if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
2271
+ if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
2272
+ const label = modeLabel(fileModes(current.config, file));
2273
+ const from = input.label ? ` from ${input.label}` : "";
2274
+ const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show$2(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
2275
+ const title = `Restore ${path} (${label})${from}`;
2276
+ const item = input.item;
2277
+ const summary = [
2278
+ `Restores \`${path}\` in ${label} to ${show$2(input.value)}${from}.`,
2279
+ ...item ? ["", `That value came from ${item.pr ? item.pr.url ? `[#${item.pr.number}](${item.pr.url})` : `#${item.pr.number}` : `commit ${item.sha.slice(0, 7)}`} ("${item.title}"), ${item.date.slice(0, 10)}, by ${item.author}.`] : []
2280
+ ].join("\n");
2281
+ return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
2383
2282
  }
2384
- function changePhrase(c) {
2385
- let m;
2386
- if (c.field === "Variants") {
2387
- const gone = (c.before ?? "").split("; ").filter(Boolean).map(shortVariant);
2388
- const born = (c.after ?? "").split("; ").filter(Boolean).map(shortVariant);
2389
- return [born.length ? `New variant${born.length === 1 ? "" : "s"} ${andList(born)}` : "", gone.length ? `${andList(gone)} variant${gone.length === 1 ? "" : "s"} removed` : ""].filter(Boolean).join("; ");
2390
- }
2391
- if (m = /^Variant (.+)$/.exec(c.field)) {
2392
- const axis = m[1];
2393
- const before2 = values(c.before);
2394
- const after2 = values(c.after);
2395
- if (!before2.length) return `New ${axis} variant axis: ${after2.join(", ")}`;
2396
- if (!after2.length) return `${axis} variant axis removed (${before2.join(", ")})`;
2397
- const added = after2.filter((x) => !before2.includes(x));
2398
- const removed = before2.filter((x) => !after2.includes(x));
2399
- const parts = [added.length ? `${andList(added)} added to ${axis}` : "", removed.length ? `${andList(removed)} removed from ${axis}` : ""].filter(Boolean);
2400
- return parts.join("; ") || `${axis} values reordered: ${after2.join(", ")}`;
2401
- }
2402
- if (m = /^Prop (.+)$/.exec(c.field)) {
2403
- const name = m[1];
2404
- const b = propShape(c.before);
2405
- const a = propShape(c.after);
2406
- if (!b && a) return `New ${a.type} prop ${name}${a.def ? `, default ${a.def}` : ""}`;
2407
- if (b && !a) return `${name} prop removed`;
2408
- if (b && a && b.type === a.type) return `${name} default goes from ${b.def || "empty"} to ${a.def || "empty"}`;
2409
- return `${name} prop goes from ${c.before ?? "none"} to ${c.after ?? "none"}`;
2283
+ const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
2284
+ function finishPlan(p) {
2285
+ const { current } = p;
2286
+ const config = current.config;
2287
+ const edits = p.planned.map((x) => x.edit);
2288
+ const notes = [];
2289
+ const empty = classifyLayered([], [], config);
2290
+ const base = {
2291
+ kind: p.kind,
2292
+ title: p.title,
2293
+ prTitle: p.prTitle,
2294
+ summary: p.summary,
2295
+ ...p.source ? { source: { id: p.source.id, sha: p.source.sha, title: p.source.title, ...p.source.pr ? { pr: p.source.pr } : {}, date: p.source.date, author: p.source.author } } : {},
2296
+ planned: p.planned,
2297
+ edits,
2298
+ conflicts: p.conflicts,
2299
+ skipped: p.skipped,
2300
+ branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
2301
+ };
2302
+ const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
2303
+ if (!edits.length) return nothing(p.conflicts.length ? 'Every change conflicts with a later one. Choose "restore the old value" on a conflict to include it.' : "Nothing left to undo: the base branch already has the old values.");
2304
+ let result;
2305
+ try {
2306
+ result = applyTokenEdits(current, edits);
2307
+ } catch (e) {
2308
+ return { ...nothing(e.message), status: "blocked" };
2410
2309
  }
2411
- if (m = /^Swapped instance(?: \((.*)\))?$/.exec(c.field)) return `Shows ${c.after ?? "another instance"} instead of ${c.before ?? "the coded one"} ${m[1] ? `in ${m[1]}` : "in every variant"}`;
2412
- if (m = /^Look of (.+)$/.exec(c.field)) return `Looks different in ${m[1]}; no variable explains it`;
2413
- const layer = parseStructureChange(c);
2414
- if (layer) return structurePhrase(layer);
2415
- m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2416
- const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2417
- const before = c.before && c.before !== "none" ? c.before : "";
2418
- const after = c.after && c.after !== "none" ? c.after : "";
2419
- if (!after) return `${m[1]} no longer uses ${before || "a token"}${where2}`;
2420
- if (!before) return `${m[1]} now uses ${after}${where2}`;
2421
- return `${m[1]} uses ${after} instead of ${before}${where2}`;
2310
+ notes.push(...result.notes);
2311
+ const after = new LayeredTokenSet(config, result.files);
2312
+ const changes = diffLayered(current, after);
2313
+ if (!changes.length) return nothing("The edits leave every token as it is now.");
2314
+ const prev = new Set(validateLayered(current).map(issueKey));
2315
+ const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
2316
+ const pairs2 = p.pairs ?? [];
2317
+ const modes = config.contrast?.modes;
2318
+ const contrast = pairs2.length ? contrastDelta(runLayeredContrast(current, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
2319
+ const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
2320
+ const tier = classifyLayered(changes, [], config);
2321
+ const impact = layeredImpact(current, after, changes);
2322
+ const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
2323
+ const changeset = config.changesets ? { path: `${config.changesets.dir.replace(/\/$/, "")}/bridge-${slugify(p.title, 48)}.md`, content: formatChangeset(config.changesets.package, tier.bump, p.title.replace(/\.?$/, ".")) } : void 0;
2324
+ const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
2325
+ const commitMessage = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
2326
+
2327
+ ${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
2328
+ const blocked = newErrors.length || newFailures.length;
2329
+ return {
2330
+ ...base,
2331
+ status: blocked ? "blocked" : "ready",
2332
+ ...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
2333
+ changes,
2334
+ contrast,
2335
+ newFailures,
2336
+ newErrors,
2337
+ tier,
2338
+ files,
2339
+ ...changeset ? { changeset } : {},
2340
+ commitMessage,
2341
+ prBody,
2342
+ notes
2343
+ };
2344
+ }
2345
+ function planToJson(plan) {
2346
+ return {
2347
+ bridge: "change-plan",
2348
+ kind: plan.kind,
2349
+ status: plan.status,
2350
+ title: plan.prTitle,
2351
+ branch: plan.branch,
2352
+ tier: plan.tier.tier,
2353
+ bump: plan.tier.bump,
2354
+ edits: plan.edits,
2355
+ conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
2356
+ files: plan.files,
2357
+ commitMessage: plan.commitMessage,
2358
+ prBody: plan.prBody
2359
+ };
2422
2360
  }
2423
- function draftChangeLine(kind, changes) {
2424
- if (kind === "new-component") {
2425
- const axes = changes.filter((x) => x.field.startsWith("Variant ")).length;
2426
- const props = changes.filter((x) => x.field.startsWith("Prop ")).length;
2427
- const parts = [axes ? plural(axes, "variant axis", "variant axes") : "", props ? plural(props, "prop") : ""].filter(Boolean);
2428
- return `New component${parts.length ? ` with ${andList(parts)}` : ""}, not in code yet`;
2429
- }
2430
- if (!changes.length) return "";
2431
- const first = changePhrase(changes[0]);
2432
- const rest = changes.length - 1;
2433
- const tail = rest ? ` (and ${rest} more)` : "";
2434
- return capital(clipText(cleanDraftText(first), DRAFT_LINE_MAX - tail.length)) + tail;
2361
+ function packageDirCandidates(config) {
2362
+ const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
2363
+ const out = [];
2364
+ for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
2365
+ return out;
2435
2366
  }
2436
- const tokenOf = (e) => e.figma ?? e.head ?? e.base;
2437
- const pathOf = (e) => tokenOf(e)?.path ?? e.path;
2438
- function pushChangeRows(entries) {
2439
- return entries.map((e) => {
2440
- const before = e.head ?? e.base;
2441
- const after = e.figma;
2442
- const show2 = (t) => {
2443
- if (!t) return void 0;
2444
- const modes = Object.keys(t.values);
2445
- if (modes.length <= 1) return formatValue(t.type, t.values[modes[0] ?? ""]);
2446
- return modes.map((m) => `${modeDisplayName(m)} ${formatValue(t.type, t.values[m])}`).join("; ");
2447
- };
2448
- const renamed = e.local?.kind === "renamed" && before && after && before.path !== after.path;
2449
- return { field: renamed ? `${before.path} -> ${after.path}` : pathOf(e), before: show2(before), after: show2(after) };
2450
- });
2367
+ function mergeReleases(changelog, tags) {
2368
+ const byVersion = /* @__PURE__ */ new Map();
2369
+ for (const r of [...changelog, ...tags]) {
2370
+ const prev = byVersion.get(r.version);
2371
+ byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
2372
+ }
2373
+ return [...byVersion.values()];
2451
2374
  }
2452
- function names(paths) {
2453
- if (paths.length <= 3) return andList(paths);
2454
- return `${paths.length} tokens, including ${andList(paths.slice(0, 2))}`;
2375
+ function buildReport(args) {
2376
+ const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2377
+ return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
2455
2378
  }
2456
- function valueModes(e) {
2457
- return (e.local?.fields ?? []).filter((f) => f.field === "value" && f.mode && f.mode !== "value").map((f) => modeDisplayName(f.mode));
2379
+ const DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
2380
+ const PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
2381
+ const ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
2382
+ function anatomyLabel(className) {
2383
+ if (ACRONYMS.has(className)) return `Size ${className}`;
2384
+ const words = className.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().toLowerCase();
2385
+ if (!words) return className;
2386
+ if (words === "root") return "Container";
2387
+ return words.charAt(0).toUpperCase() + words.slice(1);
2458
2388
  }
2459
- function draftPushDescription(entries, components = []) {
2460
- const by = { changed: [], added: [], renamed: [], removed: [] };
2461
- for (const e of entries) by[e.local?.kind ?? "changed"].push(e);
2462
- const clauses = [];
2463
- if (by.changed.length) {
2464
- const only = by.changed.length === 1 ? by.changed[0] : void 0;
2465
- const fields = new Set((only?.local?.fields ?? []).map((f) => f.field));
2466
- const modes = only ? valueModes(only) : [];
2467
- if (only && fields.size === 1 && fields.has("description")) clauses.push(`updates the description of ${pathOf(only)}`);
2468
- else if (only && modes.length) clauses.push(`changes ${pathOf(only)} in ${andList(modes)}`);
2469
- else clauses.push(`changes ${names(by.changed.map(pathOf))}`);
2470
- }
2471
- if (by.added.length) clauses.push(`adds ${names(by.added.map(pathOf))}`);
2472
- if (by.renamed.length) {
2473
- const r = by.renamed[0];
2474
- const from = (r.head ?? r.base)?.path;
2475
- clauses.push(by.renamed.length === 1 && from && from !== pathOf(r) ? `renames ${from} to ${pathOf(r)}` : `renames ${names(by.renamed.map(pathOf))}`);
2389
+ function cssModuleClasses(css) {
2390
+ const clean = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
2391
+ const out = [];
2392
+ for (const m of clean.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) {
2393
+ const before = clean[m.index - 1] ?? " ";
2394
+ if (/[\w)]/.test(before)) continue;
2395
+ if (!out.includes(m[1])) out.push(m[1]);
2476
2396
  }
2477
- if (by.removed.length) clauses.push(`removes ${names(by.removed.map(pathOf))}`);
2478
- const sentences = [];
2479
- if (clauses.length) sentences.push(`${capital(andList(clauses))}.`);
2480
- const fresh = components.filter((c) => c.kind === "new-component").map((c) => c.component);
2481
- const changed = [...new Set(components.filter((c) => c.kind !== "new-component").map((c) => c.component))];
2482
- const comp = [];
2483
- if (changed.length) comp.push(`${changed.length === 1 ? "a change" : "changes"} to ${andList(changed)}`);
2484
- if (fresh.length) comp.push(`the new ${andList(fresh)} component${fresh.length === 1 ? "" : "s"}`);
2485
- if (comp.length) sentences.push(`Requests ${andList(comp)}.`);
2486
- return cleanDraftText(sentences.join(" "));
2397
+ return out;
2487
2398
  }
2488
- const EXPLORATION_LABEL = "exploration";
2489
- const EXPLORATION_PREFIX = "explore/";
2490
- const EXPLORATION_NOTES_DIR = ".bridge/explorations";
2491
- const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
2492
- const EXPLORATION_CHECK = "Exploration not merged as is";
2493
- const EXPLORATION_READY_LABEL = "ready-for-devs";
2494
- const EXPLORATION_ARCHIVED_LABEL = "archived";
2495
- const EXPLORATION_USE_CASES = [
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." },
2497
- { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
2498
- { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
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." },
2500
- { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
2501
- ];
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
- };
2510
- const MARK$2 = "bridge-exploration";
2511
- const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2512
- const EXPLORATION_SLUG_MAX = 60;
2513
- function explorationSlug(name) {
2514
- const s = name.trim().replace(/^explore\//i, "");
2515
- if (!/[a-z0-9]/i.test(s)) return "";
2516
- return slugify(s, EXPLORATION_SLUG_MAX);
2399
+ const cell$2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
2400
+ function propsTable(name, props, inherits) {
2401
+ if (!props.length) return inherits ? `${name} has no props of its own. It accepts every native <${inherits}> attribute.` : "";
2402
+ const rows = props.map((p) => `| \`${p.name}\`${p.required ? " (required)" : ""} | \`${cell$2(p.type)}\` | ${p.defaultValue ? `\`${cell$2(p.defaultValue)}\`` : ""} | ${cell$2(p.description ?? "")} |`);
2403
+ const tail = inherits ? `
2404
+
2405
+ Also accepts every native <${inherits}> attribute.` : "";
2406
+ return ["| Prop | Type | Default | Description |", "| --- | --- | --- | --- |", ...rows].join("\n") + tail;
2517
2407
  }
2518
- function explorationBranch(nameOrBranch) {
2519
- return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
2408
+ const isEmpty = (v) => v === void 0 || v === null || typeof v === "string" && !v.trim() || Array.isArray(v) && v.length === 0;
2409
+ function missingProse(d) {
2410
+ return PROSE_KEYS.filter((k) => isEmpty(d?.[k]));
2520
2411
  }
2521
- function isExplorationBranch(ref) {
2522
- const r = (ref ?? "").replace(/^refs\/heads\//, "");
2523
- return r.startsWith(EXPLORATION_PREFIX) && SLUG_RE.test(r.slice(EXPLORATION_PREFIX.length));
2412
+ function prefillDocs(input) {
2413
+ const existing = input.existing ?? null;
2414
+ const anatomy = {};
2415
+ for (const c of input.cssClasses) anatomy[c] = existing?.anatomy?.[c] ?? anatomyLabel(c);
2416
+ const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p) => `${p.foreground} on ${p.background}: ${p.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p.min}:1 (${p.ratio >= p.min ? "passes" : "fails"}).`);
2417
+ const missing = missingProse(existing);
2418
+ const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
2419
+ const draft = {
2420
+ summary: existing?.summary ?? input.description ?? "",
2421
+ ...existing?.category ? { category: existing.category } : {},
2422
+ ...existing?.status ? { status: existing.status } : {},
2423
+ ...existing?.preview ? { preview: existing.preview } : {},
2424
+ whenToUse: existing?.whenToUse ?? [],
2425
+ whenNotToUse: existing?.whenNotToUse ?? [],
2426
+ dos: existing?.dos ?? [],
2427
+ donts: existing?.donts ?? [],
2428
+ accessibility: existing?.accessibility ?? [],
2429
+ contentGuidelines: existing?.contentGuidelines ?? [],
2430
+ anatomy,
2431
+ ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
2432
+ ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
2433
+ ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
2434
+ };
2435
+ return {
2436
+ component: input.name,
2437
+ propsTable: propsTable(input.name, input.props, input.inherits),
2438
+ tokens: [...new Set(input.tokens)].sort(),
2439
+ inheritedTokens: input.inheritedTokens ?? {},
2440
+ anatomy,
2441
+ contrastNotes,
2442
+ changelogLine,
2443
+ missing,
2444
+ draft
2445
+ };
2524
2446
  }
2525
- function explorationSlugFromBranch(ref) {
2526
- return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
2447
+ const SECTION_SHAPE = {
2448
+ summary: '"summary": "one or two sentences"',
2449
+ whenToUse: '"whenToUse": ["..."]',
2450
+ whenNotToUse: '"whenNotToUse": ["..."]',
2451
+ dos: '"dos": [{ "title": "...", "text": "..." }]',
2452
+ donts: '"donts": [{ "title": "...", "text": "..." }]',
2453
+ accessibility: '"accessibility": ["..."]',
2454
+ contentGuidelines: '"contentGuidelines": ["..."]'
2455
+ };
2456
+ const docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
2457
+ function docsPrompt(prefill, conventions = {}, options = {}) {
2458
+ const facts = [
2459
+ `Component: ${prefill.component}`,
2460
+ prefill.propsTable ? `Props (parsed from TypeScript):
2461
+ ${prefill.propsTable}` : "Props: none of its own.",
2462
+ `Tokens its stylesheet reads: ${prefill.tokens.join(", ") || "none"}`,
2463
+ ...Object.entries(prefill.inheritedTokens).map(([from, list]) => `Tokens it reads through ${from}'s stylesheet: ${list.length}`),
2464
+ `Anatomy (CSS module classes): ${Object.entries(prefill.anatomy).map(([k, v]) => `${k} = ${v}`).join("; ") || "none"}`,
2465
+ prefill.contrastNotes.length ? `Contrast checks:
2466
+ - ${prefill.contrastNotes.join("\n- ")}` : "",
2467
+ `Changelog line: ${prefill.changelogLine}`
2468
+ ].filter(Boolean);
2469
+ const block = (title, text) => text?.trim() ? `<${title}>
2470
+ ${text.trim()}
2471
+ </${title}>` : "";
2472
+ const keys = options.keys;
2473
+ const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
2474
+ const write = keys ? keys.filter((k) => !check.includes(k)) : [];
2475
+ const task = keys ? [
2476
+ ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
2477
+ ...check.length ? [
2478
+ `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
2479
+ "Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
2480
+ "Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
2481
+ ] : [],
2482
+ ...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
2483
+ ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
2484
+ const shape = keys ? [
2485
+ `- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
2486
+ `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
2487
+ ] : [
2488
+ `- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
2489
+ "- Answer with the full docs.json as one JSON object, nothing else."
2490
+ ];
2491
+ return [
2492
+ ...task,
2493
+ "",
2494
+ "Rules:",
2495
+ "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
2496
+ "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
2497
+ '- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
2498
+ ...shape,
2499
+ "",
2500
+ "Facts computed from the code:",
2501
+ facts.join("\n\n"),
2502
+ "",
2503
+ `Current draft:
2504
+ ${JSON.stringify(prefill.draft, null, 2)}`,
2505
+ "",
2506
+ block("source", conventions.source),
2507
+ block("styles", conventions.styles),
2508
+ block("agents-md", conventions.agents),
2509
+ block("voice", conventions.voice),
2510
+ block("example-docs-json", conventions.example)
2511
+ ].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
2527
2512
  }
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]) };
2513
+ function markClaudeDraft(d, sections) {
2514
+ if (!sections) {
2515
+ const { draftedSections: _all, ...rest } = d;
2516
+ return { ...rest, draftedBy: "claude", needsReview: true };
2541
2517
  }
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 };
2518
+ const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
2519
+ const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections.includes(k));
2520
+ return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
2545
2521
  }
2546
- const explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
2547
- const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
2548
- function explorationProblems(e) {
2549
- const out = [];
2550
- const name = e.name.trim();
2551
- const slug = e.slug?.trim() || explorationSlug(name);
2552
- if (!name) out.push('Name the exploration, for example "Denser data tables".');
2553
- else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
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.");
2555
- return out;
2522
+ function sectionsNeedingReview(d) {
2523
+ if (!d || d.needsReview !== true) return [];
2524
+ return d.draftedSections ? PROSE_KEYS.filter((k) => d.draftedSections.includes(k)) : [...PROSE_KEYS];
2556
2525
  }
2557
- const useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
2558
- function explorationNote(e) {
2559
- const slug = e.slug || explorationSlug(e.name);
2560
- const uc = useCaseLabel(e.useCase);
2561
- return [
2562
- `# Exploration: ${e.name.trim()}`,
2563
- "",
2564
- ...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
2565
- `- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
2566
- ...uc ? [`- Use case: ${uc}`] : [],
2567
- ...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
2568
- "",
2569
- 'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
2570
- ""
2571
- ].join("\n");
2526
+ const needsDocsReview = (d) => Boolean(d && d.needsReview === true);
2527
+ function acceptDocsDraft(d) {
2528
+ const { draftedSections: _done, ...rest } = d;
2529
+ return { ...rest, needsReview: false };
2572
2530
  }
2573
- function explorationPrTitle(e) {
2574
- const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
2575
- return `Exploration: ${n}`;
2531
+ function formatDocsJson(d) {
2532
+ const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
2533
+ const out = {};
2534
+ for (const k of order) if (d[k] !== void 0) out[k] = d[k];
2535
+ for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
2536
+ return `${JSON.stringify(out, null, 2)}
2537
+ `;
2576
2538
  }
2577
- function explorationPrBody(e, ctx) {
2578
- const slug = e.slug || explorationSlug(e.name);
2579
- const branch = `${EXPLORATION_PREFIX}${slug}`;
2580
- const uc = useCaseLabel(e.useCase);
2581
- const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
2582
- const lines = [
2583
- `## Exploration: ${e.name.trim()}`,
2584
- "",
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.`,
2586
- "",
2587
- ...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
2588
- ...uc ? [`**Use case.** ${uc}`, ""] : [],
2589
- "### What is experimental",
2590
- "",
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."],
2592
- ...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
2593
- ""
2594
- ];
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).");
2539
+ function patchNode(old, next) {
2540
+ if (plainEqual(toPlain(old), next)) return old;
2541
+ if (old.kind === "object" && next && typeof next === "object" && !Array.isArray(next)) {
2542
+ const n = next;
2543
+ const kept = old.entries.filter((e) => n[e.key] !== void 0).map((e) => ({ key: e.key, value: patchNode(e.value, n[e.key]) }));
2544
+ const added = Object.keys(n).filter((k) => n[k] !== void 0 && !getEntry(old, k)).map((k) => ({ key: k, value: fromPlain(n[k], void 0, old.inline) }));
2545
+ return { kind: "object", inline: old.inline, entries: [...kept, ...added] };
2620
2546
  }
2621
- lines.push(
2622
- "",
2623
- "### When it is done",
2624
- "",
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."}`,
2627
- "",
2628
- explorationBlock({ ...e, slug })
2629
- );
2630
- return lines.join("\n");
2547
+ if (old.kind === "array" && Array.isArray(next)) {
2548
+ return { kind: "array", inline: old.inline, items: next.map((v, i) => old.items[i] ? patchNode(old.items[i], v) : fromPlain(v, old.items[0], old.inline)) };
2549
+ }
2550
+ return fromPlain(next, old);
2631
2551
  }
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 } : {}
2552
+ function patchDocsJson(original, next) {
2553
+ const plain = JSON.parse(formatDocsJson(next));
2554
+ if (!original) return formatDocsJson(next);
2555
+ try {
2556
+ const doc = parseJson(original);
2557
+ if (doc.root.kind !== "object") return formatDocsJson(next);
2558
+ return serializeJson({ ...doc, root: patchNode(doc.root, plain) });
2559
+ } catch {
2560
+ return formatDocsJson(next);
2561
+ }
2562
+ }
2563
+ function acceptDocsText(text) {
2564
+ return patchDocsJson(text, acceptDocsDraft(JSON.parse(text)));
2565
+ }
2566
+ const DOCS_CHANGE_BUMP = "patch";
2567
+ function cleanDraftText(text) {
2568
+ return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2569
+ }
2570
+ const values = (s) => (s ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
2571
+ const article = (word) => /^[aeiou]/i.test(word) ? "an" : "a";
2572
+ const andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2573
+ const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
2574
+ const capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2575
+ const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
2576
+ function propShape(s) {
2577
+ if (!s) return void 0;
2578
+ const m = /^(.*?), default (.*)$/.exec(s);
2579
+ return m ? { type: m[1], def: m[2] } : { type: s, def: "" };
2580
+ }
2581
+ const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
2582
+ function shortVariant(key) {
2583
+ const pairs2 = key.split(", ").map((p) => {
2584
+ const at = p.indexOf("=");
2585
+ return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
2586
+ });
2587
+ const kept = pairs2.filter((p) => p.v && !/^default$/i.test(p.v));
2588
+ if (!kept.length) return key;
2589
+ const rank = (k) => {
2590
+ const i = VARIANT_FIRST.indexOf(k.toLowerCase());
2591
+ return i < 0 ? VARIANT_FIRST.length : i;
2646
2592
  };
2647
- return `<!-- ${MARK$2} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
2593
+ return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank(a.k) - rank(b.k) || a.i - b.i).map((p) => p.v).join(" ");
2648
2594
  }
2649
- function parseExplorationBody(body) {
2650
- if (!body) return void 0;
2651
- const m = new RegExp(`<!-- ${MARK$2} (\\{[\\s\\S]*?\\}) -->`).exec(body);
2595
+ const shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
2596
+ function parseStructureChange(c) {
2597
+ const m = /^(New|Removed|Moved) layer "(.*)"(?: \((.*)\))?$/.exec(c.field);
2652
2598
  if (!m) return void 0;
2653
- try {
2654
- const e = JSON.parse(m[1]);
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;
2657
- } catch {
2658
- return void 0;
2599
+ const op = m[1] === "New" ? "added" : m[1] === "Removed" ? "removed" : "moved";
2600
+ const where2 = `in ${shortVariants(m[3])}`;
2601
+ if (op === "moved") return { op, name: m[2], where: where2, at: c.after, from: c.before };
2602
+ const shape = (op === "added" ? c.after : c.before) ?? "";
2603
+ const sm = /^(\S+)(?: (.*?))?, ((?:after|before) ".*"|alone)$/.exec(shape);
2604
+ return { op, name: m[2], where: where2, ...sm ? { type: sm[1], ...sm[2] ? { main: sm[2] } : {}, at: sm[3] } : {} };
2605
+ }
2606
+ function structurePhrase(r) {
2607
+ const type = r.type ? `${r.type} ` : "";
2608
+ const main = r.main ? ` (${r.main})` : "";
2609
+ const at = r.at && r.at !== "alone" ? ` ${r.at}` : "";
2610
+ if (r.op === "added") return `New ${type}layer "${r.name}"${main}${at} ${r.where}`;
2611
+ if (r.op === "removed") return `Removed ${type}layer "${r.name}"${main}${at} ${r.where}`;
2612
+ return `Moved layer "${r.name}" ${r.at ?? ""}${r.from ? ` (was ${r.from})` : ""} ${r.where}`.replace(/\s+/g, " ");
2613
+ }
2614
+ const bare = (at) => (at ?? "").replace(/"/g, "");
2615
+ function firstChange(component, kind, c, all) {
2616
+ switch (kind) {
2617
+ case "new-component": {
2618
+ const axes = all.filter((x) => x.field.startsWith("Variant "));
2619
+ const props = all.filter((x) => x.field.startsWith("Prop "));
2620
+ const parts = [axes.length ? plural(axes.length, "variant axis", "variant axes") : "", props.length ? plural(props.length, "prop") : ""].filter(Boolean);
2621
+ return {
2622
+ title: `Add the ${component} component`,
2623
+ summary: `${component} is not in code yet.${parts.length ? ` The Figma component has ${andList(parts)}.` : ""}`
2624
+ };
2625
+ }
2626
+ case "new-variant": {
2627
+ if (!c) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2628
+ if (c.field === "Variants") {
2629
+ const gone = (c.before ?? "").split("; ").filter(Boolean);
2630
+ const born = (c.after ?? "").split("; ").filter(Boolean);
2631
+ if (gone.length === 1 && !born.length) return { title: `Remove the ${shortVariant(gone[0])} variant`, summary: `${component} no longer has the ${shortVariant(gone[0])} variant (${gone[0]}).` };
2632
+ if (born.length === 1 && !gone.length) return { title: `Add the ${shortVariant(born[0])} variant`, summary: `${component} gets a new variant, ${shortVariant(born[0])} (${born[0]}).` };
2633
+ const parts = [born.length ? `gets ${plural(born.length, "new variant")}: ${andList(born.map(shortVariant))}` : "", gone.length ? `no longer has ${andList(gone.map(shortVariant))}` : ""].filter(Boolean);
2634
+ return { title: `Update the ${component} variants`, summary: `${component} ${parts.join(", and ")}.` };
2635
+ }
2636
+ const axis = c.field.replace(/^Variant /, "");
2637
+ const before = values(c.before);
2638
+ const after = values(c.after);
2639
+ const added = after.filter((x) => !before.includes(x));
2640
+ const removed = before.filter((x) => !after.includes(x));
2641
+ const word = axis.toLowerCase();
2642
+ if (!before.length) return { title: `Add a ${axis} variant axis`, summary: `${component} gets a new variant axis, ${axis}, with ${after.length === 1 ? "the value" : "the values"} ${andList(after)}.` };
2643
+ if (!after.length) return { title: `Remove the ${axis} variant axis`, summary: `${component} no longer has the ${axis} variant axis (${andList(before)}).` };
2644
+ if (added.length === 1 && !removed.length) return { title: `Add ${article(added[0])} ${added[0]} ${word}`, summary: `${component} gets a new ${axis} value, ${added[0]}.` };
2645
+ if (added.length && !removed.length) return { title: `Add ${andList(added)} to ${axis}`, summary: `${component} gets ${plural(added.length, `new ${axis} value`)}: ${andList(added)}.` };
2646
+ if (removed.length === 1 && !added.length) return { title: `Remove the ${removed[0]} ${word}`, summary: `${component} no longer has the ${axis} value ${removed[0]}.` };
2647
+ if (removed.length && !added.length) return { title: `Remove ${andList(removed)} from ${axis}`, summary: `${component} no longer has the ${axis} values ${andList(removed)}.` };
2648
+ return { title: `Change the ${axis} values`, summary: `${axis} on ${component} goes from ${andList(before)} to ${andList(after)}.` };
2649
+ }
2650
+ case "prop-change": {
2651
+ if (!c) return { title: `Change a ${component} prop`, summary: `A prop of ${component} changed in Figma.` };
2652
+ const name = c.field.replace(/^Prop /, "");
2653
+ const b = propShape(c.before);
2654
+ const a = propShape(c.after);
2655
+ if (!b && a) return { title: `Add the ${name} prop`, summary: `${component} gets a new ${a.type} prop, ${name}${a.def ? `, default ${a.def}` : ""}.` };
2656
+ if (b && !a) return { title: `Remove the ${name} prop`, summary: `${component} no longer has the ${name} prop.` };
2657
+ if (b && a && b.type === a.type && b.def !== a.def) return { title: `Change the ${name} default to ${a.def || "empty"}`, summary: `The default of ${name} on ${component} goes from ${b.def || "empty"} to ${a.def || "empty"}.` };
2658
+ return { title: `Change the ${name} prop`, summary: `${name} on ${component} goes from ${c.before ?? "none"} to ${c.after ?? "none"}.` };
2659
+ }
2660
+ case "token-binding": {
2661
+ if (!c) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2662
+ const m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2663
+ const part = lower(m[1]);
2664
+ const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2665
+ const after = c.after && c.after !== "none" ? c.after : "";
2666
+ const before = c.before && c.before !== "none" ? c.before : "";
2667
+ if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where2} no longer uses ${before || "a token"}.` };
2668
+ return {
2669
+ title: `Use ${after} for the ${part}`,
2670
+ summary: `The ${part} of ${component}${where2} ${before ? `moves from ${before} to ${after}` : `now uses ${after}`}.`
2671
+ };
2672
+ }
2673
+ case "structure": {
2674
+ const r = c ? parseStructureChange(c) : void 0;
2675
+ if (!r) return { title: "Change the layers to match Figma", summary: `The layers of ${component} changed in Figma.` };
2676
+ if (r.op === "added") {
2677
+ const at = r.at && r.at !== "alone" ? ` ${bare(r.at)}` : "";
2678
+ return { title: `Add a layer${at}`, summary: `${component} gets ${article(r.type ?? "layer")} ${r.type ?? "layer"}${r.type ? " layer" : ""}, ${r.name}${r.main ? ` (${r.main})` : ""}${at} ${r.where}.` };
2679
+ }
2680
+ if (r.op === "removed") return { title: `Remove the ${r.name} layer`, summary: `${component} no longer has the ${r.name} layer ${r.where}.` };
2681
+ return { title: `Move the ${r.name} layer`, summary: `The ${r.name} layer of ${component} moves ${bare(r.at) || "elsewhere"}${r.from ? ` (it was ${bare(r.from)})` : ""} ${r.where}.` };
2682
+ }
2683
+ case "visual":
2684
+ return {
2685
+ title: "Update the look to match Figma",
2686
+ summary: `${component} looks different in Figma${c?.field.startsWith("Look of ") && c.field !== "Look of every variant" ? ` in ${c.field.slice(8)}` : ""}, and no variable edit explains it.`
2687
+ };
2688
+ default:
2689
+ return { title: "", summary: "" };
2659
2690
  }
2660
2691
  }
2661
- const labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.name ?? "");
2662
- function isExplorationPull(p) {
2663
- return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
2664
- }
2665
- function isHandedOff(p) {
2666
- return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
2692
+ function draftRequestText(component, kind, changes) {
2693
+ const name = component.trim() || "the component";
2694
+ if (!changes.length && kind !== "new-component") return { title: "", summary: "" };
2695
+ const d = firstChange(name, kind, changes[0], changes);
2696
+ const rest = kind === "new-component" ? 0 : changes.length - 1;
2697
+ const summary = rest > 0 ? `${d.summary} ${capital(plural(rest, "other change"))} ${rest === 1 ? "is" : "are"} listed below.` : d.summary;
2698
+ return { title: cleanDraftText(d.title), summary: cleanDraftText(summary) };
2667
2699
  }
2668
- function explorationStatus(p) {
2669
- const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
2670
- if (p.state !== "open" && !merged) return "closed";
2671
- if (p.state === "open" && labelNames(p.labels).includes(EXPLORATION_LABEL)) return "active";
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";
2700
+ const DRAFT_LINE_MAX = 120;
2701
+ function clipText(text, max) {
2702
+ const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
2703
+ if (t.length <= max) return t;
2704
+ const cut = t.slice(0, max + 1);
2705
+ const at = cut.lastIndexOf(" ");
2706
+ return (at > max * 0.5 ? cut.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
2678
2707
  }
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;
2708
+ function sanitizeDraftLine(raw) {
2709
+ if (typeof raw !== "string") return "";
2710
+ return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
2686
2711
  }
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;
2712
+ function changePhrase(c) {
2713
+ let m;
2714
+ if (c.field === "Variants") {
2715
+ const gone = (c.before ?? "").split("; ").filter(Boolean).map(shortVariant);
2716
+ const born = (c.after ?? "").split("; ").filter(Boolean).map(shortVariant);
2717
+ return [born.length ? `New variant${born.length === 1 ? "" : "s"} ${andList(born)}` : "", gone.length ? `${andList(gone)} variant${gone.length === 1 ? "" : "s"} removed` : ""].filter(Boolean).join("; ");
2702
2718
  }
2719
+ if (m = /^Variant (.+)$/.exec(c.field)) {
2720
+ const axis = m[1];
2721
+ const before2 = values(c.before);
2722
+ const after2 = values(c.after);
2723
+ if (!before2.length) return `New ${axis} variant axis: ${after2.join(", ")}`;
2724
+ if (!after2.length) return `${axis} variant axis removed (${before2.join(", ")})`;
2725
+ const added = after2.filter((x) => !before2.includes(x));
2726
+ const removed = before2.filter((x) => !after2.includes(x));
2727
+ const parts = [added.length ? `${andList(added)} added to ${axis}` : "", removed.length ? `${andList(removed)} removed from ${axis}` : ""].filter(Boolean);
2728
+ return parts.join("; ") || `${axis} values reordered: ${after2.join(", ")}`;
2729
+ }
2730
+ if (m = /^Prop (.+)$/.exec(c.field)) {
2731
+ const name = m[1];
2732
+ const b = propShape(c.before);
2733
+ const a = propShape(c.after);
2734
+ if (!b && a) return `New ${a.type} prop ${name}${a.def ? `, default ${a.def}` : ""}`;
2735
+ if (b && !a) return `${name} prop removed`;
2736
+ if (b && a && b.type === a.type) return `${name} default goes from ${b.def || "empty"} to ${a.def || "empty"}`;
2737
+ return `${name} prop goes from ${c.before ?? "none"} to ${c.after ?? "none"}`;
2738
+ }
2739
+ if (m = /^Swapped instance(?: \((.*)\))?$/.exec(c.field)) return `Shows ${c.after ?? "another instance"} instead of ${c.before ?? "the coded one"} ${m[1] ? `in ${m[1]}` : "in every variant"}`;
2740
+ if (m = /^Look of (.+)$/.exec(c.field)) return `Looks different in ${m[1]}; no variable explains it`;
2741
+ const layer = parseStructureChange(c);
2742
+ if (layer) return structurePhrase(layer);
2743
+ m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
2744
+ const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
2745
+ const before = c.before && c.before !== "none" ? c.before : "";
2746
+ const after = c.after && c.after !== "none" ? c.after : "";
2747
+ if (!after) return `${m[1]} no longer uses ${before || "a token"}${where2}`;
2748
+ if (!before) return `${m[1]} now uses ${after}${where2}`;
2749
+ return `${m[1]} uses ${after} instead of ${before}${where2}`;
2703
2750
  }
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();
2751
+ function draftChangeLine(kind, changes) {
2752
+ if (kind === "new-component") {
2753
+ const axes = changes.filter((x) => x.field.startsWith("Variant ")).length;
2754
+ const props = changes.filter((x) => x.field.startsWith("Prop ")).length;
2755
+ const parts = [axes ? plural(axes, "variant axis", "variant axes") : "", props ? plural(props, "prop") : ""].filter(Boolean);
2756
+ return `New component${parts.length ? ` with ${andList(parts)}` : ""}, not in code yet`;
2757
+ }
2758
+ if (!changes.length) return "";
2759
+ const first = changePhrase(changes[0]);
2760
+ const rest = changes.length - 1;
2761
+ const tail = rest ? ` (and ${rest} more)` : "";
2762
+ return capital(clipText(cleanDraftText(first), DRAFT_LINE_MAX - tail.length)) + tail;
2742
2763
  }
2743
- function promotionPlan(input) {
2744
- const exploration = parseExplorationBody(input.body);
2745
- const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
2746
- const title = jira ? input.title.slice(jira.length).trim() : input.title;
2747
- const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
2748
- const files = input.files.filter((f) => !f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`));
2749
- const risk = classifyRisk({ files, ...input.config ? { config: input.config } : {} });
2750
- const tokens = files.some((f) => f.path.endsWith(".tokens.json"));
2751
- const addLabels = [`tier: ${risk.tier}`, ...risk.tier === 3 && tokens ? ["tokens"] : []];
2752
- const date = (input.date ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2753
- const existing = new Set(input.body ? [...input.body.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi)].map((m) => Number(m[1])) : []);
2754
- const closes = [...new Set(input.requests ?? [])].filter((n) => !existing.has(n));
2755
- const kept = (input.body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/^## Exploration: /m, "## ").trim();
2756
- const body = [
2757
- `**Promoted from an exploration on ${date}.** Review it like any change: the tier below comes from the files it touches.`,
2758
- "",
2759
- kept || `## ${title.replace(/^Exploration:\s*/i, "")}`,
2760
- "",
2761
- `**Risk tier:** ${risk.tier}.`,
2762
- ...risk.reasons.slice(0, 8).map((r) => `- ${r}`),
2763
- ...closes.length ? ["", ...closes.map((n) => `Closes #${n}`)] : []
2764
- ].join("\n");
2765
- const t = title.replace(/^Exploration:\s*/i, "").trim() || exploration?.name || title;
2766
- return {
2767
- tier: risk.tier,
2768
- reasons: risk.reasons,
2769
- removeLabels: [EXPLORATION_LABEL],
2770
- addLabels,
2771
- removeFiles,
2772
- body,
2773
- title: `${jira ? `${jira} ` : ""}${t.charAt(0).toUpperCase()}${t.slice(1)}`,
2774
- markReady: true
2775
- };
2764
+ const tokenOf = (e) => e.figma ?? e.head ?? e.base;
2765
+ const pathOf = (e) => tokenOf(e)?.path ?? e.path;
2766
+ function pushChangeRows(entries) {
2767
+ return entries.map((e) => {
2768
+ const before = e.head ?? e.base;
2769
+ const after = e.figma;
2770
+ const show2 = (t) => {
2771
+ if (!t) return void 0;
2772
+ const modes = Object.keys(t.values);
2773
+ if (modes.length <= 1) return formatValue(t.type, t.values[modes[0] ?? ""]);
2774
+ return modes.map((m) => `${modeDisplayName(m)} ${formatValue(t.type, t.values[m])}`).join("; ");
2775
+ };
2776
+ const renamed = e.local?.kind === "renamed" && before && after && before.path !== after.path;
2777
+ return { field: renamed ? `${before.path} -> ${after.path}` : pathOf(e), before: show2(before), after: show2(after) };
2778
+ });
2776
2779
  }
2777
- function explorationTryIt(input) {
2778
- const c = readPreviewComment(input.comment);
2779
- if (!c) return { state: "building" };
2780
- if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
2781
- if (!c.command || c.source === "none") return { state: "failed" };
2782
- return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
2780
+ function names(paths) {
2781
+ if (paths.length <= 3) return andList(paths);
2782
+ return `${paths.length} tokens, including ${andList(paths.slice(0, 2))}`;
2783
2783
  }
2784
- function explorationShareMessage(input) {
2785
- const name = input.name.trim().replace(/"/g, "'");
2786
- const head = input.command ? `Exploration "${name}" (${input.branch}): try it in a staging branch of your product with: ${input.command}.` : `Exploration "${name}" (${input.branch}): its test build is being prepared, the install command for a staging branch of your product will be on the pull request in a few minutes.`;
2787
- const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
2788
- const local = !previews.length && input.local ? `Try it locally: ${input.local}, then start Storybook or the app with its dev command.` : "";
2789
- return [head, previews.length ? `Previews: ${previews.join(", ")}.` : local, `Pull request: ${input.url}.`].filter(Boolean).join(" ");
2784
+ function valueModes(e) {
2785
+ return (e.local?.fields ?? []).filter((f) => f.field === "value" && f.mode && f.mode !== "value").map((f) => modeDisplayName(f.mode));
2790
2786
  }
2791
- function explorationRules(branch) {
2792
- return [
2793
- `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
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.`
2796
- ];
2787
+ function draftPushDescription(entries, components = []) {
2788
+ const by = { changed: [], added: [], renamed: [], removed: [] };
2789
+ for (const e of entries) by[e.local?.kind ?? "changed"].push(e);
2790
+ const clauses = [];
2791
+ if (by.changed.length) {
2792
+ const only = by.changed.length === 1 ? by.changed[0] : void 0;
2793
+ const fields = new Set((only?.local?.fields ?? []).map((f) => f.field));
2794
+ const modes = only ? valueModes(only) : [];
2795
+ if (only && fields.size === 1 && fields.has("description")) clauses.push(`updates the description of ${pathOf(only)}`);
2796
+ else if (only && modes.length) clauses.push(`changes ${pathOf(only)} in ${andList(modes)}`);
2797
+ else clauses.push(`changes ${names(by.changed.map(pathOf))}`);
2798
+ }
2799
+ if (by.added.length) clauses.push(`adds ${names(by.added.map(pathOf))}`);
2800
+ if (by.renamed.length) {
2801
+ const r = by.renamed[0];
2802
+ const from = (r.head ?? r.base)?.path;
2803
+ clauses.push(by.renamed.length === 1 && from && from !== pathOf(r) ? `renames ${from} to ${pathOf(r)}` : `renames ${names(by.renamed.map(pathOf))}`);
2804
+ }
2805
+ if (by.removed.length) clauses.push(`removes ${names(by.removed.map(pathOf))}`);
2806
+ const sentences = [];
2807
+ if (clauses.length) sentences.push(`${capital(andList(clauses))}.`);
2808
+ const fresh = components.filter((c) => c.kind === "new-component").map((c) => c.component);
2809
+ const changed = [...new Set(components.filter((c) => c.kind !== "new-component").map((c) => c.component))];
2810
+ const comp = [];
2811
+ if (changed.length) comp.push(`${changed.length === 1 ? "a change" : "changes"} to ${andList(changed)}`);
2812
+ if (fresh.length) comp.push(`the new ${andList(fresh)} component${fresh.length === 1 ? "" : "s"}`);
2813
+ if (comp.length) sentences.push(`Requests ${andList(comp)}.`);
2814
+ return cleanDraftText(sentences.join(" "));
2797
2815
  }
2798
2816
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2799
2817
  function properWord(word, names2) {
@@ -4583,11 +4601,13 @@ function isPlatformRefusal(s) {
4583
4601
  return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
4584
4602
  }
4585
4603
  const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
4586
- function summarizeCi(runs, statuses = []) {
4604
+ function summarizeCi(runs, statuses = [], opts = {}) {
4587
4605
  const failed = [];
4588
4606
  const pending = [];
4589
4607
  let passed = 0;
4608
+ const skip = (name) => Boolean(opts.exploration) && isExplorationCheck(name);
4590
4609
  for (const r of runs) {
4610
+ if (skip(r.name)) continue;
4591
4611
  if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
4592
4612
  else if (r.conclusion && OK.has(r.conclusion)) passed++;
4593
4613
  else failed.push({ name: r.name, url: r.html_url });
@@ -4596,7 +4616,7 @@ function summarizeCi(runs, statuses = []) {
4596
4616
  for (const s of statuses) {
4597
4617
  if (seen.has(s.context)) continue;
4598
4618
  seen.add(s.context);
4599
- if (isPlatformRefusal(s)) continue;
4619
+ if (isPlatformRefusal(s) || skip(s.context)) continue;
4600
4620
  if (s.state === "success") passed++;
4601
4621
  else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
4602
4622
  else failed.push({ name: s.context, url: s.target_url ?? void 0 });
@@ -5904,7 +5924,7 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
5904
5924
  return out;
5905
5925
  }
5906
5926
  export {
5907
- overrideComment as $,
5927
+ COLOR_FIELDS as $,
5908
5928
  explorationNote as A,
5909
5929
  explorationNotePath as B,
5910
5930
  explorationPrTitle as C,
@@ -5918,323 +5938,326 @@ export {
5918
5938
  EXPLORATION_NOTES_DIR as K,
5919
5939
  isExplorationBranch as L,
5920
5940
  doneExploringPlan as M,
5921
- withDoneExploring as N,
5922
- explorationStartingPoint as O,
5941
+ isGuardedExploration as N,
5942
+ withDoneExploring as O,
5923
5943
  PROSE_KEYS as P,
5924
- explorationRefLabel as Q,
5944
+ explorationStartingPoint as Q,
5925
5945
  REQUESTS_BRANCH as R,
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 _,
5946
+ explorationRefLabel as S,
5947
+ EXPLORATION_STATUS_LABEL as T,
5948
+ compareSpecToMeasured as U,
5949
+ variantKeyOf as V,
5950
+ matchVariant as W,
5951
+ withoutAddedWrappers as X,
5952
+ pairParts as Y,
5953
+ hashSpec as Z,
5954
+ SPEC_FIELDS as _,
5935
5955
  parseChangelog as a,
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,
5956
+ EXPLORATION_STARTING_POINT as a$,
5957
+ DRAFTED_BY_CLAUDE_LABEL as a0,
5958
+ overrideComment as a1,
5959
+ OVERRIDE_HEADING as a2,
5960
+ overrideRefusedComment as a3,
5961
+ diffLayered as a4,
5962
+ contrastDelta as a5,
5963
+ runLayeredContrast as a6,
5964
+ layeredImpact as a7,
5965
+ classifyLayered as a8,
5966
+ composeProposalBody as a9,
5967
+ isUnder as aA,
5968
+ componentMotion as aB,
5969
+ GitHubClient as aC,
5970
+ effectiveTier as aD,
5971
+ parseRequestIssue as aE,
5972
+ requestStatus as aF,
5973
+ listSuggestionsOnGitHub as aG,
5974
+ getSuggestionOnGitHub as aH,
5975
+ closingIssueNumbers as aI,
5976
+ requestIssueTitle as aJ,
5977
+ pullState as aK,
5978
+ summarizeCi as aL,
5979
+ summarizeApprovals as aM,
5980
+ BULK_LIMIT as aN,
5981
+ CI_STATE_LABEL as aO,
5982
+ COMPONENT_SPECS_VERSION as aP,
5983
+ CONFLICT_REASON as aQ,
5984
+ DEFAULT_COMPONENT_SPECS_FILE as aR,
5985
+ DEFAULT_PROTECTED_BRANCHES as aS,
5986
+ DOCS_CHANGE_BUMP as aT,
5987
+ DOCS_CLI_ORIGIN as aU,
5988
+ DOCS_EDITOR_ORIGIN as aV,
5989
+ DOCS_KEYS as aW,
5990
+ DOCS_MCP_ORIGIN as aX,
5991
+ EXPLORATION_CHECK as aY,
5992
+ EXPLORATION_READY_LABEL as aZ,
5993
+ EXPLORATION_SLUG_MAX as a_,
5994
+ createSuggestionOnGitHub as aa,
5995
+ gitBlobSha as ab,
5996
+ SUGGESTION_SCHEMA as ac,
5997
+ branchWithJiraKey as ad,
5998
+ titleWithJiraKey as ae,
5999
+ figmaSpecPath as af,
6000
+ formatFigmaSpec as ag,
6001
+ figmaDraftBrief as ah,
6002
+ parseFigmaOrigin as ai,
6003
+ figmaPushStatus as aj,
6004
+ parseFigmaSpec as ak,
6005
+ FIGMA_SPEC_DIR as al,
6006
+ FROM_FIGMA_LABEL as am,
6007
+ withFigmaOrigin as an,
6008
+ WAITING_FOR_CLAUDE_LABEL as ao,
6009
+ planComponentPush as ap,
6010
+ figmaOriginSection as aq,
6011
+ figmaBranchTitle as ar,
6012
+ figmaSpecSlug as as,
6013
+ figmaDraftBody as at,
6014
+ REQUEST_LABEL as au,
6015
+ figmaNodeUrl as av,
6016
+ parentDir as aw,
6017
+ parseComponentSpecs as ax,
6018
+ readRepoTexts as ay,
6019
+ normalizeRepoPath as az,
6000
6020
  buildReport as b,
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,
6021
+ cssColorsToRgb as b$,
6022
+ EXPLORATION_TIER_LABEL as b0,
6023
+ EXPLORATION_USE_CASES as b1,
6024
+ FORMER_MEMBER as b2,
6025
+ LOOK_AGAIN_STATUS_LABEL as b3,
6026
+ MeasuredSpecsError as b4,
6027
+ PULL_STATE_PILL as b5,
6028
+ ProtectedBranchError as b6,
6029
+ REDUCED_MOTION_NOTE as b7,
6030
+ REQUEST_STATUS_LABEL as b8,
6031
+ REVIEW_STATUS_LABEL as b9,
6032
+ assertSafeBranch as bA,
6033
+ assignRequestOnGitHub as bB,
6034
+ assignVersions as bC,
6035
+ base64ToUtf8 as bD,
6036
+ branchVerdict as bE,
6037
+ bridgeMcpInstallCommand as bF,
6038
+ canOverride as bG,
6039
+ canonicalJson as bH,
6040
+ changeTable as bI,
6041
+ changesetBump as bJ,
6042
+ classifyAuth as bK,
6043
+ claudeStage as bL,
6044
+ cleanDocs as bM,
6045
+ clearSharedEtags as bN,
6046
+ commitFileOnGitHub as bO,
6047
+ componentContrastPairs as bP,
6048
+ componentsUsingMotionToken as bQ,
6049
+ composeChangeset as bR,
6050
+ composeSuggestionAppliedComment as bS,
6051
+ composeSuggestionIssue as bT,
6052
+ composeSuggestionOverrideComment as bU,
6053
+ composeSuggestionReviewComment as bV,
6054
+ continueDocsProposalOnGitHub as bW,
6055
+ contrastDeltaTable as bX,
6056
+ countChanges as bY,
6057
+ coverageSummary as bZ,
6058
+ cssColorToRgb as b_,
6059
+ REVIEW_STATUS_ORDER as ba,
6060
+ SUGGESTION_APPLIED_MARK as bb,
6061
+ SUGGESTION_APPLY_EXIT as bc,
6062
+ SUGGESTION_FINAL_LABELS as bd,
6063
+ SUGGESTION_KINDS as be,
6064
+ SUGGESTION_LABEL as bf,
6065
+ SUGGESTION_LIMITS as bg,
6066
+ SUGGESTION_OVERRIDE_MARK as bh,
6067
+ SUGGESTION_REVIEW_MARK as bi,
6068
+ SUGGESTION_STATE_LABELS as bj,
6069
+ SUGGESTION_STATUS_LABEL as bk,
6070
+ SUGGESTION_STATUS_LABELS as bl,
6071
+ SUGGESTION_TOO_LARGE as bm,
6072
+ StorybookIndexError as bn,
6073
+ SuggestionError as bo,
6074
+ TOKEN_REJECTED_MESSAGE as bp,
6075
+ TOKEN_SETTINGS_URL as bq,
6076
+ TRIGGER_LABELS as br,
6077
+ UNRELEASED as bs,
6078
+ acceptDocsDraft as bt,
6079
+ acceptDocsText as bu,
6080
+ allowedMergeMethods as bv,
6081
+ anatomyLabel as bw,
6082
+ appliedStatus as bx,
6083
+ approvedSuggestionNumber as by,
6084
+ approvedSuggestionOnGitHub as bz,
6065
6085
  collectHistory as c,
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,
6086
+ markUsedTokens as c$,
6087
+ cssModuleClasses as c0,
6088
+ declineComment as c1,
6089
+ declineReasonProblems as c2,
6090
+ declineRequestOnGitHub as c3,
6091
+ declineSuggestionOnGitHub as c4,
6092
+ defaultDocsFooter as c5,
6093
+ docsAuthorLine as c6,
6094
+ docsFileText as c7,
6095
+ docsProposal as c8,
6096
+ docsSectionShapes as c9,
6097
+ hasWriteAccess as cA,
6098
+ iconComponentName as cB,
6099
+ installationFor as cC,
6100
+ isActiveExploration as cD,
6101
+ isAutomationPull as cE,
6102
+ isDocsProposalBranch as cF,
6103
+ isDraft as cG,
6104
+ isExplorationCheck as cH,
6105
+ isGuardrailPath as cI,
6106
+ isHandedOff as cJ,
6107
+ isHeldExploration as cK,
6108
+ isJiraKey as cL,
6109
+ isMissingPermission as cM,
6110
+ isPlatformRefusal as cN,
6111
+ isReleasePull as cO,
6112
+ isTokenRejected as cP,
6113
+ isUnlabeledExploration as cQ,
6114
+ jiraIssueFields as cR,
6115
+ jiraKeyOf as cS,
6116
+ joinRepoPath as cT,
6117
+ linkRequestToExplorationOnGitHub as cU,
6118
+ linkSuggestionPrOnGitHub as cV,
6119
+ linkedPullsOf as cW,
6120
+ loadRepoFiles as cX,
6121
+ lookAgainReviewers as cY,
6122
+ lowerFirstWord as cZ,
6123
+ markSuggestionsAppliedOnGitHub as c_,
6124
+ docsSlug as ca,
6125
+ docsSuggestionPayload as cb,
6126
+ docsText as cc,
6127
+ draftChangeLine as cd,
6128
+ draftPushDescription as ce,
6129
+ draftRequestText as cf,
6130
+ eligibleReviewers as cg,
6131
+ evalCalc as ch,
6132
+ explorationBranch as ci,
6133
+ explorationRules as cj,
6134
+ explorationShareMessage as ck,
6135
+ explorationSlugFromBranch as cl,
6136
+ explorationTakeCommands as cm,
6137
+ fetchStorybookIndex as cn,
6138
+ figmaCell as co,
6139
+ fileModes as cp,
6140
+ filterHistory as cq,
6141
+ findOpenDocsProposal as cr,
6142
+ findOpenDocsProposalOnGitHub as cs,
6143
+ firstParagraph as ct,
6144
+ formatDocsJson as cu,
6145
+ friendlyMessage as cv,
6146
+ githubSuggestionWorkspace as cw,
6147
+ groupStorybookComponents as cx,
6148
+ hasOwnerApproval as cy,
6149
+ hasSuggestionBlock as cz,
6130
6150
  applyTokenEdits as d,
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,
6151
+ stalenessStatus as d$,
6152
+ memoryEtagStore as d0,
6153
+ memorySource as d1,
6154
+ mergeGate as d2,
6155
+ missingPermissionMessage as d3,
6156
+ missingProse as d4,
6157
+ modeLabel as d5,
6158
+ needsDocsReview as d6,
6159
+ normalizeStorybookUrl as d7,
6160
+ openPulls as d8,
6161
+ overrideHolders as d9,
6162
+ prNumberFromSubject as dA,
6163
+ prefillDocs as dB,
6164
+ proposeDocsOnGitHub as dC,
6165
+ propsTable as dD,
6166
+ pushChangeRows as dE,
6167
+ rateLimitMessage as dF,
6168
+ releasePull as dG,
6169
+ reopenEligibility as dH,
6170
+ requestBrief as dI,
6171
+ requestImagePath as dJ,
6172
+ requesterFirstName as dK,
6173
+ requesterLine as dL,
6174
+ requesterOf as dM,
6175
+ requiredApprovals as dN,
6176
+ requiredPermission as dO,
6177
+ reviewQueue as dP,
6178
+ reviewStatusKind as dQ,
6179
+ reviewStatusLabel as dR,
6180
+ reviewSuggestionOnGitHub as dS,
6181
+ reviewersToAsk as dT,
6182
+ sectionsNeedingReview as dU,
6183
+ sha256Hex as dV,
6184
+ sharedTokenPrefix as dW,
6185
+ shortVariant as dX,
6186
+ shortVariants as dY,
6187
+ specHash as dZ,
6188
+ specsUsage as d_,
6189
+ overrideReasonMin as da,
6190
+ overrideReasonProblem as db,
6191
+ overrideSuggestionOnGitHub as dc,
6192
+ ownerOverridePolicy as dd,
6193
+ parseBoxShadow as de,
6194
+ parseChangesetText as df,
6195
+ parseExplorationRef as dg,
6196
+ parseJiraKey as dh,
6197
+ parseMeasuredSpecsFile as di,
6198
+ parseOverrideComment as dj,
6199
+ parseRepo as dk,
6200
+ parseStorybookIndex as dl,
6201
+ parseStructureChange as dm,
6202
+ parseSuggestionAppliedComment as dn,
6203
+ parseSuggestionIssue as dp,
6204
+ parseSuggestionOverrideComment as dq,
6205
+ parseSuggestionReviewComment as dr,
6206
+ parseTier as ds,
6207
+ patchDocsJson as dt,
6208
+ pendingApprovals as du,
6209
+ permissionLabel as dv,
6210
+ planRestore as dw,
6211
+ planRevert as dx,
6212
+ planSuggestionApply as dy,
6213
+ planToJson as dz,
6194
6214
  buildHistoryEntry as e,
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,
6215
+ storybookIframeUrl as e0,
6216
+ storybookIndexUrls as e1,
6217
+ storybookManagerUrl as e2,
6218
+ subjectTitle as e3,
6219
+ suggestionAllowedRoots as e4,
6220
+ suggestionAppliedComment as e5,
6221
+ suggestionApplyTable as e6,
6222
+ suggestionIsTokenValues as e7,
6223
+ suggestionIssueTitle as e8,
6224
+ suggestionKindFromLabels as e9,
6225
+ versionTimeline as eA,
6226
+ webStorageEtagStore as eB,
6227
+ withAuthorLine as eC,
6228
+ withFigmaPushApprovals as eD,
6229
+ withOutdated as eE,
6230
+ withRequestExploration as eF,
6231
+ withSrgbColors as eG,
6232
+ withSuggestionApprovals as eH,
6233
+ withTokenValueApprovals as eI,
6234
+ withoutJiraKey as eJ,
6235
+ suggestionLabelChanges as ea,
6236
+ suggestionLabels as eb,
6237
+ suggestionOverrideOf as ec,
6238
+ suggestionOverrideProblem as ed,
6239
+ suggestionPathProblems as ee,
6240
+ suggestionPayloadSha as ef,
6241
+ suggestionReviewState as eg,
6242
+ suggestionShapeProblems as eh,
6243
+ suggestionStalenessOnGitHub as ei,
6244
+ suggestionStateLabel as ej,
6245
+ suggestionSubject as ek,
6246
+ suggestionTouches as el,
6247
+ summarizeSuggestionApprovals as em,
6248
+ syncSuggestionStateLabel as en,
6249
+ titleOf as eo,
6250
+ toChangeSummary as ep,
6251
+ tokenCssVar as eq,
6252
+ tokenFingerprint as er,
6253
+ tokenHistory as es,
6254
+ tokenVariablePath as et,
6255
+ unsafePathProblem as eu,
6256
+ utf8ToBase64 as ev,
6257
+ validateComponentSpec as ew,
6258
+ validateDocs as ex,
6259
+ validateIntegrations as ey,
6260
+ variantName as ez,
6238
6261
  coerceValue as f,
6239
6262
  checkLayeredPair as g,
6240
6263
  formatChangeset as h,
@@ -6253,8 +6276,8 @@ export {
6253
6276
  explorationStatus as u,
6254
6277
  explorationTryIt as v,
6255
6278
  isExplorationPull as w,
6256
- promotionPlan as x,
6257
- EXPLORATION_LABEL as y,
6279
+ EXPLORATION_LABEL as x,
6280
+ promotionPlan as y,
6258
6281
  explorationProblems as z
6259
6282
  };
6260
- //# sourceMappingURL=repo-source-CcWmqNBZ.js.map
6283
+ //# sourceMappingURL=repo-source-vRf_Jfbz.js.map