@bridge-ds/core 0.2.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as modeDisplayName, o as modesOf$1, q as isMultiMode, T as TIER_NAMES, s as TIER_AXIS, u as findReferenceProblems, w as evaluatePairs, x as contrastScope, y as contextLabel, S as SINGLE_MODE, z as pathId, A as removeToken, B as moveToken, D as effectiveScopes, E as sameScopes, F as defaultScopes, G as readTokenAt, H as upsertToken, I as serializeTokenFile, J as emptyTokenFile, K as parseTokenFile, L as modeFile, M as defaultFile, N as EXT_SCOPES, O as EXT_ID, P as DEFAULT_PROTECTED_BRANCHES, a as assertSafeBranch, Q as ProtectedBranchError, f as CONFIG_FILE, l as loadConfig, R as ConfigError, U as DEFAULT_TOKEN_PATHS, c as collectHistory, p as packageDirCandidates, b as parseChangelog, r as releasesFromTags, m as mergeReleases, d as buildReport, V as applyTokenEdits, W as buildHistoryEntry, X as tokensPath, Y as coerceValue, Z as toConfigJson, _ as parseContrastChecks, $ as checkLayeredPair, a0 as classifyRisk, a1 as formatChangeset, a2 as RISK_LABELS, a3 as parseJson, a4 as serializeJson, a5 as plainEqual, a6 as toPlain, a7 as getEntry, a8 as fromPlain, a9 as snapshotUrl, aa as tarballUrl, ab as previewLinks, ac as slugify, ad as installCommand } from "./component-spec-B0V_D3gM.js";
2
- import { ae, af, ag, k, C, ah, ai, aj, ak, al, am, an, ao, ap, aq, ar, as, at, au, av, aw, ax, ay, az, aA, i, aB, aC, aD, aE, aF, aG, aH, aI, aJ, aK, aL, aM, aN, aO, aP, aQ, aR, aS, aT, aU, aV, aW, aX, aY, aZ, a_, a$, b0, b1, b2, b3, h, j, b4, b5, b6, b7, b8, b9, g, ba, bb, bc, bd, be, bf, bg, bh, bi, bj, e, bk, bl, bm, bn, bo, bp, bq, br, bs, bt, bu, bv, bw, bx, by, t, bz, bA, bB, v, bC, bD } from "./component-spec-B0V_D3gM.js";
1
+ import { n as modeDisplayName, o as modesOf$1, q as isMultiMode, T as TIER_NAMES, s as TIER_AXIS, u as findReferenceProblems, w as evaluatePairs, x as contrastScope, y as contextLabel, S as SINGLE_MODE, z as pathId, A as removeToken, B as moveToken, D as effectiveScopes, E as sameScopes, F as defaultScopes, G as readTokenAt, H as upsertToken, I as serializeTokenFile, J as emptyTokenFile, K as parseTokenFile, L as modeFile, M as defaultFile, N as EXT_SCOPES, O as EXT_ID, P as DEFAULT_PROTECTED_BRANCHES, a as assertSafeBranch, Q as ProtectedBranchError, f as CONFIG_FILE, l as loadConfig, R as ConfigError, U as DEFAULT_TOKEN_PATHS, c as collectHistory, p as packageDirCandidates, b as parseChangelog, r as releasesFromTags, m as mergeReleases, d as buildReport, V as applyTokenEdits, W as buildHistoryEntry, X as resolveBranchPreviews, Y as tokensPath, Z as coerceValue, _ as toConfigJson, $ as parseContrastChecks, a0 as checkLayeredPair, a1 as classifyRisk, a2 as formatChangeset, a3 as RISK_LABELS, a4 as parseJson, a5 as serializeJson, a6 as plainEqual, a7 as toPlain, a8 as getEntry, a9 as fromPlain, aa as previewLinks, ab as slugify, ac as readPreviewComment, ad as findPreviewComment, ae as previewsSettled } from "./component-spec-IRFGJ7XK.js";
2
+ import { af, ag, ah, k, C, ai, aj, ak, al, am, an, ao, ap, aq, ar, as, at, au, av, aw, ax, ay, az, aA, aB, aC, aD, aE, aF, aG, aH, aI, aJ, aK, aL, i, aM, aN, aO, aP, aQ, aR, aS, aT, aU, aV, aW, aX, aY, aZ, a_, a$, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf, bg, bh, bi, bj, bk, bl, bm, h, bn, j, bo, bp, bq, br, bs, bt, bu, bv, bw, bx, g, by, bz, bA, bB, bC, bD, bE, bF, bG, bH, bI, bJ, bK, bL, e, bM, bN, bO, bP, bQ, bR, bS, bT, bU, bV, bW, bX, bY, bZ, b_, b$, c0, c1, c2, c3, c4, c5, c6, c7, c8, t, c9, ca, cb, cc, cd, v, ce, cf, cg, ch } from "./component-spec-IRFGJ7XK.js";
3
3
  import { a as collectRefs, i as isAlias, b as aliasTarget, v as valuesEqual, s as selectTokenFiles, L as LayeredTokenSet, d as validateLayered, f as formatValue } from "./layered-DGate4nv.js";
4
4
  import { D, e as e2, j as j2, k as k2, c, l, m, g as g2, h as h2, n, p, o, r, q, t as t2, u } from "./layered-DGate4nv.js";
5
5
  const issueKey = (i2) => `${i2.code}|${i2.path ?? ""}|${i2.mode ?? ""}|${i2.code === "file-problem" ? i2.message : ""}`;
@@ -165,12 +165,12 @@ function validateProposal(proposed, head, pairs, typeChanges = []) {
165
165
  else if (existing.has(issueKey(i2))) warnings.push({ ...i2, severity: "warning", message: `Already on main: ${i2.message}` });
166
166
  else blocking.push(i2);
167
167
  }
168
- for (const c2 of typeChanges) {
168
+ for (const c10 of typeChanges) {
169
169
  blocking.push({
170
170
  code: "type-change",
171
171
  severity: "error",
172
- path: c2.path,
173
- message: `${c2.path} changed type from ${c2.from} to ${c2.to}. Type changes are not allowed; create a new token instead.`
172
+ path: c10.path,
173
+ message: `${c10.path} changed type from ${c10.from} to ${c10.to}. Type changes are not allowed; create a new token instead.`
174
174
  });
175
175
  }
176
176
  return { blocking, warnings, contrastBefore: before.contrast, contrastAfter: after.contrast };
@@ -304,137 +304,191 @@ function composeFiles(head, originals, changes) {
304
304
  }
305
305
  return { files, allFiles };
306
306
  }
307
- const VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
308
- const IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
309
- function cssVarsIn(text) {
310
- const out = /* @__PURE__ */ new Set();
311
- for (const m2 of text.matchAll(VAR_RE)) out.add(m2[1]);
312
- return [...out];
313
- }
314
- function importsIn(text) {
315
- const out = /* @__PURE__ */ new Set();
316
- for (const m2 of text.matchAll(IMPORT_RE)) out.add(m2[1] ?? m2[2]);
317
- return [...out];
318
- }
319
- const norm = (p2) => p2.replace(/\\/g, "/").replace(/\/+$/, "");
320
- const isStory = (p2) => /\.stories\.[cm]?[jt]sx?$/.test(p2);
321
- const isTest = (p2) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p2);
322
- const isCss = (p2) => /\.(css|scss|less)$/.test(p2);
323
- const isScript = (p2) => /\.[cm]?[jt]sx?$/.test(p2);
324
- function componentOf(from, spec, componentsDir) {
325
- if (!spec.startsWith(".")) return null;
326
- const parts = from.split("/").slice(0, -1);
327
- for (const seg of spec.split("/")) {
328
- if (seg === "." || seg === "") continue;
329
- if (seg === "..") parts.pop();
330
- else parts.push(seg);
331
- }
332
- const abs = parts.join("/");
333
- if (!abs.startsWith(`${componentsDir}/`)) return null;
334
- return abs.slice(componentsDir.length + 1).split("/")[0] || null;
335
- }
336
- function componentForPath(path, componentsDir) {
337
- const dir = norm(componentsDir);
338
- const p2 = norm(path);
339
- if (!p2.startsWith(`${dir}/`)) return null;
340
- const rest = p2.slice(dir.length + 1).split("/");
341
- return rest.length >= 2 ? rest[0] : null;
342
- }
343
- function buildUsageIndex(files, componentsDir) {
344
- const dir = norm(componentsDir);
345
- const modules = {};
346
- const vars = /* @__PURE__ */ new Map();
347
- const imports = /* @__PURE__ */ new Map();
348
- const storyImports = /* @__PURE__ */ new Map();
349
- const globalVars = {};
350
- const get = (m2, k3) => m2.get(k3) ?? m2.set(k3, /* @__PURE__ */ new Set()).get(k3);
351
- for (const f of files) {
352
- const path = norm(f.path);
353
- const id = componentForPath(path, dir);
354
- if (!id) {
355
- if (isCss(path)) {
356
- const v2 = cssVarsIn(f.text);
357
- if (v2.length) globalVars[path] = v2.sort();
307
+ const PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
308
+ const protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
309
+ const ADMIN_ROLE_ID = 5;
310
+ function protectionRuleset(config, opts) {
311
+ const branch = config?.repo?.defaultBranch ?? "main";
312
+ const approvals = Math.max(1, Math.floor(Number.isFinite(opts.approvals) ? opts.approvals : 1));
313
+ const checks = [...new Set(opts.checks.map((c10) => c10.trim()).filter(Boolean))];
314
+ const rules = [
315
+ { type: "deletion" },
316
+ { type: "non_fast_forward" },
317
+ {
318
+ type: "pull_request",
319
+ parameters: {
320
+ required_approving_review_count: approvals,
321
+ dismiss_stale_reviews_on_push: true,
322
+ require_code_owner_review: false,
323
+ require_last_push_approval: false,
324
+ required_review_thread_resolution: false
358
325
  }
359
- continue;
360
- }
361
- const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
362
- mod.files.push(path);
363
- if (isTest(path)) continue;
364
- for (const v2 of cssVarsIn(f.text)) get(vars, id).add(v2);
365
- if (!isScript(path)) continue;
366
- for (const spec of importsIn(f.text)) {
367
- const dep = componentOf(path, spec, dir);
368
- if (!dep || dep === id) continue;
369
- get(isStory(path) ? storyImports : imports, id).add(dep);
370
326
  }
327
+ ];
328
+ if (checks.length) {
329
+ rules.push({
330
+ type: "required_status_checks",
331
+ parameters: {
332
+ strict_required_status_checks_policy: false,
333
+ required_status_checks: checks.map((context) => ({ context }))
334
+ }
335
+ });
371
336
  }
372
- const byVar = {};
373
- for (const mod of Object.values(modules)) {
374
- mod.files.sort();
375
- mod.vars = [...vars.get(mod.id) ?? []].sort();
376
- mod.imports = [...imports.get(mod.id) ?? []].sort();
377
- mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d) => !mod.imports.includes(d)).sort();
378
- for (const v2 of mod.vars) (byVar[v2] ??= []).push(mod.id);
379
- }
380
- for (const k3 of Object.keys(byVar)) byVar[k3].sort();
381
- return { componentsDir: dir, modules, byVar, globalVars };
382
- }
383
- function impactedModules(index, input) {
384
- const reasons = /* @__PURE__ */ new Map();
385
- const add = (id, reason, indirect) => {
386
- const r2 = reasons.get(id);
387
- if (r2) {
388
- if (!r2.reasons.includes(reason)) r2.reasons.push(reason);
389
- r2.indirect &&= indirect;
390
- } else reasons.set(id, { reasons: [reason], indirect });
337
+ return {
338
+ name: protectionRulesetName(branch),
339
+ target: "branch",
340
+ enforcement: "active",
341
+ conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
342
+ rules,
343
+ bypass_actors: opts.adminBypass ? [{ actor_id: ADMIN_ROLE_ID, actor_type: "RepositoryRole", bypass_mode: "pull_request" }] : []
391
344
  };
392
- const changedVars = new Set(input.vars ?? []);
393
- for (const v2 of changedVars) for (const id of index.byVar[v2] ?? []) add(id, `uses ${v2}`, false);
394
- for (const f of input.files ?? []) {
395
- const id = componentForPath(f, index.componentsDir);
396
- if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
397
- }
398
- const renderedBy = /* @__PURE__ */ new Map();
399
- for (const m2 of Object.values(index.modules)) for (const d of m2.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m2.id);
400
- const queue = [...reasons.keys()];
401
- while (queue.length) {
402
- const id = queue.shift();
403
- for (const parent of renderedBy.get(id) ?? []) {
404
- const known = reasons.has(parent);
405
- add(parent, `renders ${id}`, true);
406
- if (!known) queue.push(parent);
407
- }
408
- }
409
- const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v2) => changedVars.has(v2)) })).filter((g3) => g3.vars.length > 0);
410
- const modules = [...reasons.entries()].map(([id, r2]) => ({ id, reasons: r2.reasons, indirect: r2.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
411
- return { modules, globalHits };
412
345
  }
413
- function tokenUsage(tokens, reads) {
414
- const readVars = /* @__PURE__ */ new Set();
415
- for (const [v2, mods] of Object.entries(reads.byVar ?? {})) if (mods.length) readVars.add(v2);
416
- for (const vars of Object.values(reads.globalVars ?? {})) for (const v2 of vars) readVars.add(v2);
417
- const byPath = new Map(tokens.map((t3) => [t3.path, t3]));
418
- const direct = new Set(tokens.filter((t3) => readVars.has(t3.cssVar)).map((t3) => t3.path));
419
- const used = /* @__PURE__ */ new Set();
420
- const queue = [...direct];
421
- while (queue.length) {
422
- const p2 = queue.pop();
423
- if (used.has(p2)) continue;
424
- used.add(p2);
425
- for (const r2 of byPath.get(p2)?.refs ?? []) if (!used.has(r2) && byPath.has(r2)) queue.push(r2);
346
+ const isVercel = (r2) => /vercel/i.test(r2.app ?? "") || /^vercel\b/i.test(r2.name);
347
+ const ran = (r2) => r2.conclusion !== "skipped" && r2.conclusion !== "cancelled" && r2.conclusion !== "stale";
348
+ function requiredCheckNames(defaultBranchRuns, pullRequestRuns) {
349
+ const names2 = [...new Set(defaultBranchRuns.filter((r2) => !isVercel(r2) && ran(r2)).map((r2) => r2.name))];
350
+ if (!pullRequestRuns?.length) return names2;
351
+ const onPulls = new Set(pullRequestRuns.filter((r2) => !isVercel(r2)).map((r2) => r2.name));
352
+ return names2.filter((n2) => onPulls.has(n2));
353
+ }
354
+ function refPattern(pattern) {
355
+ const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
356
+ return new RegExp(`^${src}$`);
357
+ }
358
+ function rulesetTargetsDefault(ruleset, defaultBranch) {
359
+ if (ruleset.target && ruleset.target !== "branch") return false;
360
+ const ref = `refs/heads/${defaultBranch}`;
361
+ const hit = (p2) => p2 === "~ALL" || p2 === "~DEFAULT_BRANCH" || p2 === ref || p2 === defaultBranch || refPattern(p2.startsWith("refs/") ? p2 : `refs/heads/${p2}`).test(ref);
362
+ const cond = ruleset.conditions?.ref_name;
363
+ if (!cond) return false;
364
+ if ((cond.exclude ?? []).some(hit)) return false;
365
+ return (cond.include ?? []).some(hit);
366
+ }
367
+ const count = (v2) => typeof v2 === "number" ? v2 : 0;
368
+ function rulesetParts(ruleset) {
369
+ const parts = /* @__PURE__ */ new Set();
370
+ if (ruleset.enforcement !== "active") return parts;
371
+ for (const rule of ruleset.rules ?? []) {
372
+ if (rule.type === "deletion") parts.add("deletion");
373
+ if (rule.type === "non_fast_forward") parts.add("history");
374
+ if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
375
+ if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
376
+ }
377
+ return parts;
378
+ }
379
+ function classicParts(p2) {
380
+ const parts = /* @__PURE__ */ new Set();
381
+ if (count(p2.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
382
+ if (((p2.required_status_checks?.contexts?.length ?? 0) || (p2.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
383
+ if (!p2.allow_force_pushes?.enabled) parts.add("history");
384
+ if (!p2.allow_deletions?.enabled) parts.add("deletion");
385
+ return parts;
386
+ }
387
+ const rulesetUrl = (r2) => r2.html_url ?? r2._links?.html?.href;
388
+ function protectionState(rulesets, branchProtection, defaultBranch) {
389
+ const bridge = rulesets?.find((r2) => r2.name === protectionRulesetName(defaultBranch));
390
+ const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
391
+ const covered = /* @__PURE__ */ new Set();
392
+ let url;
393
+ for (const r2 of rulesets ?? []) {
394
+ if (!rulesetTargetsDefault(r2, defaultBranch)) continue;
395
+ const parts = rulesetParts(r2);
396
+ if (parts.size && !url) url = rulesetUrl(r2);
397
+ parts.forEach((p2) => covered.add(p2));
398
+ }
399
+ if (branchProtection) classicParts(branchProtection).forEach((p2) => covered.add(p2));
400
+ const missing = PROTECTION_PARTS.filter((p2) => !covered.has(p2));
401
+ const withUrl = url ? { url } : {};
402
+ if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
403
+ const unreadable = rulesets == null || branchProtection === void 0;
404
+ if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
405
+ return { kind: "partial", missing, ...extra, ...withUrl };
406
+ }
407
+ const PROTECTION_PART_PHRASE = {
408
+ review: "changes can skip review",
409
+ checks: "checks do not have to pass",
410
+ history: "past changes can be rewritten",
411
+ deletion: "the library can be deleted"
412
+ };
413
+ const joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
414
+ function protectionSentence(state) {
415
+ switch (state.kind) {
416
+ case "protected":
417
+ return "Protected: every change goes through review.";
418
+ case "unprotected":
419
+ return "Your library can be changed without review.";
420
+ case "partial": {
421
+ const s = joinAnd(state.missing.map((p2) => PROTECTION_PART_PHRASE[p2]));
422
+ return `Partly protected: ${s}.`;
423
+ }
424
+ default:
425
+ return "Protection status unknown: GitHub did not let this token read it.";
426
426
  }
427
- return { used, direct };
428
427
  }
429
428
  class GitHubError extends Error {
430
- constructor(status, message, detail) {
429
+ constructor(status, message, detail, info = {}) {
431
430
  super(message);
432
431
  this.status = status;
433
432
  this.detail = detail;
434
433
  this.name = "GitHubError";
434
+ this.auth = info.auth;
435
+ this.permission = info.permission;
435
436
  }
436
437
  status;
437
438
  detail;
439
+ /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
440
+ auth;
441
+ /** With auth "missing-permission": the permission the call needed, when known. */
442
+ permission;
443
+ }
444
+ const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
445
+ const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
446
+ const PERMISSION_LABELS = {
447
+ issues: "Issues",
448
+ pull_requests: "Pull requests",
449
+ contents: "Contents",
450
+ checks: "Checks",
451
+ statuses: "Commit statuses",
452
+ metadata: "Metadata",
453
+ actions: "Actions",
454
+ deployments: "Deployments",
455
+ workflows: "Workflows",
456
+ administration: "Administration"
457
+ };
458
+ function permissionLabel(permission) {
459
+ return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c10) => c10.toUpperCase());
460
+ }
461
+ const TOKEN_SETTINGS_URL = "https://github.com/settings/personal-access-tokens";
462
+ const TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
463
+ function missingPermissionMessage(permission, action) {
464
+ const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
465
+ if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
466
+ return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
467
+ }
468
+ function requiredPermission(path, acceptedHeader) {
469
+ if (acceptedHeader) {
470
+ const first = acceptedHeader.split(";")[0].split(",").map((p22) => p22.trim().split("=")[0].trim()).filter((p22) => p22 && p22 !== "metadata");
471
+ if (first.length) return first[0];
472
+ }
473
+ const p2 = path.replace(/[?#].*$/, "");
474
+ if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p2)) return "issues";
475
+ if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p2)) return "pull_requests";
476
+ if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p2)) return "administration";
477
+ if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p2)) return "contents";
478
+ if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p2)) return "checks";
479
+ if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p2)) return "statuses";
480
+ if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p2)) return "deployments";
481
+ if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p2)) return "actions";
482
+ return void 0;
483
+ }
484
+ 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;
485
+ function classifyAuth(status, detail, opts = {}) {
486
+ if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
487
+ if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
488
+ if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
489
+ return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
490
+ }
491
+ return {};
438
492
  }
439
493
  const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r2) => setTimeout(r2, ms)) };
440
494
  const isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
@@ -497,7 +551,7 @@ class GitHubClient {
497
551
  }
498
552
  }
499
553
  assertToken(what) {
500
- if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`);
554
+ if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`, "no-token");
501
555
  }
502
556
  get repoPath() {
503
557
  return `/repos/${this.owner}/${this.name}`;
@@ -546,7 +600,12 @@ class GitHubClient {
546
600
  detail
547
601
  );
548
602
  }
549
- throw new GitHubError(res.status, friendlyMessage(res.status, detail), detail);
603
+ const info = classifyAuth(res.status, detail, {
604
+ path,
605
+ acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
606
+ withToken: Boolean(this.token)
607
+ });
608
+ throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
550
609
  }
551
610
  return await res.json();
552
611
  }
@@ -725,6 +784,11 @@ class GitHubClient {
725
784
  async addComment(issueNumber, body) {
726
785
  await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
727
786
  }
787
+ /** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
788
+ async listIssueComments(issueNumber) {
789
+ const r2 = await this.request("GET", `${this.repoPath}/issues/${issueNumber}/comments?per_page=100`);
790
+ return r2.map((c10) => ({ id: c10.id, body: c10.body ?? "", author: c10.user?.login ?? "unknown", updatedAt: c10.updated_at }));
791
+ }
728
792
  // --- Issues (component change requests) ---------------------------------------------------
729
793
  async createIssue(input) {
730
794
  const r2 = await this.request("POST", `${this.repoPath}/issues`, {
@@ -745,6 +809,67 @@ class GitHubClient {
745
809
  async getIssue(n2) {
746
810
  return toIssue(await this.request("GET", `${this.repoPath}/issues/${n2}`));
747
811
  }
812
+ /** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
813
+ async updateIssue(n2, patch) {
814
+ this.assertToken("Editing an issue");
815
+ return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n2}`, patch));
816
+ }
817
+ /** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
818
+ async addAssignees(n2, logins) {
819
+ this.assertToken("Assigning an issue");
820
+ const r2 = await this.request("POST", `${this.repoPath}/issues/${n2}/assignees`, { assignees: logins });
821
+ return (r2.assignees ?? []).map((a) => a.login);
822
+ }
823
+ /** People with at least `permission` on the repository (reading the list needs push access). First 100. */
824
+ async listCollaborators(permission = "push") {
825
+ const r2 = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
826
+ return r2.map((c10) => ({ login: c10.login, avatarUrl: c10.avatar_url ?? "" }));
827
+ }
828
+ /** Whether a token is sent with each request (TeamReader). */
829
+ get hasToken() {
830
+ return Boolean(this.token);
831
+ }
832
+ /** Every page of a list endpoint (per_page 100), up to `maxPages`. */
833
+ async paginate(path, maxPages = 20) {
834
+ const out = [];
835
+ const sep = path.includes("?") ? "&" : "?";
836
+ for (let page = 1; page <= maxPages; page++) {
837
+ const r2 = await this.request("GET", `${path}${sep}per_page=100&page=${page}`);
838
+ out.push(...r2);
839
+ if (r2.length < 100) break;
840
+ }
841
+ return out;
842
+ }
843
+ /** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
844
+ async listTeamMembers(org, slug) {
845
+ const r2 = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members`);
846
+ return r2.map((m2) => m2.login);
847
+ }
848
+ /** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
849
+ async getTeamMembership(org, slug, login) {
850
+ try {
851
+ const r2 = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/memberships/${encodeURIComponent(login)}`);
852
+ return r2.state === "active" ? "active" : "pending";
853
+ } catch (err) {
854
+ if (err instanceof GitHubError && err.status === 404) return null;
855
+ throw err;
856
+ }
857
+ }
858
+ /** "User" or "Organization" for a login, null when GitHub does not know it. */
859
+ async getAccountType(login) {
860
+ try {
861
+ const r2 = await this.request("GET", `/users/${encodeURIComponent(login)}`);
862
+ return r2.type === "Organization" ? "Organization" : r2.type === "User" ? "User" : null;
863
+ } catch (err) {
864
+ if (err instanceof GitHubError && err.status === 404) return null;
865
+ throw err;
866
+ }
867
+ }
868
+ /** The organization's teams this token can see. */
869
+ async listOrgTeams(org) {
870
+ const r2 = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
871
+ return r2.map((t3) => ({ slug: t3.slug, name: t3.name }));
872
+ }
748
873
  /** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
749
874
  async putBase64File(input) {
750
875
  this.assertWritableBranch(input.branch);
@@ -873,13 +998,34 @@ class GitHubClient {
873
998
  this.listCheckRuns(sha).catch(() => [])
874
999
  ]);
875
1000
  const checks = [
876
- ...runs.map((c2) => ({ name: c2.name, status: c2.status, conclusion: c2.conclusion })),
1001
+ ...runs.map((c10) => ({ name: c10.name, status: c10.status, conclusion: c10.conclusion })),
877
1002
  ...combined.statuses.map((s) => ({ name: s.context, status: s.state === "pending" ? "in_progress" : "completed", conclusion: s.state === "pending" ? null : s.state }))
878
1003
  ];
879
- const failing = checks.some((c2) => c2.conclusion && !["success", "neutral", "skipped"].includes(c2.conclusion));
880
- const pending = checks.some((c2) => c2.status !== "completed");
1004
+ const failing = checks.some((c10) => c10.conclusion && !["success", "neutral", "skipped"].includes(c10.conclusion));
1005
+ const pending = checks.some((c10) => c10.status !== "completed");
881
1006
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
882
1007
  }
1008
+ /** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
1009
+ async listCommitStatuses(sha) {
1010
+ const r2 = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
1011
+ return r2.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
1012
+ }
1013
+ /**
1014
+ * GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
1015
+ * preview). Latest per environment only, so it costs one read plus one per environment.
1016
+ */
1017
+ async listDeployments(sha) {
1018
+ const list = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
1019
+ const latest = /* @__PURE__ */ new Map();
1020
+ for (const d of list) if (!latest.has(d.environment)) latest.set(d.environment, d);
1021
+ return Promise.all(
1022
+ [...latest.values()].map(async (d) => {
1023
+ const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
1024
+ const s = st[0];
1025
+ return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
1026
+ })
1027
+ );
1028
+ }
883
1029
  async getPullRequest(n2) {
884
1030
  const r2 = await this.request("GET", `${this.repoPath}/pulls/${n2}`);
885
1031
  return { ...toSummary(r2), body: r2.body ?? "", mergeable: r2.mergeable ?? null };
@@ -906,7 +1052,44 @@ class GitHubClient {
906
1052
  "GET",
907
1053
  `${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
908
1054
  );
909
- return r2.check_runs.map((c2) => ({ id: c2.id, name: c2.name, status: c2.status, conclusion: c2.conclusion, url: c2.html_url }));
1055
+ return r2.check_runs.map((c10) => ({ id: c10.id, name: c10.name, status: c10.status, conclusion: c10.conclusion, url: c10.html_url, ...c10.app?.slug ? { app: c10.app.slug } : {} }));
1056
+ }
1057
+ /** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
1058
+ async listRulesets() {
1059
+ const r2 = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
1060
+ return r2.map(withRulesetUrl);
1061
+ }
1062
+ /** One ruleset with its rules. */
1063
+ async getRuleset(id) {
1064
+ return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
1065
+ }
1066
+ /** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
1067
+ async createRuleset(payload) {
1068
+ this.assertToken("Protecting the library");
1069
+ return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
1070
+ }
1071
+ /** Replaces a repository ruleset (same permissions as createRuleset). */
1072
+ async updateRuleset(id, payload) {
1073
+ this.assertToken("Protecting the library");
1074
+ return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
1075
+ }
1076
+ /**
1077
+ * Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
1078
+ * token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
1079
+ * whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
1080
+ */
1081
+ async getBranchProtection(branch) {
1082
+ try {
1083
+ return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
1084
+ } catch (err) {
1085
+ if (!(err instanceof GitHubError)) throw err;
1086
+ if (err.status === 404) return null;
1087
+ if (err.status === 403 || err.status === 401) {
1088
+ const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
1089
+ return b?.protected === false ? null : void 0;
1090
+ }
1091
+ throw err;
1092
+ }
910
1093
  }
911
1094
  /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
912
1095
  async createCheckRun(input) {
@@ -923,13 +1106,13 @@ class GitHubClient {
923
1106
  await this.request("DELETE", `${this.repoPath}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`);
924
1107
  }
925
1108
  }
926
- function toCommitSummary(c2) {
1109
+ function toCommitSummary(c10) {
927
1110
  return {
928
- sha: c2.sha,
929
- parents: (c2.parents ?? []).map((p2) => p2.sha),
930
- date: c2.commit?.committer?.date ?? c2.commit?.author?.date ?? "",
931
- author: c2.author?.login ?? c2.commit?.author?.name ?? "unknown",
932
- message: c2.commit?.message ?? ""
1111
+ sha: c10.sha,
1112
+ parents: (c10.parents ?? []).map((p2) => p2.sha),
1113
+ date: c10.commit?.committer?.date ?? c10.commit?.author?.date ?? "",
1114
+ author: c10.author?.login ?? c10.commit?.author?.name ?? "unknown",
1115
+ message: c10.commit?.message ?? ""
933
1116
  };
934
1117
  }
935
1118
  function toSummary(r2) {
@@ -948,7 +1131,9 @@ function toSummary(r2) {
948
1131
  ...typeof r2.body === "string" ? { body: r2.body } : {}
949
1132
  };
950
1133
  }
951
- function friendlyMessage(status, detail) {
1134
+ function friendlyMessage(status, detail, info = {}) {
1135
+ if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
1136
+ if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
952
1137
  switch (status) {
953
1138
  case 401:
954
1139
  return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
@@ -1005,6 +1190,10 @@ async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
1005
1190
  await Promise.all([worker(), worker(), worker(), worker()]);
1006
1191
  return { commitSha, treeSha, files, contrastChecks, config, ...config ? { configText } : {}, tokensPath: tokensPath2, warnings };
1007
1192
  }
1193
+ function withRulesetUrl(r2) {
1194
+ const href = r2._links?.html?.href;
1195
+ return href && !r2.html_url ? { ...r2, html_url: href } : r2;
1196
+ }
1008
1197
  function toIssue(r2) {
1009
1198
  return {
1010
1199
  number: r2.number,
@@ -1019,15 +1208,165 @@ function toIssue(r2) {
1019
1208
  updatedAt: r2.updated_at
1020
1209
  };
1021
1210
  }
1022
- function gitHubHistoryBackend(client, config, opts = {}) {
1023
- const trees = /* @__PURE__ */ new Map();
1024
- const tree = (sha) => {
1025
- let t3 = trees.get(sha);
1026
- if (!t3) {
1027
- t3 = client.getCommitTree(sha).then((ts) => client.getTree(ts, true)).then((r2) => r2.entries);
1028
- trees.set(sha, t3);
1211
+ async function loadProtection(client, defaultBranch) {
1212
+ const rulesets = await client.listRulesets().then((list) => Promise.all(list.map((r2) => r2.enforcement === "disabled" || r2.id === void 0 ? r2 : client.getRuleset(r2.id)))).catch(() => null);
1213
+ const classic = await client.getBranchProtection(defaultBranch).catch(() => void 0);
1214
+ return protectionState(rulesets, classic, defaultBranch);
1215
+ }
1216
+ async function loadRequiredChecks(client, defaultBranch) {
1217
+ const onDefault = await client.listCheckRuns(defaultBranch).catch(() => []);
1218
+ const merged = await client.listPullRequests("closed").then((prs) => prs.find((p2) => p2.state === "merged" && p2.baseRef === defaultBranch)).catch(() => void 0);
1219
+ const onPull = merged?.headSha ? await client.listCheckRuns(merged.headSha).catch(() => []) : [];
1220
+ return requiredCheckNames(onDefault, onPull);
1221
+ }
1222
+ async function protectLibrary(client, defaultBranch, opts = {}) {
1223
+ const checks = opts.checks ?? await loadRequiredChecks(client, defaultBranch);
1224
+ const payload = protectionRuleset({ repo: { defaultBranch } }, { checks, approvals: opts.approvals ?? 1, adminBypass: opts.adminBypass ?? true });
1225
+ const id = opts.state?.bridgeRulesetId;
1226
+ return id !== void 0 ? client.updateRuleset(id, payload) : client.createRuleset(payload);
1227
+ }
1228
+ const PLAN_REFUSED = /upgrade to github (pro|team)|make this repository public|not available for private repositories/i;
1229
+ const isPlanRefusal = (err) => err instanceof GitHubError && (err.status === 403 || err.status === 422) && PLAN_REFUSED.test(`${err.detail ?? ""} ${err.message}`);
1230
+ const rulesSettingsUrl = (owner, repo) => `https://github.com/${owner}/${repo}/settings/rules`;
1231
+ function protectRefusal(err) {
1232
+ if (!(err instanceof GitHubError)) return null;
1233
+ if (isPlanRefusal(err)) return "GitHub's plan for this private repository does not include protection rules. Upgrade the plan or make the repository public, then try again.";
1234
+ if (/admin rights/i.test(`${err.detail ?? ""}`)) return "Only a repository admin can protect the library. Ask an admin, or set it up in GitHub settings.";
1235
+ if (err.auth === "missing-permission") return "This token cannot change the protection. Add Administration: Read and write to the token on GitHub, then try again.";
1236
+ if (err.status === 403 || err.status === 404) return "Only a repository admin can protect the library. Ask an admin, or set it up in GitHub settings.";
1237
+ return null;
1238
+ }
1239
+ const VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
1240
+ const IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
1241
+ function cssVarsIn(text) {
1242
+ const out = /* @__PURE__ */ new Set();
1243
+ for (const m2 of text.matchAll(VAR_RE)) out.add(m2[1]);
1244
+ return [...out];
1245
+ }
1246
+ function importsIn(text) {
1247
+ const out = /* @__PURE__ */ new Set();
1248
+ for (const m2 of text.matchAll(IMPORT_RE)) out.add(m2[1] ?? m2[2]);
1249
+ return [...out];
1250
+ }
1251
+ const norm = (p2) => p2.replace(/\\/g, "/").replace(/\/+$/, "");
1252
+ const isStory = (p2) => /\.stories\.[cm]?[jt]sx?$/.test(p2);
1253
+ const isTest = (p2) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p2);
1254
+ const isCss = (p2) => /\.(css|scss|less)$/.test(p2);
1255
+ const isScript = (p2) => /\.[cm]?[jt]sx?$/.test(p2);
1256
+ function componentOf(from, spec, componentsDir) {
1257
+ if (!spec.startsWith(".")) return null;
1258
+ const parts = from.split("/").slice(0, -1);
1259
+ for (const seg of spec.split("/")) {
1260
+ if (seg === "." || seg === "") continue;
1261
+ if (seg === "..") parts.pop();
1262
+ else parts.push(seg);
1263
+ }
1264
+ const abs = parts.join("/");
1265
+ if (!abs.startsWith(`${componentsDir}/`)) return null;
1266
+ return abs.slice(componentsDir.length + 1).split("/")[0] || null;
1267
+ }
1268
+ function componentForPath(path, componentsDir) {
1269
+ const dir = norm(componentsDir);
1270
+ const p2 = norm(path);
1271
+ if (!p2.startsWith(`${dir}/`)) return null;
1272
+ const rest = p2.slice(dir.length + 1).split("/");
1273
+ return rest.length >= 2 ? rest[0] : null;
1274
+ }
1275
+ function buildUsageIndex(files, componentsDir) {
1276
+ const dir = norm(componentsDir);
1277
+ const modules = {};
1278
+ const vars = /* @__PURE__ */ new Map();
1279
+ const imports = /* @__PURE__ */ new Map();
1280
+ const storyImports = /* @__PURE__ */ new Map();
1281
+ const globalVars = {};
1282
+ const get = (m2, k3) => m2.get(k3) ?? m2.set(k3, /* @__PURE__ */ new Set()).get(k3);
1283
+ for (const f of files) {
1284
+ const path = norm(f.path);
1285
+ const id = componentForPath(path, dir);
1286
+ if (!id) {
1287
+ if (isCss(path)) {
1288
+ const v2 = cssVarsIn(f.text);
1289
+ if (v2.length) globalVars[path] = v2.sort();
1290
+ }
1291
+ continue;
1029
1292
  }
1030
- return t3;
1293
+ const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
1294
+ mod.files.push(path);
1295
+ if (isTest(path)) continue;
1296
+ for (const v2 of cssVarsIn(f.text)) get(vars, id).add(v2);
1297
+ if (!isScript(path)) continue;
1298
+ for (const spec of importsIn(f.text)) {
1299
+ const dep = componentOf(path, spec, dir);
1300
+ if (!dep || dep === id) continue;
1301
+ get(isStory(path) ? storyImports : imports, id).add(dep);
1302
+ }
1303
+ }
1304
+ const byVar = {};
1305
+ for (const mod of Object.values(modules)) {
1306
+ mod.files.sort();
1307
+ mod.vars = [...vars.get(mod.id) ?? []].sort();
1308
+ mod.imports = [...imports.get(mod.id) ?? []].sort();
1309
+ mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d) => !mod.imports.includes(d)).sort();
1310
+ for (const v2 of mod.vars) (byVar[v2] ??= []).push(mod.id);
1311
+ }
1312
+ for (const k3 of Object.keys(byVar)) byVar[k3].sort();
1313
+ return { componentsDir: dir, modules, byVar, globalVars };
1314
+ }
1315
+ function impactedModules(index, input) {
1316
+ const reasons = /* @__PURE__ */ new Map();
1317
+ const add = (id, reason, indirect) => {
1318
+ const r2 = reasons.get(id);
1319
+ if (r2) {
1320
+ if (!r2.reasons.includes(reason)) r2.reasons.push(reason);
1321
+ r2.indirect &&= indirect;
1322
+ } else reasons.set(id, { reasons: [reason], indirect });
1323
+ };
1324
+ const changedVars = new Set(input.vars ?? []);
1325
+ for (const v2 of changedVars) for (const id of index.byVar[v2] ?? []) add(id, `uses ${v2}`, false);
1326
+ for (const f of input.files ?? []) {
1327
+ const id = componentForPath(f, index.componentsDir);
1328
+ if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
1329
+ }
1330
+ const renderedBy = /* @__PURE__ */ new Map();
1331
+ for (const m2 of Object.values(index.modules)) for (const d of m2.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m2.id);
1332
+ const queue = [...reasons.keys()];
1333
+ while (queue.length) {
1334
+ const id = queue.shift();
1335
+ for (const parent of renderedBy.get(id) ?? []) {
1336
+ const known = reasons.has(parent);
1337
+ add(parent, `renders ${id}`, true);
1338
+ if (!known) queue.push(parent);
1339
+ }
1340
+ }
1341
+ const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v2) => changedVars.has(v2)) })).filter((g3) => g3.vars.length > 0);
1342
+ const modules = [...reasons.entries()].map(([id, r2]) => ({ id, reasons: r2.reasons, indirect: r2.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
1343
+ return { modules, globalHits };
1344
+ }
1345
+ function tokenUsage(tokens, reads) {
1346
+ const readVars = /* @__PURE__ */ new Set();
1347
+ for (const [v2, mods] of Object.entries(reads.byVar ?? {})) if (mods.length) readVars.add(v2);
1348
+ for (const vars of Object.values(reads.globalVars ?? {})) for (const v2 of vars) readVars.add(v2);
1349
+ const byPath = new Map(tokens.map((t3) => [t3.path, t3]));
1350
+ const direct = new Set(tokens.filter((t3) => readVars.has(t3.cssVar)).map((t3) => t3.path));
1351
+ const used = /* @__PURE__ */ new Set();
1352
+ const queue = [...direct];
1353
+ while (queue.length) {
1354
+ const p2 = queue.pop();
1355
+ if (used.has(p2)) continue;
1356
+ used.add(p2);
1357
+ for (const r2 of byPath.get(p2)?.refs ?? []) if (!used.has(r2) && byPath.has(r2)) queue.push(r2);
1358
+ }
1359
+ return { used, direct };
1360
+ }
1361
+ function gitHubHistoryBackend(client, config, opts = {}) {
1362
+ const trees = /* @__PURE__ */ new Map();
1363
+ const tree = (sha) => {
1364
+ let t3 = trees.get(sha);
1365
+ if (!t3) {
1366
+ t3 = client.getCommitTree(sha).then((ts) => client.getTree(ts, true)).then((r2) => r2.entries);
1367
+ trees.set(sha, t3);
1368
+ }
1369
+ return t3;
1031
1370
  };
1032
1371
  const root = config.tokens.root.replace(/\/$/, "");
1033
1372
  const prefix = root === "." || !root ? "" : `${root}/`;
@@ -1046,20 +1385,20 @@ function gitHubHistoryBackend(client, config, opts = {}) {
1046
1385
  const commits = [];
1047
1386
  for (let page = 1; commits.length < max && page <= 10; page++) {
1048
1387
  const batch = await client.listCommits({ sha: ref, path: paths[0], perPage: Math.min(100, max), page });
1049
- for (const c2 of batch) {
1050
- const [subject, ...rest] = c2.message.split("\n");
1051
- commits.push({ sha: c2.sha, parents: c2.parents, date: c2.date, author: c2.author, subject: subject ?? "", body: rest.join("\n").trim() });
1388
+ for (const c10 of batch) {
1389
+ const [subject, ...rest] = c10.message.split("\n");
1390
+ commits.push({ sha: c10.sha, parents: c10.parents, date: c10.date, author: c10.author, subject: subject ?? "", body: rest.join("\n").trim() });
1052
1391
  }
1053
1392
  if (batch.length < Math.min(100, max)) break;
1054
1393
  }
1055
1394
  const list = commits.slice(0, max);
1056
1395
  const merges = /* @__PURE__ */ new Map();
1057
- const enriched = await pool(list, async (c2) => {
1058
- const prs = await client.listPullRequestsForCommit(c2.sha).catch(() => []);
1396
+ const enriched = await pool(list, async (c10) => {
1397
+ const prs = await client.listPullRequestsForCommit(c10.sha).catch(() => []);
1059
1398
  const pr = prs.find((p2) => p2.mergedAt && p2.baseRef === ref) ?? prs.find((p2) => p2.mergedAt);
1060
- if (!pr) return c2;
1399
+ if (!pr) return c10;
1061
1400
  const prInfo = { number: pr.number, title: pr.title, url: pr.url, author: pr.author, ...pr.mergedAt ? { mergedAt: pr.mergedAt } : {}, body: pr.body };
1062
- if (pr.mergeCommitSha && pr.mergeCommitSha !== c2.sha) {
1401
+ if (pr.mergeCommitSha && pr.mergeCommitSha !== c10.sha) {
1063
1402
  let m2 = merges.get(pr.mergeCommitSha);
1064
1403
  if (!m2) {
1065
1404
  m2 = client.getCommitInfo(pr.mergeCommitSha).then(
@@ -1074,7 +1413,7 @@ function gitHubHistoryBackend(client, config, opts = {}) {
1074
1413
  const merge = await m2;
1075
1414
  if (merge) return { ...merge, pr: prInfo };
1076
1415
  }
1077
- return { ...c2, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
1416
+ return { ...c10, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
1078
1417
  });
1079
1418
  return enriched;
1080
1419
  },
@@ -1175,6 +1514,36 @@ function demoHistory(config, current) {
1175
1514
  });
1176
1515
  return { ...buildReport({ source: "demo", label: "Demo history (fixture data)", ref: "demo", entries, releases: DEMO_RELEASES, truncated: false }), demo: true };
1177
1516
  }
1517
+ async function branchPreviewUrls(gh, ref, config, opts = {}) {
1518
+ const apps = config?.previews?.apps ?? {};
1519
+ if (!Object.values(apps).some(Boolean)) return [];
1520
+ let pr;
1521
+ let branch = opts.branch;
1522
+ let sha = opts.sha;
1523
+ if (typeof ref === "number") {
1524
+ pr = ref;
1525
+ if (!branch || !sha) {
1526
+ const p2 = await gh.getPullRequest(ref);
1527
+ branch ??= p2.headRef;
1528
+ sha ??= p2.headSha;
1529
+ }
1530
+ } else {
1531
+ sha = ref;
1532
+ if (!branch || !opts.comments) {
1533
+ const open = (await gh.listPullRequestsForCommit(ref).catch(() => [])).find((p2) => p2.state === "open");
1534
+ if (open) {
1535
+ pr = open.number;
1536
+ branch ??= open.headRef;
1537
+ }
1538
+ }
1539
+ }
1540
+ const [comments, statuses, deployments] = await Promise.all([
1541
+ opts.comments ? Promise.resolve(opts.comments) : pr !== void 0 ? gh.listIssueComments(pr).catch(() => []) : Promise.resolve([]),
1542
+ sha ? gh.listCommitStatuses(sha).catch(() => []) : Promise.resolve([]),
1543
+ sha ? gh.listDeployments(sha).catch(() => []) : Promise.resolve([])
1544
+ ]);
1545
+ return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
1546
+ }
1178
1547
  const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
1179
1548
  const LIGHTNESS = { 50: 0.975, 100: 0.952, 200: 0.912, 300: 0.85, 400: 0.735, 500: 0.63, 600: 0.53, 700: 0.445, 800: 0.36, 900: 0.27, 950: 0.205 };
1180
1549
  const BELL = { 50: 0.12, 100: 0.22, 200: 0.4, 300: 0.62, 400: 0.85, 500: 1, 600: 1, 700: 0.92, 800: 0.8, 900: 0.62, 950: 0.5 };
@@ -1196,16 +1565,16 @@ function oklchToLinear(L, C2, h3) {
1196
1565
  ];
1197
1566
  }
1198
1567
  const encode = (x) => {
1199
- const c2 = Math.min(1, Math.max(0, x));
1200
- return c2 <= 31308e-7 ? 12.92 * c2 : 1.055 * c2 ** (1 / 2.4) - 0.055;
1568
+ const c10 = Math.min(1, Math.max(0, x));
1569
+ return c10 <= 31308e-7 ? 12.92 * c10 : 1.055 * c10 ** (1 / 2.4) - 0.055;
1201
1570
  };
1202
1571
  const decode = (x) => x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
1203
1572
  function oklch(L, C2, h3) {
1204
- let c2 = C2;
1205
- let lin = oklchToLinear(L, c2, h3);
1573
+ let c10 = C2;
1574
+ let lin = oklchToLinear(L, c10, h3);
1206
1575
  for (let i2 = 0; i2 < 60 && !lin.every((x) => x >= -5e-4 && x <= 1.0005); i2++) {
1207
- c2 *= 0.95;
1208
- lin = oklchToLinear(L, c2, h3);
1576
+ c10 *= 0.95;
1577
+ lin = oklchToLinear(L, c10, h3);
1209
1578
  }
1210
1579
  return lin.map(encode);
1211
1580
  }
@@ -1426,7 +1795,7 @@ function planNewBrand(source, input) {
1426
1795
  const doc = JSON.parse(templateText);
1427
1796
  const before = new LayeredTokenSet(config, files);
1428
1797
  const chain = before.resolve("color.action.primary.bg", before.normalizeCombo({ brand: template })).chain;
1429
- const actionStep = Number(/^palette\.primary\.(\d+)$/.exec(chain.find((c2) => /^palette\.primary\.\d+$/.test(c2)) ?? "")?.[1] ?? 600);
1798
+ const actionStep = Number(/^palette\.primary\.(\d+)$/.exec(chain.find((c10) => /^palette\.primary\.\d+$/.test(c10)) ?? "")?.[1] ?? 600);
1430
1799
  const anchors = {};
1431
1800
  const safeRamp = (role, hue, kind, pin) => {
1432
1801
  try {
@@ -1576,10 +1945,10 @@ function planNewBrand(source, input) {
1576
1945
  }
1577
1946
  const pairs = parseContrastChecks(source.contrastText);
1578
1947
  const inGate = nextConfig.axes.brand?.values.includes(slug);
1579
- const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c2) => c2.brand === slug) : [];
1580
- const results = combos.flatMap((c2) => pairs.map((p2) => checkLayeredPair(after, p2, c2)));
1948
+ const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c10) => c10.brand === slug) : [];
1949
+ const results = combos.flatMap((c10) => pairs.map((p2) => checkLayeredPair(after, p2, c10)));
1581
1950
  const failing = results.filter((r2) => !r2.pass);
1582
- const themes = [...new Set(combos.map((c2) => c2.theme).filter((t3) => !!t3))];
1951
+ const themes = [...new Set(combos.map((c10) => c10.theme).filter((t3) => !!t3))];
1583
1952
  const ramps = {};
1584
1953
  if (inGate) {
1585
1954
  const combo = after.normalizeCombo({ brand: slug });
@@ -1653,10 +2022,10 @@ function newBrandSection(plan) {
1653
2022
  `| --- | ${roles.map(() => "---").join(" | ")} |`,
1654
2023
  ...STEPS.map((s) => `| ${s}${s === plan.actionStep ? " (actions)" : ""} | ${roles.map((r2) => code(plan.ramps[r2].find((x) => x.step === s)?.hex ?? "")).join(" | ")} |`)
1655
2024
  ].join("\n") : "_No ramps resolved._";
1656
- const c2 = plan.contrast;
1657
- const contrastLine = c2.failing.length ? `**${c2.failing.length} of ${c2.checks} pairs fail** for ${plan.name} (${c2.themes.join(" and ")}).` : `All ${c2.checks} contrast checks pass for ${plan.name} in ${c2.themes.join(" and ")}.`;
1658
- const failRows = c2.failing.slice(0, 20).map((r2) => `| ${code(r2.fg)} on ${code(r2.bg)} | ${r2.modes.split(",").find((m2) => m2.startsWith("theme="))?.slice(6) ?? r2.modes} | ${r2.ratio ?? "n/a"} | ${r2.min} |`);
1659
- const tightest = [...c2.results].filter((r2) => r2.pass && r2.ratio !== null).sort((a, b) => a.ratio - a.min - (b.ratio - b.min)).slice(0, 3);
2025
+ const c10 = plan.contrast;
2026
+ const contrastLine = c10.failing.length ? `**${c10.failing.length} of ${c10.checks} pairs fail** for ${plan.name} (${c10.themes.join(" and ")}).` : `All ${c10.checks} contrast checks pass for ${plan.name} in ${c10.themes.join(" and ")}.`;
2027
+ const failRows = c10.failing.slice(0, 20).map((r2) => `| ${code(r2.fg)} on ${code(r2.bg)} | ${r2.modes.split(",").find((m2) => m2.startsWith("theme="))?.slice(6) ?? r2.modes} | ${r2.ratio ?? "n/a"} | ${r2.min} |`);
2028
+ const tightest = [...c10.results].filter((r2) => r2.pass && r2.ratio !== null).sort((a, b) => a.ratio - a.min - (b.ratio - b.min)).slice(0, 3);
1660
2029
  return [
1661
2030
  "## New brand",
1662
2031
  "",
@@ -1756,7 +2125,7 @@ function missingProse(d) {
1756
2125
  function prefillDocs(input) {
1757
2126
  const existing = input.existing ?? null;
1758
2127
  const anatomy = {};
1759
- for (const c2 of input.cssClasses) anatomy[c2] = existing?.anatomy?.[c2] ?? anatomyLabel(c2);
2128
+ for (const c10 of input.cssClasses) anatomy[c10] = existing?.anatomy?.[c10] ?? anatomyLabel(c10);
1760
2129
  const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p2) => `${p2.foreground} on ${p2.background}: ${p2.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p2.min}:1 (${p2.ratio >= p2.min ? "passes" : "fails"}).`);
1761
2130
  const missing = missingProse(existing);
1762
2131
  const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
@@ -1773,6 +2142,7 @@ function prefillDocs(input) {
1773
2142
  contentGuidelines: existing?.contentGuidelines ?? [],
1774
2143
  anatomy,
1775
2144
  ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
2145
+ ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
1776
2146
  ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
1777
2147
  };
1778
2148
  return {
@@ -1787,7 +2157,17 @@ function prefillDocs(input) {
1787
2157
  draft
1788
2158
  };
1789
2159
  }
1790
- function docsPrompt(prefill, conventions = {}) {
2160
+ const SECTION_SHAPE = {
2161
+ summary: '"summary": "one or two sentences"',
2162
+ whenToUse: '"whenToUse": ["..."]',
2163
+ whenNotToUse: '"whenNotToUse": ["..."]',
2164
+ dos: '"dos": [{ "title": "...", "text": "..." }]',
2165
+ donts: '"donts": [{ "title": "...", "text": "..." }]',
2166
+ accessibility: '"accessibility": ["..."]',
2167
+ contentGuidelines: '"contentGuidelines": ["..."]'
2168
+ };
2169
+ const docsSectionShapes = (keys) => `{ ${keys.map((k3) => SECTION_SHAPE[k3]).join(", ")} }`;
2170
+ function docsPrompt(prefill, conventions = {}, options = {}) {
1791
2171
  const facts = [
1792
2172
  `Component: ${prefill.component}`,
1793
2173
  prefill.propsTable ? `Props (parsed from TypeScript):
@@ -1802,15 +2182,33 @@ ${prefill.propsTable}` : "Props: none of its own.",
1802
2182
  const block = (title, text) => text?.trim() ? `<${title}>
1803
2183
  ${text.trim()}
1804
2184
  </${title}>` : "";
2185
+ const keys = options.keys;
2186
+ const check = keys ? (options.check ?? []).filter((k3) => keys.includes(k3)) : [];
2187
+ const write = keys ? keys.filter((k3) => !check.includes(k3)) : [];
2188
+ const task = keys ? [
2189
+ ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
2190
+ ...check.length ? [
2191
+ `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
2192
+ "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.",
2193
+ "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."
2194
+ ] : [],
2195
+ ...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."] : []
2196
+ ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
2197
+ const shape = keys ? [
2198
+ `- ${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.`,
2199
+ `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
2200
+ ] : [
2201
+ `- 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.`,
2202
+ "- Answer with the full docs.json as one JSON object, nothing else."
2203
+ ];
1805
2204
  return [
1806
- `Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`,
2205
+ ...task,
1807
2206
  "",
1808
2207
  "Rules:",
1809
2208
  "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
1810
2209
  "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
1811
2210
  '- 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.',
1812
- `- 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.`,
1813
- "- Answer with the full docs.json as one JSON object, nothing else.",
2211
+ ...shape,
1814
2212
  "",
1815
2213
  "Facts computed from the code:",
1816
2214
  facts.join("\n\n"),
@@ -1819,20 +2217,32 @@ ${text.trim()}
1819
2217
  ${JSON.stringify(prefill.draft, null, 2)}`,
1820
2218
  "",
1821
2219
  block("source", conventions.source),
2220
+ block("styles", conventions.styles),
1822
2221
  block("agents-md", conventions.agents),
1823
2222
  block("voice", conventions.voice),
1824
2223
  block("example-docs-json", conventions.example)
1825
2224
  ].filter((x, i2, all) => x !== "" || all[i2 - 1] !== "").join("\n");
1826
2225
  }
1827
- function markClaudeDraft(d) {
1828
- return { ...d, draftedBy: "claude", needsReview: true };
2226
+ function markClaudeDraft(d, sections) {
2227
+ if (!sections) {
2228
+ const { draftedSections: _all, ...rest } = d;
2229
+ return { ...rest, draftedBy: "claude", needsReview: true };
2230
+ }
2231
+ const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
2232
+ const union = PROSE_KEYS.filter((k3) => waiting.includes(k3) || sections.includes(k3));
2233
+ return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
2234
+ }
2235
+ function sectionsNeedingReview(d) {
2236
+ if (!d || d.needsReview !== true) return [];
2237
+ return d.draftedSections ? PROSE_KEYS.filter((k3) => d.draftedSections.includes(k3)) : [...PROSE_KEYS];
1829
2238
  }
1830
2239
  const needsDocsReview = (d) => Boolean(d && d.needsReview === true);
1831
2240
  function acceptDocsDraft(d) {
1832
- return { ...d, needsReview: false };
2241
+ const { draftedSections: _done, ...rest } = d;
2242
+ return { ...rest, needsReview: false };
1833
2243
  }
1834
2244
  function formatDocsJson(d) {
1835
- const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k3) => k3 !== "summary"), "draftedBy", "needsReview"];
2245
+ const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k3) => k3 !== "summary"), "draftedBy", "draftedSections", "needsReview"];
1836
2246
  const out = {};
1837
2247
  for (const k3 of order) if (d[k3] !== void 0) out[k3] = d[k3];
1838
2248
  for (const [k3, v2] of Object.entries(d)) if (!(k3 in out) && v2 !== void 0) out[k3] = v2;
@@ -1867,6 +2277,416 @@ function acceptDocsText(text) {
1867
2277
  return patchDocsJson(text, acceptDocsDraft(JSON.parse(text)));
1868
2278
  }
1869
2279
  const DOCS_CHANGE_BUMP = "patch";
2280
+ function cleanDraftText(text) {
2281
+ return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2282
+ }
2283
+ const values = (s) => (s ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
2284
+ const article = (word) => /^[aeiou]/i.test(word) ? "an" : "a";
2285
+ const andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2286
+ const plural = (n2, word, many = `${word}s`) => `${n2} ${n2 === 1 ? word : many}`;
2287
+ const capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2288
+ const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
2289
+ function propShape(s) {
2290
+ if (!s) return void 0;
2291
+ const m2 = /^(.*?), default (.*)$/.exec(s);
2292
+ return m2 ? { type: m2[1], def: m2[2] } : { type: s, def: "" };
2293
+ }
2294
+ const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
2295
+ function shortVariant(key) {
2296
+ const pairs = key.split(", ").map((p2) => {
2297
+ const at2 = p2.indexOf("=");
2298
+ return at2 < 0 ? { k: "", v: p2.trim() } : { k: p2.slice(0, at2).trim(), v: p2.slice(at2 + 1).trim() };
2299
+ });
2300
+ const kept = pairs.filter((p2) => p2.v && !/^default$/i.test(p2.v));
2301
+ if (!kept.length) return key;
2302
+ const rank = (k3) => {
2303
+ const i2 = VARIANT_FIRST.indexOf(k3.toLowerCase());
2304
+ return i2 < 0 ? VARIANT_FIRST.length : i2;
2305
+ };
2306
+ return kept.map((p2, i2) => ({ ...p2, i: i2 })).sort((a, b) => rank(a.k) - rank(b.k) || a.i - b.i).map((p2) => p2.v).join(" ");
2307
+ }
2308
+ const shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
2309
+ function parseStructureChange(c10) {
2310
+ const m2 = /^(New|Removed|Moved) layer "(.*)"(?: \((.*)\))?$/.exec(c10.field);
2311
+ if (!m2) return void 0;
2312
+ const op = m2[1] === "New" ? "added" : m2[1] === "Removed" ? "removed" : "moved";
2313
+ const where = `in ${shortVariants(m2[3])}`;
2314
+ if (op === "moved") return { op, name: m2[2], where, at: c10.after, from: c10.before };
2315
+ const shape = (op === "added" ? c10.after : c10.before) ?? "";
2316
+ const sm = /^(\S+)(?: (.*?))?, ((?:after|before) ".*"|alone)$/.exec(shape);
2317
+ return { op, name: m2[2], where, ...sm ? { type: sm[1], ...sm[2] ? { main: sm[2] } : {}, at: sm[3] } : {} };
2318
+ }
2319
+ function structurePhrase(r2) {
2320
+ const type = r2.type ? `${r2.type} ` : "";
2321
+ const main = r2.main ? ` (${r2.main})` : "";
2322
+ const at2 = r2.at && r2.at !== "alone" ? ` ${r2.at}` : "";
2323
+ if (r2.op === "added") return `New ${type}layer "${r2.name}"${main}${at2} ${r2.where}`;
2324
+ if (r2.op === "removed") return `Removed ${type}layer "${r2.name}"${main}${at2} ${r2.where}`;
2325
+ return `Moved layer "${r2.name}" ${r2.at ?? ""}${r2.from ? ` (was ${r2.from})` : ""} ${r2.where}`.replace(/\s+/g, " ");
2326
+ }
2327
+ const bare = (at2) => (at2 ?? "").replace(/"/g, "");
2328
+ function firstChange(component, kind, c10, all) {
2329
+ switch (kind) {
2330
+ case "new-component": {
2331
+ const axes = all.filter((x) => x.field.startsWith("Variant "));
2332
+ const props = all.filter((x) => x.field.startsWith("Prop "));
2333
+ const parts = [axes.length ? plural(axes.length, "variant axis", "variant axes") : "", props.length ? plural(props.length, "prop") : ""].filter(Boolean);
2334
+ return {
2335
+ title: `Add the ${component} component`,
2336
+ summary: `${component} is not in code yet.${parts.length ? ` The Figma component has ${andList(parts)}.` : ""}`
2337
+ };
2338
+ }
2339
+ case "new-variant": {
2340
+ if (!c10) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2341
+ const axis = c10.field.replace(/^Variant /, "");
2342
+ const before = values(c10.before);
2343
+ const after = values(c10.after);
2344
+ const added = after.filter((x) => !before.includes(x));
2345
+ const removed = before.filter((x) => !after.includes(x));
2346
+ const word = axis.toLowerCase();
2347
+ 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)}.` };
2348
+ if (!after.length) return { title: `Remove the ${axis} variant axis`, summary: `${component} no longer has the ${axis} variant axis (${andList(before)}).` };
2349
+ if (added.length === 1 && !removed.length) return { title: `Add ${article(added[0])} ${added[0]} ${word}`, summary: `${component} gets a new ${axis} value, ${added[0]}.` };
2350
+ if (added.length && !removed.length) return { title: `Add ${andList(added)} to ${axis}`, summary: `${component} gets ${plural(added.length, `new ${axis} value`)}: ${andList(added)}.` };
2351
+ if (removed.length === 1 && !added.length) return { title: `Remove the ${removed[0]} ${word}`, summary: `${component} no longer has the ${axis} value ${removed[0]}.` };
2352
+ if (removed.length && !added.length) return { title: `Remove ${andList(removed)} from ${axis}`, summary: `${component} no longer has the ${axis} values ${andList(removed)}.` };
2353
+ return { title: `Change the ${axis} values`, summary: `${axis} on ${component} goes from ${andList(before)} to ${andList(after)}.` };
2354
+ }
2355
+ case "prop-change": {
2356
+ if (!c10) return { title: `Change a ${component} prop`, summary: `A prop of ${component} changed in Figma.` };
2357
+ const name = c10.field.replace(/^Prop /, "");
2358
+ const b = propShape(c10.before);
2359
+ const a = propShape(c10.after);
2360
+ if (!b && a) return { title: `Add the ${name} prop`, summary: `${component} gets a new ${a.type} prop, ${name}${a.def ? `, default ${a.def}` : ""}.` };
2361
+ if (b && !a) return { title: `Remove the ${name} prop`, summary: `${component} no longer has the ${name} prop.` };
2362
+ 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"}.` };
2363
+ return { title: `Change the ${name} prop`, summary: `${name} on ${component} goes from ${c10.before ?? "none"} to ${c10.after ?? "none"}.` };
2364
+ }
2365
+ case "token-binding": {
2366
+ if (!c10) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2367
+ const m2 = /^(.*?)(?: \((.*)\))?$/.exec(c10.field);
2368
+ const part = lower(m2[1]);
2369
+ const where = m2[2] ? ` in ${m2[2]}` : "";
2370
+ const after = c10.after && c10.after !== "none" ? c10.after : "";
2371
+ const before = c10.before && c10.before !== "none" ? c10.before : "";
2372
+ if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where} no longer uses ${before || "a token"}.` };
2373
+ return {
2374
+ title: `Use ${after} for the ${part}`,
2375
+ summary: `The ${part} of ${component}${where} ${before ? `moves from ${before} to ${after}` : `now uses ${after}`}.`
2376
+ };
2377
+ }
2378
+ case "structure": {
2379
+ const r2 = c10 ? parseStructureChange(c10) : void 0;
2380
+ if (!r2) return { title: "Change the layers to match Figma", summary: `The layers of ${component} changed in Figma.` };
2381
+ if (r2.op === "added") {
2382
+ const at2 = r2.at && r2.at !== "alone" ? ` ${bare(r2.at)}` : "";
2383
+ return { title: `Add a layer${at2}`, summary: `${component} gets ${article(r2.type ?? "layer")} ${r2.type ?? "layer"}${r2.type ? " layer" : ""}, ${r2.name}${r2.main ? ` (${r2.main})` : ""}${at2} ${r2.where}.` };
2384
+ }
2385
+ if (r2.op === "removed") return { title: `Remove the ${r2.name} layer`, summary: `${component} no longer has the ${r2.name} layer ${r2.where}.` };
2386
+ return { title: `Move the ${r2.name} layer`, summary: `The ${r2.name} layer of ${component} moves ${bare(r2.at) || "elsewhere"}${r2.from ? ` (it was ${bare(r2.from)})` : ""} ${r2.where}.` };
2387
+ }
2388
+ case "visual":
2389
+ return {
2390
+ title: "Update the look to match Figma",
2391
+ summary: `${component} looks different in Figma${c10?.field.startsWith("Look of ") && c10.field !== "Look of every variant" ? ` in ${c10.field.slice(8)}` : ""}, and no variable edit explains it.`
2392
+ };
2393
+ default:
2394
+ return { title: "", summary: "" };
2395
+ }
2396
+ }
2397
+ function draftRequestText(component, kind, changes) {
2398
+ const name = component.trim() || "the component";
2399
+ if (!changes.length && kind !== "new-component") return { title: "", summary: "" };
2400
+ const d = firstChange(name, kind, changes[0], changes);
2401
+ const rest = kind === "new-component" ? 0 : changes.length - 1;
2402
+ const summary = rest > 0 ? `${d.summary} ${capital(plural(rest, "other change"))} ${rest === 1 ? "is" : "are"} listed below.` : d.summary;
2403
+ return { title: cleanDraftText(d.title), summary: cleanDraftText(summary) };
2404
+ }
2405
+ const DRAFT_LINE_MAX = 120;
2406
+ function clipText(text, max) {
2407
+ const t3 = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
2408
+ if (t3.length <= max) return t3;
2409
+ const cut = t3.slice(0, max + 1);
2410
+ const at2 = cut.lastIndexOf(" ");
2411
+ return (at2 > max * 0.5 ? cut.slice(0, at2) : t3.slice(0, max)).replace(/[\s,;:(]+$/, "");
2412
+ }
2413
+ function sanitizeDraftLine(raw) {
2414
+ if (typeof raw !== "string") return "";
2415
+ return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
2416
+ }
2417
+ function changePhrase(c10) {
2418
+ let m2;
2419
+ if (m2 = /^Variant (.+)$/.exec(c10.field)) {
2420
+ const axis = m2[1];
2421
+ const before2 = values(c10.before);
2422
+ const after2 = values(c10.after);
2423
+ if (!before2.length) return `New ${axis} variant axis: ${after2.join(", ")}`;
2424
+ if (!after2.length) return `${axis} variant axis removed (${before2.join(", ")})`;
2425
+ const added = after2.filter((x) => !before2.includes(x));
2426
+ const removed = before2.filter((x) => !after2.includes(x));
2427
+ const parts = [added.length ? `${andList(added)} added to ${axis}` : "", removed.length ? `${andList(removed)} removed from ${axis}` : ""].filter(Boolean);
2428
+ return parts.join("; ") || `${axis} values reordered: ${after2.join(", ")}`;
2429
+ }
2430
+ if (m2 = /^Prop (.+)$/.exec(c10.field)) {
2431
+ const name = m2[1];
2432
+ const b = propShape(c10.before);
2433
+ const a = propShape(c10.after);
2434
+ if (!b && a) return `New ${a.type} prop ${name}${a.def ? `, default ${a.def}` : ""}`;
2435
+ if (b && !a) return `${name} prop removed`;
2436
+ if (b && a && b.type === a.type) return `${name} default goes from ${b.def || "empty"} to ${a.def || "empty"}`;
2437
+ return `${name} prop goes from ${c10.before ?? "none"} to ${c10.after ?? "none"}`;
2438
+ }
2439
+ if (m2 = /^Swapped instance(?: \((.*)\))?$/.exec(c10.field)) return `Shows ${c10.after ?? "another instance"} instead of ${c10.before ?? "the coded one"} ${m2[1] ? `in ${m2[1]}` : "in every variant"}`;
2440
+ if (m2 = /^Look of (.+)$/.exec(c10.field)) return `Looks different in ${m2[1]}; no variable explains it`;
2441
+ const layer = parseStructureChange(c10);
2442
+ if (layer) return structurePhrase(layer);
2443
+ m2 = /^(.*?)(?: \((.*)\))?$/.exec(c10.field);
2444
+ const where = m2[2] ? ` in ${m2[2]}` : "";
2445
+ const before = c10.before && c10.before !== "none" ? c10.before : "";
2446
+ const after = c10.after && c10.after !== "none" ? c10.after : "";
2447
+ if (!after) return `${m2[1]} no longer uses ${before || "a token"}${where}`;
2448
+ if (!before) return `${m2[1]} now uses ${after}${where}`;
2449
+ return `${m2[1]} uses ${after} instead of ${before}${where}`;
2450
+ }
2451
+ function draftChangeLine(kind, changes) {
2452
+ if (kind === "new-component") {
2453
+ const axes = changes.filter((x) => x.field.startsWith("Variant ")).length;
2454
+ const props = changes.filter((x) => x.field.startsWith("Prop ")).length;
2455
+ const parts = [axes ? plural(axes, "variant axis", "variant axes") : "", props ? plural(props, "prop") : ""].filter(Boolean);
2456
+ return `New component${parts.length ? ` with ${andList(parts)}` : ""}, not in code yet`;
2457
+ }
2458
+ if (!changes.length) return "";
2459
+ const first = changePhrase(changes[0]);
2460
+ const rest = changes.length - 1;
2461
+ const tail = rest ? ` (and ${rest} more)` : "";
2462
+ return capital(clipText(cleanDraftText(first), DRAFT_LINE_MAX - tail.length)) + tail;
2463
+ }
2464
+ const tokenOf = (e3) => e3.figma ?? e3.head ?? e3.base;
2465
+ const pathOf = (e3) => tokenOf(e3)?.path ?? e3.path;
2466
+ function pushChangeRows(entries) {
2467
+ return entries.map((e3) => {
2468
+ const before = e3.head ?? e3.base;
2469
+ const after = e3.figma;
2470
+ const show = (t3) => {
2471
+ if (!t3) return void 0;
2472
+ const modes = Object.keys(t3.values);
2473
+ if (modes.length <= 1) return formatValue(t3.type, t3.values[modes[0] ?? ""]);
2474
+ return modes.map((m2) => `${modeDisplayName(m2)} ${formatValue(t3.type, t3.values[m2])}`).join("; ");
2475
+ };
2476
+ const renamed = e3.local?.kind === "renamed" && before && after && before.path !== after.path;
2477
+ return { field: renamed ? `${before.path} -> ${after.path}` : pathOf(e3), before: show(before), after: show(after) };
2478
+ });
2479
+ }
2480
+ function names(paths) {
2481
+ if (paths.length <= 3) return andList(paths);
2482
+ return `${paths.length} tokens, including ${andList(paths.slice(0, 2))}`;
2483
+ }
2484
+ function valueModes(e3) {
2485
+ return (e3.local?.fields ?? []).filter((f) => f.field === "value" && f.mode && f.mode !== "value").map((f) => modeDisplayName(f.mode));
2486
+ }
2487
+ function draftPushDescription(entries, components = []) {
2488
+ const by2 = { changed: [], added: [], renamed: [], removed: [] };
2489
+ for (const e3 of entries) by2[e3.local?.kind ?? "changed"].push(e3);
2490
+ const clauses = [];
2491
+ if (by2.changed.length) {
2492
+ const only = by2.changed.length === 1 ? by2.changed[0] : void 0;
2493
+ const fields = new Set((only?.local?.fields ?? []).map((f) => f.field));
2494
+ const modes = only ? valueModes(only) : [];
2495
+ if (only && fields.size === 1 && fields.has("description")) clauses.push(`updates the description of ${pathOf(only)}`);
2496
+ else if (only && modes.length) clauses.push(`changes ${pathOf(only)} in ${andList(modes)}`);
2497
+ else clauses.push(`changes ${names(by2.changed.map(pathOf))}`);
2498
+ }
2499
+ if (by2.added.length) clauses.push(`adds ${names(by2.added.map(pathOf))}`);
2500
+ if (by2.renamed.length) {
2501
+ const r2 = by2.renamed[0];
2502
+ const from = (r2.head ?? r2.base)?.path;
2503
+ clauses.push(by2.renamed.length === 1 && from && from !== pathOf(r2) ? `renames ${from} to ${pathOf(r2)}` : `renames ${names(by2.renamed.map(pathOf))}`);
2504
+ }
2505
+ if (by2.removed.length) clauses.push(`removes ${names(by2.removed.map(pathOf))}`);
2506
+ const sentences2 = [];
2507
+ if (clauses.length) sentences2.push(`${capital(andList(clauses))}.`);
2508
+ const fresh = components.filter((c10) => c10.kind === "new-component").map((c10) => c10.component);
2509
+ const changed = [...new Set(components.filter((c10) => c10.kind !== "new-component").map((c10) => c10.component))];
2510
+ const comp = [];
2511
+ if (changed.length) comp.push(`${changed.length === 1 ? "a change" : "changes"} to ${andList(changed)}`);
2512
+ if (fresh.length) comp.push(`the new ${andList(fresh)} component${fresh.length === 1 ? "" : "s"}`);
2513
+ if (comp.length) sentences2.push(`Requests ${andList(comp)}.`);
2514
+ return cleanDraftText(sentences2.join(" "));
2515
+ }
2516
+ const DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
2517
+ const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
2518
+ const DOCS_SELECTION_MODES = {
2519
+ clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
2520
+ shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
2521
+ voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
2522
+ };
2523
+ const isDocsSelectionMode = (m2) => typeof m2 === "string" && m2 in DOCS_SELECTION_MODES;
2524
+ const isProseKey = (k3) => typeof k3 === "string" && PROSE_KEYS.includes(k3);
2525
+ function validateDocsTask(x) {
2526
+ if (!x || typeof x !== "object") return ["The docs task is missing."];
2527
+ const t3 = x;
2528
+ if (t3.kind === "sections") {
2529
+ const keys = t3.keys;
2530
+ if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
2531
+ if (t3.check !== void 0 && (!Array.isArray(t3.check) || !t3.check.every(isProseKey))) return ["check must list prose sections."];
2532
+ return [];
2533
+ }
2534
+ if (t3.kind === "selection") {
2535
+ const out = [];
2536
+ if (!isProseKey(t3.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
2537
+ if (typeof t3.text !== "string" || !t3.text.trim()) out.push("The selection has no text.");
2538
+ if (!isDocsSelectionMode(t3.mode)) out.push("The selection mode must be clearer, shorter or voice.");
2539
+ return out;
2540
+ }
2541
+ return ['The docs task kind must be "sections" or "selection".'];
2542
+ }
2543
+ const docsTaskKeys = (t3) => t3.kind === "sections" ? t3.keys : [t3.key];
2544
+ function docsSelectionPrompt(prefill, conventions, sel) {
2545
+ const base = docsPrompt(prefill, conventions, { keys: [sel.key] });
2546
+ const facts = base.slice(base.indexOf("Facts computed from the code:"));
2547
+ return [
2548
+ `Rewrite one passage of the "${sel.key}" section of ${prefill.component}.docs.json for the Bridge design system.`,
2549
+ DOCS_SELECTION_MODES[sel.mode],
2550
+ "",
2551
+ "Rules:",
2552
+ "- Keep its meaning and every fact that the code supports. Never add props, variants, keyboard behavior or tokens that are not in the facts or the source below.",
2553
+ "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words, no Markdown.",
2554
+ "- Rewrite only the passage: it is pasted back in place of the selection, inside the rest of the section.",
2555
+ '- Answer with one JSON object, nothing else: { "replacement": "the rewritten passage" }.',
2556
+ "",
2557
+ `<passage>
2558
+ ${sel.text.trim()}
2559
+ </passage>`,
2560
+ "",
2561
+ facts
2562
+ ].join("\n");
2563
+ }
2564
+ function docsTaskPrompt(prefill, conventions, task) {
2565
+ if (task.kind === "selection") return docsSelectionPrompt(prefill, conventions, task);
2566
+ return docsPrompt(prefill, conventions, { keys: task.keys, ...task.rewrite ? { rewrite: true } : {}, ...task.check?.length ? { check: task.check } : {} });
2567
+ }
2568
+ function parseDocsReply(text) {
2569
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
2570
+ const body = fenced ? fenced[1] : text;
2571
+ const start = body.indexOf("{");
2572
+ const end = body.lastIndexOf("}");
2573
+ if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
2574
+ const json = JSON.parse(body.slice(start, end + 1));
2575
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
2576
+ return json;
2577
+ }
2578
+ const cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
2579
+ const isExample = (x) => {
2580
+ if (typeof x === "string") return true;
2581
+ if (!x || typeof x !== "object" || Array.isArray(x)) return false;
2582
+ const n2 = x;
2583
+ if (typeof n2.c !== "string" || !n2.c) return false;
2584
+ if (n2.p !== void 0 && (!n2.p || typeof n2.p !== "object" || Array.isArray(n2.p))) return false;
2585
+ return n2.children === void 0 || Array.isArray(n2.children) && n2.children.every(isExample);
2586
+ };
2587
+ const textList = (x) => {
2588
+ if (typeof x === "string") x = [x];
2589
+ if (!Array.isArray(x)) return void 0;
2590
+ const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
2591
+ return out.length ? out : void 0;
2592
+ };
2593
+ const guidanceList = (x, examples) => {
2594
+ if (!Array.isArray(x)) return void 0;
2595
+ const out = [];
2596
+ for (const g3 of x) {
2597
+ if (!g3 || typeof g3 !== "object") continue;
2598
+ const r2 = g3;
2599
+ const title = typeof r2.title === "string" ? cleanDocsText(r2.title) : "";
2600
+ const text = typeof r2.text === "string" ? cleanDocsText(r2.text) : "";
2601
+ if (!title || !text) continue;
2602
+ out.push({ title, text, ...examples && r2.example !== void 0 && isExample(r2.example) ? { example: r2.example } : {} });
2603
+ }
2604
+ return out.length ? out : void 0;
2605
+ };
2606
+ function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
2607
+ const out = {};
2608
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
2609
+ const r2 = raw;
2610
+ for (const k3 of keys) {
2611
+ const v2 = r2[k3];
2612
+ if (k3 === "summary") {
2613
+ const s = typeof v2 === "string" ? cleanDocsText(v2) : "";
2614
+ if (s) out.summary = s;
2615
+ } else if (k3 === "dos" || k3 === "donts") {
2616
+ const g3 = guidanceList(v2, opts.examples ?? false);
2617
+ if (g3) out[k3] = g3;
2618
+ } else {
2619
+ const l2 = textList(v2);
2620
+ if (l2) out[k3] = l2;
2621
+ }
2622
+ }
2623
+ return out;
2624
+ }
2625
+ function sanitizeDocsReplacement(raw) {
2626
+ if (typeof raw !== "string") return "";
2627
+ return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
2628
+ }
2629
+ function parseDocsTaskAnswer(task, text) {
2630
+ if (task.kind === "selection") {
2631
+ let raw = text;
2632
+ try {
2633
+ const json = parseDocsReply(text);
2634
+ raw = json.replacement ?? json.text ?? json[task.key];
2635
+ } catch {
2636
+ raw = text.replace(/```[a-z]*\s*|```/gi, "");
2637
+ }
2638
+ const replacement = sanitizeDocsReplacement(raw);
2639
+ if (!replacement) throw new Error("Claude sent no rewritten passage. Try again.");
2640
+ return { sections: {}, replacement };
2641
+ }
2642
+ return docsSectionsAnswer(task, parseDocsReply(text));
2643
+ }
2644
+ function docsSectionsAnswer(task, raw) {
2645
+ const check = (task.check ?? []).filter((k3) => task.keys.includes(k3));
2646
+ const sections = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
2647
+ return { sections, checked: check.filter((k3) => sections[k3] === void 0) };
2648
+ }
2649
+ function docsSectionsResult(task, raw) {
2650
+ const sections = sanitizeDocsSections(raw, task.keys);
2651
+ if (!Object.keys(sections).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
2652
+ return sections;
2653
+ }
2654
+ function applyDocsSections(docs, sections) {
2655
+ const keys = PROSE_KEYS.filter((k3) => sections[k3] !== void 0);
2656
+ if (!keys.length) return docs;
2657
+ return markClaudeDraft({ ...docs, ...sections }, keys);
2658
+ }
2659
+ function mergeDocsDraft(prefillDraft, reply) {
2660
+ const empty = PROSE_KEYS.filter((k3) => {
2661
+ const v2 = prefillDraft[k3];
2662
+ return v2 === void 0 || (typeof v2 === "string" ? !v2.trim() : Array.isArray(v2) && !v2.length);
2663
+ });
2664
+ const sections = sanitizeDocsSections(reply, empty, { examples: true });
2665
+ const merged = { ...prefillDraft, ...sections };
2666
+ if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
2667
+ merged.anatomy = prefillDraft.anatomy;
2668
+ const drafted = PROSE_KEYS.filter((k3) => sections[k3] !== void 0);
2669
+ return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
2670
+ }
2671
+ function anthropicRequest(input) {
2672
+ const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
2673
+ if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
2674
+ if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
2675
+ const body = {
2676
+ model: input.model || DOCS_DRAFT_MODEL,
2677
+ max_tokens: input.maxTokens ?? 16e3,
2678
+ ...input.fallback ? { fallbacks: "default" } : {},
2679
+ ...input.system ? { system: input.system } : {},
2680
+ messages: [{ role: "user", content: input.prompt }]
2681
+ };
2682
+ return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
2683
+ }
2684
+ function anthropicReplyText(msg) {
2685
+ const m2 = msg ?? {};
2686
+ if (m2.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
2687
+ if (m2.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
2688
+ return (m2.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
2689
+ }
1870
2690
  const EXPLORATION_LABEL = "exploration";
1871
2691
  const EXPLORATION_PREFIX = "explore/";
1872
2692
  const EXPLORATION_NOTES_DIR = ".bridge/explorations";
@@ -1925,16 +2745,6 @@ function explorationNote(e3) {
1925
2745
  ""
1926
2746
  ].join("\n");
1927
2747
  }
1928
- function explorationInstall(input) {
1929
- const packages = input.packages.map((name) => ({ name, url: snapshotUrl(input.owner, input.repo, name, input.pr) }));
1930
- const named = (list) => installCommand(list.map((p2) => `${p2.name}@${p2.url}`));
1931
- const out = { command: named(packages), packages };
1932
- if (input.sha) {
1933
- const tarballs = input.packages.map((name) => ({ name, url: tarballUrl(input.owner, input.repo, input.pr, name, input.sha) }));
1934
- out.fallback = { command: named(tarballs), packages: tarballs, release: `https://github.com/${input.owner}/${input.repo}/releases/tag/preview-pr-${input.pr}` };
1935
- }
1936
- return out;
1937
- }
1938
2748
  function explorationPrTitle(e3) {
1939
2749
  const n2 = e3.name.trim().replace(/\.$/, "");
1940
2750
  return `Exploration: ${n2.charAt(0).toLowerCase()}${n2.slice(1)}`;
@@ -1960,20 +2770,9 @@ function explorationPrBody(e3, ctx) {
1960
2770
  "### Try it in a product staging branch",
1961
2771
  ""
1962
2772
  ];
1963
- if (ctx.pr) {
1964
- const install = explorationInstall({ owner: ctx.owner, repo: ctx.repo, pr: ctx.pr, packages: ctx.packages });
1965
- lines.push(
1966
- "In the product repo, on a branch that deploys to staging (production keeps the released version):",
1967
- "",
1968
- "```sh",
1969
- install.command,
1970
- "```",
1971
- "",
1972
- `The snapshots follow the latest commit of this pull request. The "Try this DS branch in your product" comment lists the exact commit, and the tarball URLs from the [\`preview-pr-${ctx.pr}\` prerelease](https://github.com/${ctx.owner}/${ctx.repo}/releases/tag/preview-pr-${ctx.pr}) when pkg.pr.new is not set up.`
1973
- );
1974
- } else {
1975
- lines.push('The install command appears here and in the "Try this DS branch in your product" comment once the first snapshot is published.');
1976
- }
2773
+ lines.push(
2774
+ '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.'
2775
+ );
1977
2776
  lines.push("", "### Previews", "");
1978
2777
  const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
1979
2778
  if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l2) => `| ${l2.label} | ${l2.url} |`));
@@ -2043,6 +2842,19 @@ function promotionPlan(input) {
2043
2842
  markReady: true
2044
2843
  };
2045
2844
  }
2845
+ function explorationTryIt(input) {
2846
+ const c10 = readPreviewComment(input.comment);
2847
+ if (!c10) return { state: "building" };
2848
+ if (input.headSha && c10.sha7 && !input.headSha.startsWith(c10.sha7)) return { state: "building" };
2849
+ if (!c10.command || c10.source === "none") return { state: "failed" };
2850
+ return { state: "ready", command: c10.command, source: c10.source, sha7: c10.sha7 };
2851
+ }
2852
+ function explorationShareMessage(input) {
2853
+ const name = input.name.trim().replace(/"/g, "'");
2854
+ 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.`;
2855
+ const previews = (input.previews ?? []).map((p2) => `${p2.label} ${p2.url}`);
2856
+ return [head, previews.length ? `Previews: ${previews.join(", ")}.` : "", `Pull request: ${input.url}.`].filter(Boolean).join(" ");
2857
+ }
2046
2858
  function explorationRules(branch) {
2047
2859
  return [
2048
2860
  `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
@@ -2053,7 +2865,8 @@ function explorationRules(branch) {
2053
2865
  const REQUEST_KINDS = [
2054
2866
  { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
2055
2867
  { value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
2056
- { value: "prop-change", label: "Prop change", hint: "Add, rename or change a prop or its default." },
2868
+ { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
2869
+ { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
2057
2870
  { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
2058
2871
  { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
2059
2872
  { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
@@ -2068,13 +2881,14 @@ function requestIssueTitle(r2) {
2068
2881
  return `${r2.component}: ${t3.charAt(0).toLowerCase()}${t3.slice(1)}`;
2069
2882
  }
2070
2883
  function composeRequestIssue(r2, opts = {}) {
2071
- const rows = r2.changes.length ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r2.changes.map((c2) => `| ${cell(c2.field)} | ${cell(c2.before ?? "")} | ${cell(c2.after ?? "")} |`)] : [];
2884
+ const rows = r2.changes.length ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r2.changes.map((c10) => `| ${cell(c10.field)} | ${cell(c10.before ?? "")} | ${cell(c10.after ?? "")} |`)] : [];
2072
2885
  const figma = r2.figma?.url ? `[${r2.figma.nodeName ?? "Open in Figma"}](${r2.figma.url})` : r2.figma?.nodeName ? r2.figma.nodeName : "";
2073
2886
  const body = [
2074
2887
  r2.kind === "new-component" ? `## New component request: ${r2.component}` : `## ${kindLabel(r2.kind)} request for ${r2.component}`,
2075
2888
  "",
2076
2889
  r2.summary.trim(),
2077
2890
  "",
2891
+ ...r2.line?.trim() ? [`**What changes:** ${r2.line.trim()}`, ""] : [],
2078
2892
  ...rows.length ? [...rows, ""] : [],
2079
2893
  ...r2.images?.length ? [...r2.images.map((u2, i2) => `![${r2.component} design ${i2 + 1}](${u2})`), ""] : [],
2080
2894
  ...figma ? [`**Design:** ${figma}`, ""] : [],
@@ -2102,6 +2916,32 @@ function parseRequestIssue(body) {
2102
2916
  return void 0;
2103
2917
  }
2104
2918
  }
2919
+ function withRequestExploration(body, branch) {
2920
+ const r2 = parseRequestIssue(body);
2921
+ if (!r2 || !body) return void 0;
2922
+ const block = new RegExp(`<!-- ${MARK} \\{[\\s\\S]*?\\} -->`);
2923
+ let out = body.replace(block, () => `<!-- ${MARK} ${JSON.stringify({ ...r2, exploration: branch })} -->`);
2924
+ const line = `**Exploration:** \`${branch}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot and is promoted to review only when the idea is validated.`;
2925
+ if (/^\*\*Exploration:\*\* .*$/m.test(out)) out = out.replace(/^\*\*Exploration:\*\* .*$/m, () => line);
2926
+ else if (out.includes("\n## How to implement")) out = out.replace("\n## How to implement", () => `
2927
+ ${line}
2928
+
2929
+ ## How to implement`);
2930
+ else out = out.replace(block, (m2) => `${line}
2931
+
2932
+ ${m2}`);
2933
+ return out;
2934
+ }
2935
+ function declineReasonProblems(reason) {
2936
+ const r2 = reason.trim();
2937
+ if (!r2) return ["Say why, in one or two sentences: the requester reads it."];
2938
+ if (r2.length < 10) return ["Say a bit more, so the requester knows why: at least 10 characters."];
2939
+ return [];
2940
+ }
2941
+ function declineComment(reason, by2) {
2942
+ const quote = reason.trim().split("\n").map((l2) => `> ${l2}`.trimEnd()).join("\n");
2943
+ return [`Declined${by2 ? ` by @${by2.replace(/^@/, "")}` : ""} in Bridge Studio.`, "", quote, "", "<sub>Closed as not planned. Reopen the issue if things change.</sub>"].join("\n");
2944
+ }
2105
2945
  function requestStatus(issue) {
2106
2946
  const prs = issue.linkedPrs ?? [];
2107
2947
  if (issue.state === "open") return prs.some((p2) => p2.state === "open") ? "in-progress" : "requested";
@@ -2127,10 +2967,11 @@ function requestBrief(r2, ctx) {
2127
2967
  `Component: ${r2.component}`,
2128
2968
  `Kind: ${kindLabel(r2.kind)}`,
2129
2969
  `Request: ${r2.title}`,
2970
+ ...r2.line?.trim() ? [`What changes: ${r2.line.trim()}`] : [],
2130
2971
  "",
2131
2972
  r2.summary.trim(),
2132
2973
  "",
2133
- ...r2.changes.length ? ["Changes asked:", ...r2.changes.map((c2) => `- ${c2.field}: ${c2.before ?? "(none)"} -> ${c2.after ?? "(removed)"}`), ""] : [],
2974
+ ...r2.changes.length ? ["Changes asked:", ...r2.changes.map((c10) => `- ${c10.field}: ${c10.before ?? "(none)"} -> ${c10.after ?? "(removed)"}`), ""] : [],
2134
2975
  ...r2.figma?.url ? [`Design: ${r2.figma.url}`, ""] : [],
2135
2976
  ...r2.images?.length ? ["Images:", ...r2.images.map((u2) => `- ${u2}`), ""] : [],
2136
2977
  ...ctx.files?.length ? ["Files of this component:", ...ctx.files.map((f) => `- ${f}`), ""] : [],
@@ -2152,7 +2993,7 @@ function validateIntegrations(i2) {
2152
2993
  }
2153
2994
  function jiraIssueFields(r2, jira, issueUrl) {
2154
2995
  const text = (t3) => ({ type: "text", text: t3 });
2155
- const para = (...c2) => ({ type: "paragraph", content: c2 });
2996
+ const para = (...c10) => ({ type: "paragraph", content: c10 });
2156
2997
  return {
2157
2998
  project: { key: jira.project },
2158
2999
  issuetype: { name: jira.issueType ?? "Task" },
@@ -2163,145 +3004,13 @@ function jiraIssueFields(r2, jira, issueUrl) {
2163
3004
  version: 1,
2164
3005
  content: [
2165
3006
  para(text(r2.summary.trim() || r2.title)),
2166
- ...r2.changes.map((c2) => para(text(`${c2.field}: ${c2.before ?? "(none)"} -> ${c2.after ?? "(removed)"}`))),
3007
+ ...r2.line?.trim() ? [para(text(r2.line.trim()))] : [],
3008
+ ...r2.changes.map((c10) => para(text(`${c10.field}: ${c10.before ?? "(none)"} -> ${c10.after ?? "(removed)"}`))),
2167
3009
  para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
2168
3010
  ]
2169
3011
  }
2170
3012
  };
2171
3013
  }
2172
- function cleanDraftText(text) {
2173
- return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2174
- }
2175
- const values = (s) => (s ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
2176
- const article = (word) => /^[aeiou]/i.test(word) ? "an" : "a";
2177
- const andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2178
- const plural = (n2, word, many = `${word}s`) => `${n2} ${n2 === 1 ? word : many}`;
2179
- const capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2180
- const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
2181
- function propShape(s) {
2182
- if (!s) return void 0;
2183
- const m2 = /^(.*?), default (.*)$/.exec(s);
2184
- return m2 ? { type: m2[1], def: m2[2] } : { type: s, def: "" };
2185
- }
2186
- function firstChange(component, kind, c2, all) {
2187
- switch (kind) {
2188
- case "new-component": {
2189
- const axes = all.filter((x) => x.field.startsWith("Variant "));
2190
- const props = all.filter((x) => x.field.startsWith("Prop "));
2191
- const parts = [axes.length ? plural(axes.length, "variant axis", "variant axes") : "", props.length ? plural(props.length, "prop") : ""].filter(Boolean);
2192
- return {
2193
- title: `Add the ${component} component`,
2194
- summary: `${component} is not in code yet.${parts.length ? ` The Figma component has ${andList(parts)}.` : ""}`
2195
- };
2196
- }
2197
- case "new-variant": {
2198
- if (!c2) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2199
- const axis = c2.field.replace(/^Variant /, "");
2200
- const before = values(c2.before);
2201
- const after = values(c2.after);
2202
- const added = after.filter((x) => !before.includes(x));
2203
- const removed = before.filter((x) => !after.includes(x));
2204
- const word = axis.toLowerCase();
2205
- 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)}.` };
2206
- if (!after.length) return { title: `Remove the ${axis} variant axis`, summary: `${component} no longer has the ${axis} variant axis (${andList(before)}).` };
2207
- if (added.length === 1 && !removed.length) return { title: `Add ${article(added[0])} ${added[0]} ${word}`, summary: `${component} gets a new ${axis} value, ${added[0]}.` };
2208
- if (added.length && !removed.length) return { title: `Add ${andList(added)} to ${axis}`, summary: `${component} gets ${plural(added.length, `new ${axis} value`)}: ${andList(added)}.` };
2209
- if (removed.length === 1 && !added.length) return { title: `Remove the ${removed[0]} ${word}`, summary: `${component} no longer has the ${axis} value ${removed[0]}.` };
2210
- if (removed.length && !added.length) return { title: `Remove ${andList(removed)} from ${axis}`, summary: `${component} no longer has the ${axis} values ${andList(removed)}.` };
2211
- return { title: `Change the ${axis} values`, summary: `${axis} on ${component} goes from ${andList(before)} to ${andList(after)}.` };
2212
- }
2213
- case "prop-change": {
2214
- if (!c2) return { title: `Change a ${component} prop`, summary: `A prop of ${component} changed in Figma.` };
2215
- const name = c2.field.replace(/^Prop /, "");
2216
- const b = propShape(c2.before);
2217
- const a = propShape(c2.after);
2218
- if (!b && a) return { title: `Add the ${name} prop`, summary: `${component} gets a new ${a.type} prop, ${name}${a.def ? `, default ${a.def}` : ""}.` };
2219
- if (b && !a) return { title: `Remove the ${name} prop`, summary: `${component} no longer has the ${name} prop.` };
2220
- 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"}.` };
2221
- return { title: `Change the ${name} prop`, summary: `${name} on ${component} goes from ${c2.before ?? "none"} to ${c2.after ?? "none"}.` };
2222
- }
2223
- case "token-binding": {
2224
- if (!c2) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2225
- const m2 = /^(.*?)(?: \((.*)\))?$/.exec(c2.field);
2226
- const part = lower(m2[1]);
2227
- const where = m2[2] ? ` in ${m2[2]}` : "";
2228
- const after = c2.after && c2.after !== "none" ? c2.after : "";
2229
- const before = c2.before && c2.before !== "none" ? c2.before : "";
2230
- if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where} no longer uses ${before || "a token"}.` };
2231
- return {
2232
- title: `Use ${after} for the ${part}`,
2233
- summary: `The ${part} of ${component}${where} ${before ? `moves from ${before} to ${after}` : `now uses ${after}`}.`
2234
- };
2235
- }
2236
- case "visual":
2237
- return {
2238
- title: "Update the look to match Figma",
2239
- summary: `${component} looks different in Figma${c2?.field && c2.field !== "Look of every variant" ? ` (${lower(c2.field)})` : ""}, and no variable edit explains it.`
2240
- };
2241
- default:
2242
- return { title: "", summary: "" };
2243
- }
2244
- }
2245
- function draftRequestText(component, kind, changes) {
2246
- const name = component.trim() || "the component";
2247
- if (!changes.length && kind !== "new-component") return { title: "", summary: "" };
2248
- const d = firstChange(name, kind, changes[0], changes);
2249
- const rest = kind === "new-component" ? 0 : changes.length - 1;
2250
- const summary = rest > 0 ? `${d.summary} ${capital(plural(rest, "other change"))} ${rest === 1 ? "is" : "are"} listed below.` : d.summary;
2251
- return { title: cleanDraftText(d.title), summary: cleanDraftText(summary) };
2252
- }
2253
- const tokenOf = (e3) => e3.figma ?? e3.head ?? e3.base;
2254
- const pathOf = (e3) => tokenOf(e3)?.path ?? e3.path;
2255
- function pushChangeRows(entries) {
2256
- return entries.map((e3) => {
2257
- const before = e3.head ?? e3.base;
2258
- const after = e3.figma;
2259
- const show = (t3) => {
2260
- if (!t3) return void 0;
2261
- const modes = Object.keys(t3.values);
2262
- if (modes.length <= 1) return formatValue(t3.type, t3.values[modes[0] ?? ""]);
2263
- return modes.map((m2) => `${modeDisplayName(m2)} ${formatValue(t3.type, t3.values[m2])}`).join("; ");
2264
- };
2265
- const renamed = e3.local?.kind === "renamed" && before && after && before.path !== after.path;
2266
- return { field: renamed ? `${before.path} -> ${after.path}` : pathOf(e3), before: show(before), after: show(after) };
2267
- });
2268
- }
2269
- function names(paths) {
2270
- if (paths.length <= 3) return andList(paths);
2271
- return `${paths.length} tokens, including ${andList(paths.slice(0, 2))}`;
2272
- }
2273
- function valueModes(e3) {
2274
- return (e3.local?.fields ?? []).filter((f) => f.field === "value" && f.mode && f.mode !== "value").map((f) => modeDisplayName(f.mode));
2275
- }
2276
- function draftPushDescription(entries, components = []) {
2277
- const by2 = { changed: [], added: [], renamed: [], removed: [] };
2278
- for (const e3 of entries) by2[e3.local?.kind ?? "changed"].push(e3);
2279
- const clauses = [];
2280
- if (by2.changed.length) {
2281
- const only = by2.changed.length === 1 ? by2.changed[0] : void 0;
2282
- const fields = new Set((only?.local?.fields ?? []).map((f) => f.field));
2283
- const modes = only ? valueModes(only) : [];
2284
- if (only && fields.size === 1 && fields.has("description")) clauses.push(`updates the description of ${pathOf(only)}`);
2285
- else if (only && modes.length) clauses.push(`changes ${pathOf(only)} in ${andList(modes)}`);
2286
- else clauses.push(`changes ${names(by2.changed.map(pathOf))}`);
2287
- }
2288
- if (by2.added.length) clauses.push(`adds ${names(by2.added.map(pathOf))}`);
2289
- if (by2.renamed.length) {
2290
- const r2 = by2.renamed[0];
2291
- const from = (r2.head ?? r2.base)?.path;
2292
- clauses.push(by2.renamed.length === 1 && from && from !== pathOf(r2) ? `renames ${from} to ${pathOf(r2)}` : `renames ${names(by2.renamed.map(pathOf))}`);
2293
- }
2294
- if (by2.removed.length) clauses.push(`removes ${names(by2.removed.map(pathOf))}`);
2295
- const sentences2 = [];
2296
- if (clauses.length) sentences2.push(`${capital(andList(clauses))}.`);
2297
- const fresh = components.filter((c2) => c2.kind === "new-component").map((c2) => c2.component);
2298
- const changed = [...new Set(components.filter((c2) => c2.kind !== "new-component").map((c2) => c2.component))];
2299
- const comp = [];
2300
- if (changed.length) comp.push(`${changed.length === 1 ? "a change" : "changes"} to ${andList(changed)}`);
2301
- if (fresh.length) comp.push(`the new ${andList(fresh)} component${fresh.length === 1 ? "" : "s"}`);
2302
- if (comp.length) sentences2.push(`Requests ${andList(comp)}.`);
2303
- return cleanDraftText(sentences2.join(" "));
2304
- }
2305
3014
  const DRAFT_EXCHANGE_VERSION = 1;
2306
3015
  const DRAFTS_DIR = ".bridge/drafts";
2307
3016
  const DRAFT_PHRASE = "Draft my Bridge changes";
@@ -2314,12 +3023,16 @@ const DRAFT_VOICE_RULES = [
2314
3023
  "Use only facts from the changes, the images and the designer's notes. Never invent numbers, names, usage or reasons. If the reason is unknown, say what changes and leave the reason out.",
2315
3024
  "Keep the designer's intent from their notes.",
2316
3025
  "Never use an em dash: use a colon, a comma or parentheses. No exclamation marks, no emoji, no Markdown.",
2317
- "A request title never repeats the component name: the issue title already starts with it."
3026
+ "A request title never repeats the component name: the issue title already starts with it.",
3027
+ `For each change line, write one short sentence (at most ${DRAFT_LINE_MAX} characters, no trailing period) of what changed for developers, read from the before and after images: the part, the variant and the values when you can see them, for example "Icon added after the label in primary sm: Pencil, 16px". Never guess a value you cannot read.`,
3028
+ `For each change line, also write the title of the request it becomes: short, sentence case, starts with a verb, at most ${DRAFT_TITLE_MAX} characters, no trailing period, never the component name, for example "Add an icon after the label".`
2318
3029
  ];
2319
3030
  const DRAFT_OUTPUT_CONTRACT = [
2320
- `Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "..."}]}, one result per item.`,
3031
+ `Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "...", "lines": [{"id": "<line id>", "line": "...", "title": "..."}]}]}, one result per item.`,
2321
3032
  `title: one line, at most ${DRAFT_TITLE_MAX} characters, no trailing period.`,
2322
3033
  `summary: at most ${DRAFT_SUMMARY_SENTENCES} sentences, plain text.`,
3034
+ `lines: one per change line the item lists (its "lines"), by line id: line is one sentence, at most ${DRAFT_LINE_MAX} characters, no trailing period; title is the request title for that component change, at most ${DRAFT_TITLE_MAX} characters, no trailing period. Leave lines out when the item lists none.`,
3035
+ `Docs items (scope "docs", from Bridge Studio): follow the item's prompt and send {"id": "<item id>", "docs": {<only the sections it asks for>}} for a sections task (a section to check goes in only when something in it is no longer true for the code: leave accurate ones out, {"docs": {}} when all are), or {"id": "<item id>", "replacement": "..."} for a selection task. No title or summary. The designer reviews them on the Library page.`,
2323
3036
  "The designer reviews the text in Figma before anything is sent."
2324
3037
  ];
2325
3038
  function newDraftSessionId(random = Math.random) {
@@ -2344,7 +3057,7 @@ function composePendingDrafts(input) {
2344
3057
  sessionId: input.sessionId ?? newDraftSessionId(input.random),
2345
3058
  createdAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
2346
3059
  status: "pending",
2347
- source: "figma",
3060
+ source: input.source ?? "figma",
2348
3061
  items: input.items.map((i2) => ({ ...i2, changes: i2.changes ?? [] }))
2349
3062
  };
2350
3063
  const problems = validatePendingDrafts(p2);
@@ -2362,7 +3075,7 @@ function validatePendingDrafts(x) {
2362
3075
  if (!isDraftSessionId(p2.sessionId)) out.push("The session id is missing or invalid.");
2363
3076
  if (!str(p2.createdAt) || Number.isNaN(Date.parse(p2.createdAt))) out.push("createdAt must be an ISO date.");
2364
3077
  if (p2.status !== "pending" && p2.status !== "drafted") out.push('status must be "pending" or "drafted".');
2365
- if (p2.source !== "figma") out.push('source must be "figma".');
3078
+ if (p2.source !== "figma" && p2.source !== "studio") out.push('source must be "figma" or "studio".');
2366
3079
  if (!Array.isArray(p2.items) || !p2.items.length) out.push("A draft session needs at least one item.");
2367
3080
  const ids = /* @__PURE__ */ new Set();
2368
3081
  for (const [n2, i2] of (Array.isArray(p2.items) ? p2.items : []).entries()) {
@@ -2374,18 +3087,30 @@ function validatePendingDrafts(x) {
2374
3087
  if (!str(i2.id) || !i2.id) out.push(`${at2} has no id.`);
2375
3088
  else if (ids.has(i2.id)) out.push(`${at2} repeats the id "${i2.id}".`);
2376
3089
  else ids.add(i2.id);
2377
- if (i2.scope !== "request" && i2.scope !== "push") out.push(`${at2}: scope must be "request" or "push".`);
3090
+ if (i2.scope !== "request" && i2.scope !== "push" && i2.scope !== "docs") out.push(`${at2}: scope must be "request", "push" or "docs".`);
3091
+ if (i2.scope === "docs") {
3092
+ const d = i2.docs;
3093
+ if (!d || typeof d !== "object" || !str(d.slug) || !d.slug || !str(d.prompt) || !d.prompt || !optStr(d.path)) out.push(`${at2}: a docs item needs docs {slug, task, prompt}.`);
3094
+ else out.push(...validateDocsTask(d.task).map((m2) => `${at2}: ${m2}`));
3095
+ } else if (i2.docs !== void 0) out.push(`${at2}: only a docs item has docs.`);
2378
3096
  if (!optStr(i2.component)) out.push(`${at2}: component must be text.`);
2379
3097
  if (i2.kind !== void 0 && !KINDS.has(i2.kind)) out.push(`${at2}: unknown kind "${String(i2.kind)}".`);
2380
- if (!Array.isArray(i2.changes) || i2.changes.some((c2) => !c2 || !str(c2.field) || !optStr(c2.before) || !optStr(c2.after))) out.push(`${at2}: changes must be a list of {field, before, after}.`);
3098
+ if (!Array.isArray(i2.changes) || i2.changes.some((c10) => !c10 || !str(c10.field) || !optStr(c10.before) || !optStr(c10.after))) out.push(`${at2}: changes must be a list of {field, before, after}.`);
2381
3099
  if (i2.variables !== void 0 && (!Array.isArray(i2.variables) || i2.variables.some((v2) => !v2 || !str(v2.path)))) out.push(`${at2}: variables must be a list of {path, before, after}.`);
2382
3100
  if (i2.variables !== void 0 && i2.scope !== "push") out.push(`${at2}: only a push item has variables.`);
2383
3101
  if (i2.images !== void 0 && (!Array.isArray(i2.images) || i2.images.some((m2) => !m2 || m2.label !== "before" && m2.label !== "after" || !str(m2.url)))) out.push(`${at2}: images must be a list of {label: before or after, url}.`);
2384
3102
  if (!i2.draft || !str(i2.draft.title) || !str(i2.draft.summary)) out.push(`${at2}: draft must hold a title and a summary.`);
3103
+ if (i2.lines !== void 0 && (!Array.isArray(i2.lines) || i2.lines.some((l2) => !l2 || !str(l2.id) || !l2.id || !str(l2.line) || !optStr(l2.title) || !Array.isArray(l2.changes)))) out.push(`${at2}: lines must be a list of {id, changes, line, title}.`);
2385
3104
  }
2386
3105
  if (p2.results !== void 0) {
2387
3106
  if (!Array.isArray(p2.results)) out.push("results must be a list.");
2388
- else for (const r2 of p2.results) if (!r2 || !str(r2.id) || !str(r2.title) || !str(r2.summary)) out.push("Each result needs an id, a title and a summary.");
3107
+ else
3108
+ for (const r2 of p2.results) {
3109
+ if (!r2 || !str(r2.id) || !str(r2.title) || !str(r2.summary)) out.push("Each result needs an id, a title and a summary.");
3110
+ else if (r2.docs !== void 0 && (!r2.docs || typeof r2.docs !== "object" || Array.isArray(r2.docs))) out.push("Result docs must be an object of sections.");
3111
+ else if (!optStr(r2.replacement)) out.push("A result replacement must be text.");
3112
+ else if (r2.lines !== void 0 && (!Array.isArray(r2.lines) || r2.lines.some((l2) => !l2 || !str(l2.id) || !str(l2.line) || !optStr(l2.title)))) out.push("Result lines must be a list of {id, line, title}.");
3113
+ }
2389
3114
  }
2390
3115
  return out;
2391
3116
  }
@@ -2422,31 +3147,88 @@ function sanitizeDraftText(raw) {
2422
3147
  const summary = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
2423
3148
  return { title, summary };
2424
3149
  }
3150
+ function sanitizeDocsResult(item, r2) {
3151
+ const task = item.docs.task;
3152
+ if (task.kind === "selection") {
3153
+ const raw = r2.replacement ?? (r2.docs && typeof r2.docs === "object" ? r2.docs[task.key] : void 0);
3154
+ const replacement = sanitizeDocsReplacement(raw);
3155
+ if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
3156
+ return { replacement };
3157
+ }
3158
+ try {
3159
+ const { sections, checked } = docsSectionsAnswer(task, r2.docs ?? {});
3160
+ return { docs: sections, checked };
3161
+ } catch {
3162
+ throw new Error(`Item "${item.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item.id}", "docs": {${task.keys.map((k3) => `"${k3}": ...`).join(", ")}}} in the shapes of its prompt.`);
3163
+ }
3164
+ }
3165
+ function sanitizeResultLines(item, raw) {
3166
+ if (raw === void 0 || raw === null) return [];
3167
+ if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
3168
+ const known = new Set((item.lines ?? []).map((l2) => l2.id));
3169
+ const out = /* @__PURE__ */ new Map();
3170
+ for (const l2 of raw) {
3171
+ const id = typeof l2?.id === "string" ? l2.id : "";
3172
+ if (!known.has(id)) throw new Error(`Item "${item.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
3173
+ const line = sanitizeDraftLine(l2.line);
3174
+ const title = sanitizeDraftTitle(l2.title);
3175
+ if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
3176
+ }
3177
+ return [...out.values()];
3178
+ }
3179
+ function sanitizeDraftTitle(raw) {
3180
+ if (typeof raw !== "string") return "";
3181
+ const t3 = clip(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_TITLE_MAX);
3182
+ return t3.charAt(0).toUpperCase() + t3.slice(1);
3183
+ }
2425
3184
  function mergeDraftResults(p2, results, at2 = /* @__PURE__ */ new Date()) {
2426
3185
  const ids = new Set(p2.items.map((i2) => i2.id));
2427
3186
  const next = new Map((p2.results ?? []).map((r2) => [r2.id, r2]));
2428
3187
  for (const r2 of results) {
2429
3188
  if (!ids.has(r2.id)) throw new Error(`Session ${p2.sessionId} has no item "${r2.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
3189
+ const item = p2.items.find((i2) => i2.id === r2.id);
3190
+ if (item.scope === "docs") {
3191
+ next.set(r2.id, { id: r2.id, title: "", summary: "", ...sanitizeDocsResult(item, r2), by: "claude", at: at2.toISOString() });
3192
+ continue;
3193
+ }
2430
3194
  const clean = sanitizeDraftText(r2);
2431
- if (!clean.title && !clean.summary) throw new Error(`The result for "${r2.id}" has no title and no summary.`);
2432
- next.set(r2.id, { id: r2.id, title: clean.title, summary: clean.summary, by: "claude", at: at2.toISOString() });
3195
+ const lines = sanitizeResultLines(item, r2.lines);
3196
+ if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r2.id}" has no title and no summary, and no line.`);
3197
+ next.set(r2.id, { id: r2.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at2.toISOString() });
2433
3198
  }
2434
3199
  const merged = p2.items.map((i2) => next.get(i2.id)).filter((r2) => !!r2);
2435
3200
  return { ...p2, results: merged, status: merged.length === p2.items.length ? "drafted" : "pending" };
2436
3201
  }
2437
3202
  function draftItemBrief(i2) {
3203
+ if (i2.scope === "docs" && i2.docs) {
3204
+ const t3 = i2.docs.task;
3205
+ const check = t3.kind === "sections" ? (t3.check ?? []).filter((k3) => t3.keys.includes(k3)) : [];
3206
+ const write = t3.kind === "sections" ? t3.keys.filter((k3) => !check.includes(k3)) : [];
3207
+ const what = t3.kind === "selection" ? `rewrite one passage of the ${t3.key} section (${t3.mode}), answer with "replacement"` : [
3208
+ write.length ? `${t3.rewrite ? "rewrite" : "write"} the sections ${write.join(", ")}` : "",
3209
+ check.length ? `check the sections ${check.join(", ")} against the code and return one only when something in it is no longer true (leave accurate ones out)` : ""
3210
+ ].filter(Boolean).join("; ") + ', answer with "docs"';
3211
+ return [`Item ${i2.id}: component docs${i2.component ? ` of ${i2.component}` : ""} from Bridge Studio (${i2.docs.path ?? i2.docs.slug}): ${what}.`, "", i2.docs.prompt].join("\n");
3212
+ }
2438
3213
  const kind = i2.kind ? REQUEST_KINDS.find((k3) => k3.value === i2.kind)?.label : void 0;
2439
3214
  const cell2 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
2440
3215
  const lines = [`Item ${i2.id}: ${i2.scope === "request" ? "a component change request (a GitHub issue developers implement)" : "the description of a pull request with design token (variable) edits from Figma"}.`];
2441
3216
  if (i2.component) lines.push(`Component: ${i2.component}`);
2442
3217
  if (kind) lines.push(`Kind: ${kind}`);
2443
3218
  if (i2.components?.length) lines.push(`Components with a change request: ${i2.components.join(", ")}`);
2444
- if (i2.changes.length) lines.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i2.changes.map((c2) => `| ${cell2(c2.field)} | ${cell2(c2.before) || "none"} | ${cell2(c2.after) || "removed"} |`));
3219
+ if (i2.changes.length) lines.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i2.changes.map((c10) => `| ${cell2(c10.field)} | ${cell2(c10.before) || "none"} | ${cell2(c10.after) || "removed"} |`));
2445
3220
  if (i2.variables?.length) lines.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i2.variables.map((v2) => `| ${cell2(v2.path)} | ${cell2(v2.before) || "none"} | ${cell2(v2.after) || "removed"} |`));
2446
- if (!i2.changes.length && !i2.variables?.length) lines.push("", "No change detected: rely on the notes and the images.");
3221
+ if (!i2.changes.length && !i2.variables?.length && !i2.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
2447
3222
  if (i2.draft.title || i2.draft.summary) lines.push("", "Starting draft, written from the changes only:", `Title: ${i2.draft.title}`, `Summary: ${i2.draft.summary}`);
2448
3223
  if (i2.notes?.trim()) lines.push("", "The designer's notes (keep their intent):", i2.notes.trim());
2449
- if (i2.images?.length) lines.push("", `Images: ${i2.images.map((m2) => `${m2.label} ${m2.url}`).join(", ")}`);
3224
+ if (i2.lines?.length) {
3225
+ lines.push("", `Change lines to write (one sentence each, at most ${DRAFT_LINE_MAX} characters, from the before and after images), each with the title of its request (at most ${DRAFT_TITLE_MAX} characters):`);
3226
+ for (const l2 of i2.lines) {
3227
+ lines.push(`- Line ${l2.id}${l2.component ? `, ${l2.component}` : ""}${l2.kind ? ` (${REQUEST_KINDS.find((k3) => k3.value === l2.kind)?.label ?? l2.kind})` : ""}. Starting line: ${l2.line || "(none)"}${l2.title ? `. Starting title: ${l2.title}` : ""}`);
3228
+ for (const c10 of l2.changes) lines.push(` - ${c10.field}: ${c10.before || "none"} -> ${c10.after || "removed"}`);
3229
+ }
3230
+ }
3231
+ if (i2.images?.length) lines.push("", `Images: ${i2.images.map((m2) => `${m2.label}${m2.line ? ` (line ${m2.line})` : ""} ${m2.url}`).join(", ")}`);
2450
3232
  return lines.join("\n");
2451
3233
  }
2452
3234
  const isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
@@ -2507,7 +3289,96 @@ async function deletePendingDrafts(client, sessionId, opts = {}) {
2507
3289
  }
2508
3290
  return true;
2509
3291
  }
2510
- function explorationSummary(p2, owner, repo, s) {
3292
+ async function ensureRequestsBranch(client, baseBranch) {
3293
+ if (await client.getBranchSha(REQUESTS_BRANCH)) return;
3294
+ const head = await client.getBranchSha(baseBranch);
3295
+ if (!head) throw new Error(`Branch "${baseBranch}" was not found, so the ${REQUESTS_BRANCH} branch cannot start from it.`);
3296
+ await client.createBranch(REQUESTS_BRANCH, head);
3297
+ }
3298
+ async function createDocsDraftSession(client, input) {
3299
+ await ensureRequestsBranch(client, input.baseBranch);
3300
+ const what = input.task.kind === "selection" ? `Rewrite a passage of ${input.task.key}` : `${input.task.rewrite ? "Rewrite" : "Write"} ${input.task.keys.join(", ")}`;
3301
+ const item = {
3302
+ id: "docs-1",
3303
+ scope: "docs",
3304
+ component: input.component,
3305
+ changes: [],
3306
+ draft: { title: `${input.component} docs`, summary: what },
3307
+ docs: { slug: input.slug, ...input.path ? { path: input.path } : {}, task: input.task, prompt: input.prompt }
3308
+ };
3309
+ return createPendingDrafts(client, composePendingDrafts({ items: [item], source: "studio", now: input.now, random: input.random }));
3310
+ }
3311
+ const DRAFT_POLL_INTERVAL_MS = 5e3;
3312
+ const DRAFT_POLL_TIMEOUT_MS = 10 * 6e4;
3313
+ class DraftPoller {
3314
+ constructor(o2) {
3315
+ this.o = o2;
3316
+ }
3317
+ o;
3318
+ handle;
3319
+ startedAt = 0;
3320
+ running = false;
3321
+ busy = false;
3322
+ get active() {
3323
+ return this.running;
3324
+ }
3325
+ now() {
3326
+ return (this.o.now ?? Date.now)();
3327
+ }
3328
+ schedule() {
3329
+ const set = this.o.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
3330
+ this.handle = set(() => void this.tick(), this.o.intervalMs ?? DRAFT_POLL_INTERVAL_MS);
3331
+ }
3332
+ /** Starts (or restarts, after a timeout) polling; the timeout counts from now. */
3333
+ start() {
3334
+ this.stop();
3335
+ this.running = true;
3336
+ this.startedAt = this.now();
3337
+ this.schedule();
3338
+ }
3339
+ stop() {
3340
+ if (this.handle !== void 0) (this.o.clearTimer ?? ((h3) => clearTimeout(h3)))(this.handle);
3341
+ this.handle = void 0;
3342
+ this.running = false;
3343
+ }
3344
+ async tick() {
3345
+ if (!this.running) return;
3346
+ this.handle = void 0;
3347
+ const done = await this.check();
3348
+ if (done || !this.running) return;
3349
+ if (this.now() - this.startedAt >= (this.o.timeoutMs ?? DRAFT_POLL_TIMEOUT_MS)) {
3350
+ this.stop();
3351
+ this.o.onTimeout();
3352
+ return;
3353
+ }
3354
+ this.schedule();
3355
+ }
3356
+ /** Reads the file once. True when polling is over (answered or gone). */
3357
+ async check() {
3358
+ if (this.busy) return false;
3359
+ this.busy = true;
3360
+ try {
3361
+ const cur = await readPendingDrafts(this.o.client, this.o.sessionId);
3362
+ if (!cur) {
3363
+ this.stop();
3364
+ this.o.onGone?.();
3365
+ return true;
3366
+ }
3367
+ if (cur.drafts.status === "drafted" && cur.drafts.results?.length) {
3368
+ this.stop();
3369
+ this.o.onResults(cur.drafts.results, cur.drafts);
3370
+ return true;
3371
+ }
3372
+ return false;
3373
+ } catch (err) {
3374
+ this.o.onError?.(err instanceof Error ? err.message : String(err));
3375
+ return false;
3376
+ } finally {
3377
+ this.busy = false;
3378
+ }
3379
+ }
3380
+ }
3381
+ function explorationSummary(p2, _owner, _repo, s) {
2511
3382
  const e3 = parseExplorationBody(p2.body);
2512
3383
  const slug = p2.headRef?.startsWith(EXPLORATION_PREFIX) ? p2.headRef.slice(EXPLORATION_PREFIX.length) : e3?.slug ?? explorationSlug(p2.title);
2513
3384
  return {
@@ -2522,8 +3393,8 @@ function explorationSummary(p2, owner, repo, s) {
2522
3393
  draft: p2.draft,
2523
3394
  author: e3?.author ?? p2.author,
2524
3395
  updatedAt: p2.updatedAt,
2525
- previews: previewLinks(s.apps, { branch: p2.headRef, pr: p2.number, sha: p2.headSha }),
2526
- install: explorationInstall({ owner, repo, pr: p2.number, packages: s.packages, sha: p2.headSha })
3396
+ headSha: p2.headSha,
3397
+ previews: previewLinks(s.apps, { branch: p2.headRef, pr: p2.number, sha: p2.headSha })
2527
3398
  };
2528
3399
  }
2529
3400
  async function listExplorationsOnGitHub(gh, s, opts = {}) {
@@ -2549,7 +3420,7 @@ ${full.purpose.trim()}
2549
3420
  Exploration: never merged as is. Promote it to review or close it.`,
2550
3421
  files: [{ path: explorationNotePath(slug), content: explorationNote(full) }]
2551
3422
  });
2552
- const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps };
3423
+ const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
2553
3424
  const pr = await x.createPullRequest({ title: explorationPrTitle(full), head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
2554
3425
  await x.addLabels(pr.number, [EXPLORATION_LABEL]);
2555
3426
  await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
@@ -2559,10 +3430,20 @@ Exploration: never merged as is. Promote it to review or close it.`,
2559
3430
  pr: pr.number,
2560
3431
  url: pr.url,
2561
3432
  commit: commit.sha,
2562
- install: explorationInstall({ owner: gh.owner, repo: gh.name, pr: pr.number, packages: s.packages, sha: commit.sha }),
3433
+ tryIt: { state: "building" },
2563
3434
  previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
2564
3435
  };
2565
3436
  }
3437
+ async function explorationTryItOnGitHub(gh, e3) {
3438
+ const [comments, head] = await Promise.all([gh.listIssueComments(e3.number), gh.getBranchSha(e3.branch).catch(() => void 0)]);
3439
+ return explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
3440
+ }
3441
+ async function explorationTestBuildOnGitHub(gh, e3, config) {
3442
+ const [comments, head] = await Promise.all([gh.listIssueComments(e3.number), gh.getBranchSha(e3.branch).catch(() => void 0)]);
3443
+ const tryIt = explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
3444
+ const previews = await branchPreviewUrls(gh, e3.number, config, { branch: e3.branch, sha: head, comments }).catch(() => []);
3445
+ return { tryIt, previews, settled: tryIt.state !== "building" && previewsSettled(previews) };
3446
+ }
2566
3447
  async function findExplorationOnGitHub(gh, ref) {
2567
3448
  if (typeof ref === "number" || /^#?\d+$/.test(String(ref))) {
2568
3449
  const p2 = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
@@ -2586,6 +3467,7 @@ async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
2586
3467
  const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
2587
3468
  if (plan.removeFiles.length) {
2588
3469
  await x.commitToBranch({ branch: p2.headRef, message: "chore(explore): promote to review, remove the exploration note", files: plan.removeFiles.map((path) => ({ path, content: null })) }).catch((e3) => {
3470
+ if (e3 instanceof GitHubError && e3.auth === "rejected") throw e3;
2589
3471
  out.warnings.push(`The exploration note stays on the branch (${e3.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
2590
3472
  });
2591
3473
  }
@@ -2605,35 +3487,76 @@ async function closeExplorationOnGitHub(gh, ref) {
2605
3487
  await gh.updatePullRequest(p2.number, { state: "closed" });
2606
3488
  return { number: p2.number, url: p2.url, branch: p2.headRef };
2607
3489
  }
3490
+ async function linkRequestToExplorationOnGitHub(gh, n2, branch, opts = {}) {
3491
+ if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
3492
+ const issue = await gh.getIssue(n2);
3493
+ const body = withRequestExploration(issue.body, branch);
3494
+ if (!body) throw new GitHubError(422, `#${n2} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
3495
+ await gh.updateIssue(n2, { body });
3496
+ const warnings = [];
3497
+ if (!await gh.addLabels(n2, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n2}.`);
3498
+ let assignees = [];
3499
+ if (opts.assignee) {
3500
+ try {
3501
+ assignees = await gh.addAssignees(n2, [opts.assignee.replace(/^@/, "")]);
3502
+ } catch {
3503
+ warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n2}.`);
3504
+ }
3505
+ }
3506
+ return { number: n2, branch, assignees, warnings };
3507
+ }
3508
+ async function assignRequestOnGitHub(gh, n2, login) {
3509
+ const who = login.trim().replace(/^@/, "");
3510
+ if (!who) throw new GitHubError(422, "Pick who should work on it.");
3511
+ const assignees = await gh.addAssignees(n2, [who]);
3512
+ if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
3513
+ return assignees;
3514
+ }
3515
+ async function declineRequestOnGitHub(gh, n2, reason, by2) {
3516
+ const problems = declineReasonProblems(reason);
3517
+ if (problems.length) throw new GitHubError(422, problems.join(" "));
3518
+ await gh.addComment(n2, declineComment(reason, by2));
3519
+ await gh.updateIssue(n2, { state: "closed", state_reason: "not_planned" });
3520
+ }
2608
3521
  export {
2609
- ae as AAA_MIN,
2610
- af as AA_MIN,
3522
+ af as AAA_MIN,
3523
+ ag as AA_MIN,
3524
+ ADMIN_ROLE_ID,
3525
+ ANTHROPIC_MESSAGES_URL,
2611
3526
  BELL,
2612
3527
  BRAND_FONTS,
2613
3528
  BRAND_SLUG_RE,
2614
- ag as BULK_LIMIT,
3529
+ ah as BULK_LIMIT,
2615
3530
  k as COLOR_FIELDS,
2616
3531
  C as COMPONENT_SPECS_VERSION,
2617
3532
  CONFIG_FILE,
2618
- ah as CONTRAST_CELL_MAX,
2619
- ai as CONTRAST_CHECKS_FILE,
3533
+ ai as CONTRAST_CELL_MAX,
3534
+ aj as CONTRAST_CHECKS_FILE,
2620
3535
  ConfigError,
2621
- aj as DEFAULT_COMPONENT_SPECS_FILE,
2622
- ak as DEFAULT_GOVERNANCE,
2623
- al as DEFAULT_OVERRIDE,
3536
+ ak as DEFAULT_COMPONENT_SPECS_FILE,
3537
+ al as DEFAULT_GOVERNANCE,
3538
+ am as DEFAULT_OVERRIDE,
2624
3539
  DEFAULT_PROTECTED_BRANCHES,
3540
+ an as DEFAULT_RIGHTS,
2625
3541
  DEFAULT_TOKEN_PATHS,
3542
+ ao as DEFAULT_VISUAL_IMPACT,
2626
3543
  DEMO_RELEASES,
2627
3544
  DOCS_CHANGE_BUMP,
3545
+ DOCS_DRAFT_MODEL,
2628
3546
  DOCS_KEYS,
3547
+ DOCS_SELECTION_MODES,
2629
3548
  DRAFTS_DIR,
2630
3549
  DRAFT_EXCHANGE_VERSION,
3550
+ DRAFT_LINE_MAX,
2631
3551
  DRAFT_OUTPUT_CONTRACT,
2632
3552
  DRAFT_PHRASE,
3553
+ DRAFT_POLL_INTERVAL_MS,
3554
+ DRAFT_POLL_TIMEOUT_MS,
2633
3555
  DRAFT_SUMMARY_SENTENCES,
2634
3556
  DRAFT_TITLE_MAX,
2635
3557
  DRAFT_VOICE_RULES,
2636
3558
  D as DTCG_TYPES,
3559
+ DraftPoller,
2637
3560
  EXPLORATION_CHECK,
2638
3561
  EXPLORATION_LABEL,
2639
3562
  EXPLORATION_NOTES_DIR,
@@ -2645,66 +3568,86 @@ export {
2645
3568
  EXT_SCOPES,
2646
3569
  GitHubClient,
2647
3570
  GitHubError,
2648
- am as JsonParseError,
3571
+ ap as JsonParseError,
2649
3572
  LIGHTNESS,
2650
3573
  LayeredTokenSet,
2651
3574
  NEUTRAL_ANCHORS,
3575
+ aq as OWNER_GROUPS,
2652
3576
  PIN_TOLERANCE,
2653
- an as PREVIEW_APP_LABELS,
2654
- ao as PREVIEW_COMMENT_MARKER,
2655
- ap as PREVIEW_PLACEHOLDERS,
2656
- aq as PRIMITIVE_MODE,
3577
+ ar as PREVIEW_APP_LABELS,
3578
+ as as PREVIEW_COMMENT_MARKER,
3579
+ at as PREVIEW_PLACEHOLDERS,
3580
+ au as PRIMITIVE_MODE,
2657
3581
  PROSE_KEYS,
3582
+ PROTECTION_PARTS,
3583
+ PROTECTION_PART_PHRASE,
2658
3584
  ProtectedBranchError,
2659
3585
  RADIUS_PRESETS,
2660
3586
  REQUESTS_BRANCH,
2661
3587
  REQUEST_KINDS,
2662
3588
  REQUEST_LABEL,
2663
3589
  REQUEST_STATUS_LABEL,
3590
+ av as RIGHTS,
3591
+ aw as RIGHT_LABEL,
2664
3592
  RISK_LABELS,
2665
- ar as RISK_TIER,
3593
+ ax as RISK_TIER,
2666
3594
  SIMILARITY_THRESHOLD,
2667
3595
  SINGLE_MODE,
2668
- as as SPEC_FIELDS,
3596
+ ay as SPEC_FIELDS,
2669
3597
  STEPS,
2670
3598
  TIER_AXIS,
2671
3599
  TIER_NAMES,
2672
- at as TIER_ORDER,
2673
- au as UNRELEASED,
3600
+ az as TIER_ORDER,
3601
+ TOKEN_REJECTED_MESSAGE,
3602
+ TOKEN_SETTINGS_URL,
3603
+ aA as TeamDirectory,
3604
+ aB as UNRELEASED,
3605
+ aC as VISUAL_CHECK_LABEL,
2674
3606
  acceptDocsDraft,
2675
3607
  acceptDocsText,
2676
3608
  addBrandToConfigText,
2677
- av as affectedContrast,
3609
+ aD as affectedContrast,
2678
3610
  e2 as aliasOf,
2679
3611
  aliasTarget,
2680
3612
  anatomyLabel,
2681
3613
  anchoredRamp,
2682
- aw as appUrl,
3614
+ anthropicReplyText,
3615
+ anthropicRequest,
3616
+ aE as appForProject,
3617
+ aF as appUrl,
2683
3618
  applyConfirmedRenames,
3619
+ applyDocsSections,
2684
3620
  applyTokenEdits,
3621
+ aG as approverEntries,
3622
+ aH as approverLists,
2685
3623
  assertSafeBranch,
2686
- ax as assignVersions,
3624
+ assignRequestOnGitHub,
3625
+ aI as assignVersions,
2687
3626
  base64ToUtf8,
2688
- ay as baseBranchOf,
3627
+ aJ as baseBranchOf,
2689
3628
  bellChroma,
2690
- az as branchName,
2691
- aA as branchSlug,
3629
+ aK as branchName,
3630
+ branchPreviewUrls,
3631
+ aL as branchSlug,
2692
3632
  brandDisplayName,
2693
3633
  brandPreviewVariables,
2694
3634
  buildHistoryEntry,
2695
3635
  buildReport,
2696
3636
  i as buildTokenSet,
2697
3637
  buildUsageIndex,
2698
- aB as changeKind,
2699
- aC as changeTable,
2700
- aD as changedModes,
2701
- aE as changesetBump,
3638
+ aM as canShip,
3639
+ aN as changeKind,
3640
+ aO as changeTable,
3641
+ aP as changedModes,
3642
+ aQ as changesetBump,
2702
3643
  checkBrandSlug,
2703
3644
  checkLayeredPair,
2704
- aF as classifyLayered,
3645
+ classifyAuth,
3646
+ aR as classifyLayered,
2705
3647
  classifyRisk,
3648
+ cleanDocsText,
2706
3649
  cleanDraftText,
2707
- aG as clearRawDeep,
3650
+ aS as clearRawDeep,
2708
3651
  closeExplorationOnGitHub,
2709
3652
  closingIssueNumbers,
2710
3653
  coerceValue,
@@ -2713,39 +3656,50 @@ export {
2713
3656
  j2 as colorToCss,
2714
3657
  k2 as colorsEqual,
2715
3658
  c as comboKey,
2716
- aH as commitMessage,
3659
+ aT as commitMessage,
2717
3660
  l as compactJson,
2718
3661
  componentForPath,
2719
- aI as composeChangeset,
3662
+ aU as composeChangeset,
2720
3663
  composeFiles,
2721
3664
  composeNewBrandBody,
2722
3665
  composePendingDrafts,
2723
- aJ as composePrBody,
2724
- aK as composeProposalBody,
3666
+ aV as composePrBody,
3667
+ aW as composeProposalBody,
2725
3668
  composeRequestIssue,
2726
- aL as composite,
2727
- aM as contextFor,
3669
+ aX as composite,
3670
+ aY as contextFor,
2728
3671
  contextLabel,
2729
- aN as contrastDelta,
2730
- aO as contrastDeltaTable,
2731
- aP as contrastLevel,
2732
- aQ as contrastPair,
2733
- aR as contrastRatio,
3672
+ aZ as contrastDelta,
3673
+ a_ as contrastDeltaTable,
3674
+ a$ as contrastLevel,
3675
+ b0 as contrastPair,
3676
+ b1 as contrastRatio,
2734
3677
  contrastScope,
2735
- aS as countChanges,
3678
+ b2 as countChanges,
3679
+ createDocsDraftSession,
2736
3680
  createPendingDrafts,
2737
3681
  cssModuleClasses,
2738
3682
  cssVarsIn,
2739
- aT as defaultContext,
3683
+ declineComment,
3684
+ declineReasonProblems,
3685
+ declineRequestOnGitHub,
3686
+ b3 as defaultContext,
2740
3687
  defaultFile,
2741
3688
  defaultScopes,
2742
- aU as defaultTitle,
2743
- aV as defineConfig,
3689
+ b4 as defaultTitle,
3690
+ b5 as defineConfig,
2744
3691
  deletePendingDrafts,
2745
3692
  demoHistory,
2746
- aW as detectLayout,
2747
- aX as diffLayered,
3693
+ b6 as detectLayout,
3694
+ b7 as diffLayered,
2748
3695
  docsPrompt,
3696
+ docsSectionShapes,
3697
+ docsSectionsAnswer,
3698
+ docsSectionsResult,
3699
+ docsSelectionPrompt,
3700
+ docsTaskKeys,
3701
+ docsTaskPrompt,
3702
+ draftChangeLine,
2749
3703
  draftImagePath,
2750
3704
  draftItemBrief,
2751
3705
  draftPushDescription,
@@ -2754,78 +3708,104 @@ export {
2754
3708
  dtcgColor,
2755
3709
  effectiveScopes,
2756
3710
  emptyTokenFile,
3711
+ ensureRequestsBranch,
3712
+ b8 as entriesFor,
3713
+ b9 as entryLabel,
2757
3714
  evaluatePairs,
3715
+ ba as expandEntries,
3716
+ bb as expandOwners,
2758
3717
  explorationBranch,
2759
- explorationInstall,
2760
3718
  explorationNote,
2761
3719
  explorationNotePath,
2762
3720
  explorationPrBody,
2763
3721
  explorationPrTitle,
2764
3722
  explorationProblems,
2765
3723
  explorationRules,
3724
+ explorationShareMessage,
2766
3725
  explorationSlug,
2767
3726
  explorationSlugFromBranch,
2768
3727
  explorationStatus,
2769
3728
  explorationSummary,
2770
- aY as fieldChanges,
2771
- aZ as fileModes,
2772
- a_ as filterHistory,
3729
+ explorationTestBuildOnGitHub,
3730
+ explorationTryIt,
3731
+ explorationTryItOnGitHub,
3732
+ bc as fieldChanges,
3733
+ bd as fileModes,
3734
+ be as filterHistory,
2773
3735
  findExplorationOnGitHub,
3736
+ findPreviewComment,
2774
3737
  findReferenceProblems,
2775
- a$ as findToken,
2776
- b0 as firstParagraph,
3738
+ bf as findToken,
3739
+ bg as findVercelComment,
3740
+ bh as firstParagraph,
2777
3741
  m as flattenLayerFile,
2778
- b1 as flattenTokens,
3742
+ bi as flattenTokens,
2779
3743
  formatChangeset,
2780
3744
  formatDocsJson,
2781
- b2 as formatNumber,
3745
+ bj as formatNumber,
3746
+ bk as formatOwnerEntry,
2782
3747
  formatValue,
2783
3748
  friendlyMessage,
2784
3749
  fromHex,
2785
3750
  fromPlain,
2786
3751
  generateRamp,
2787
- b3 as getChild,
3752
+ bl as getChild,
2788
3753
  getEntry,
2789
3754
  gitHubHistoryBackend,
2790
3755
  g2 as globToRegExp,
3756
+ bm as groupsPhrase,
2791
3757
  h as hashSpec,
2792
3758
  h2 as hexToRgba,
3759
+ bn as holdersOf,
2793
3760
  j as iconComponentName,
2794
3761
  idExtensionFor,
2795
3762
  impactedModules,
2796
3763
  importsIn,
2797
- b4 as indexByPath,
2798
- b5 as inheritedTypeAt,
2799
- installCommand,
3764
+ bo as indexByPath,
3765
+ bp as inheritedTypeAt,
3766
+ bq as installCommand,
3767
+ br as installCommandFromPreviewComment,
2800
3768
  isActiveExploration,
2801
3769
  isAlias,
3770
+ isDocsSelectionMode,
2802
3771
  isDraftSessionId,
2803
3772
  isExplorationBranch,
2804
3773
  isExplorationPull,
2805
- b6 as isGuardrail,
3774
+ bs as isGuardrail,
2806
3775
  isHex,
3776
+ isMissingPermission,
2807
3777
  isMultiMode,
2808
- b7 as isTokenFile,
3778
+ isPlanRefusal,
3779
+ isProseKey,
3780
+ bt as isTeamEntry,
3781
+ bu as isTokenFile,
3782
+ isTokenRejected,
3783
+ bv as isVercelComment,
2809
3784
  jiraIssueFields,
2810
- b8 as kindForMin,
2811
- b9 as layeredImpact,
3785
+ bw as kindForMin,
3786
+ bx as layeredImpact,
2812
3787
  n as layeredParityIssues,
2813
3788
  g as layoutOptions,
2814
3789
  levenshtein,
3790
+ linkRequestToExplorationOnGitHub,
2815
3791
  listBrands,
2816
3792
  listExplorationsOnGitHub,
2817
3793
  listPendingDrafts,
2818
3794
  loadConfig,
2819
3795
  loadGitHubHistory,
3796
+ loadProtection,
2820
3797
  loadRepoFiles,
2821
- ba as luminance,
3798
+ loadRequiredChecks,
3799
+ by as luminance,
2822
3800
  markClaudeDraft,
3801
+ mergeDocsDraft,
2823
3802
  mergeDraftResults,
2824
3803
  mergeReleases,
3804
+ missingPermissionMessage,
2825
3805
  missingProse,
2826
3806
  modeDisplayName,
2827
3807
  modeFile,
2828
- bb as modeLabel,
3808
+ bz as modeLabel,
2829
3809
  modesOf$1 as modesOf,
2830
3810
  moveToken,
2831
3811
  nearestStep,
@@ -2834,102 +3814,146 @@ export {
2834
3814
  neutralOffset,
2835
3815
  newBrandSection,
2836
3816
  newDraftSessionId,
2837
- bc as nodesEqual,
3817
+ bA as nodesEqual,
3818
+ bB as normalizeRights,
2838
3819
  oklch,
2839
3820
  oklchToLinear,
2840
- bd as overrideKey,
3821
+ bC as overrideKey,
3822
+ bD as ownerEntryProblems,
2841
3823
  packageDirCandidates,
2842
- be as pairKey,
3824
+ bE as pairKey,
2843
3825
  parseChangelog,
2844
- bf as parseChangesetText,
3826
+ bF as parseChangesetText,
2845
3827
  p as parseColor,
2846
- bg as parseComponentSpecs,
3828
+ bG as parseComponentSpecs,
2847
3829
  parseContrastChecks,
2848
3830
  o as parseDimension,
3831
+ parseDocsReply,
3832
+ parseDocsTaskAnswer,
2849
3833
  parseExplorationBody,
2850
3834
  parseJson,
3835
+ bH as parseOwnerEntry,
2851
3836
  parsePendingDrafts,
2852
3837
  parseRepo,
2853
3838
  parseRequestIssue,
3839
+ parseStructureChange,
2854
3840
  parseTokenFile,
3841
+ bI as parseVercelComment,
2855
3842
  patchDocsJson,
2856
3843
  pathId,
2857
3844
  pathSimilarity,
3845
+ permissionLabel,
2858
3846
  plainEqual,
2859
3847
  planNewBrand,
2860
- bh as planRestore,
2861
- bi as planRevert,
2862
- bj as planToJson,
3848
+ bJ as planRestore,
3849
+ bK as planRevert,
3850
+ bL as planToJson,
2863
3851
  e as prNumberFromSubject,
2864
3852
  prefillDocs,
2865
- bk as previewComment,
3853
+ bM as previewComment,
2866
3854
  previewLinks,
2867
- bl as previewUrl,
3855
+ bN as previewLinksOf,
3856
+ bO as previewUrl,
3857
+ previewsSettled,
3858
+ bP as projectOfLabel,
2868
3859
  promoteExplorationOnGitHub,
2869
3860
  promotionPlan,
2870
3861
  propsTable,
2871
- bm as protectedBranchesOf,
3862
+ protectLibrary,
3863
+ protectRefusal,
3864
+ bQ as protectedBranchesOf,
3865
+ protectionRuleset,
3866
+ protectionRulesetName,
3867
+ protectionSentence,
3868
+ protectionState,
2872
3869
  pushChangeRows,
2873
3870
  readPendingDrafts,
3871
+ readPreviewComment,
2874
3872
  readTokenAt,
2875
3873
  reconcileIds,
2876
3874
  releasesFromTags,
2877
3875
  removeToken,
2878
- bn as renderedColor,
3876
+ bR as renderedColor,
2879
3877
  requestBrief,
2880
3878
  requestImagePath,
2881
3879
  requestIssueTitle,
2882
3880
  requestStatus,
2883
- bo as resolve,
3881
+ requiredCheckNames,
3882
+ requiredPermission,
3883
+ bS as resolve,
3884
+ resolveBranchPreviews,
2884
3885
  r as rgbaToDtcg,
2885
3886
  q as rgbaToHex,
3887
+ bT as rightsProblems,
2886
3888
  round3,
2887
- bp as runLayeredContrast,
3889
+ rulesSettingsUrl,
3890
+ rulesetTargetsDefault,
3891
+ bU as runLayeredContrast,
2888
3892
  sameScopes,
2889
- bq as sameToken,
3893
+ bV as sameToken,
3894
+ sanitizeDocsReplacement,
3895
+ sanitizeDocsResult,
3896
+ sanitizeDocsSections,
3897
+ sanitizeDraftLine,
2890
3898
  sanitizeDraftText,
3899
+ sanitizeDraftTitle,
3900
+ sanitizeResultLines,
3901
+ sectionsNeedingReview,
2891
3902
  selectTokenFiles,
2892
3903
  serializeJson,
2893
3904
  serializePendingDrafts,
2894
3905
  serializeTokenFile,
2895
3906
  shadowedEdits,
3907
+ bW as shipPolicy,
3908
+ shortVariant,
3909
+ shortVariants,
2896
3910
  slugify,
2897
- snapshotUrl,
2898
- br as specHash,
3911
+ bX as snapshotUrl,
3912
+ bY as specHash,
2899
3913
  startExplorationOnGitHub,
2900
- bs as summarizeTokenChange,
2901
- bt as tarballAssetName,
2902
- tarballUrl,
2903
- bu as threeWayDiff,
3914
+ bZ as summarizeTokenChange,
3915
+ b_ as tarballAssetName,
3916
+ b$ as tarballUrl,
3917
+ c0 as teamKey,
3918
+ c1 as teamKeyOf,
3919
+ c2 as teamUnreadableMessage,
3920
+ c3 as teamsIn,
3921
+ c4 as threeWayDiff,
2904
3922
  tintedNeutral,
2905
- bv as titleOf,
3923
+ c5 as titleOf,
2906
3924
  t2 as toAlias,
2907
- bw as toChangeSummary,
3925
+ c6 as toChangeSummary,
2908
3926
  toConfigJson,
2909
3927
  toHex,
2910
- bx as toMap,
3928
+ c7 as toMap,
2911
3929
  toOklch,
2912
3930
  toPlain,
2913
- by as tokenBump,
3931
+ c8 as tokenBump,
2914
3932
  t as tokenCssVar,
2915
- bz as tokenHistory,
2916
- bA as tokenNode,
3933
+ c9 as tokenHistory,
3934
+ ca as tokenNode,
2917
3935
  tokenUsage,
2918
3936
  tokensPath,
3937
+ cb as unreadableShipTeams,
3938
+ cc as unreadableTeams,
2919
3939
  updatePendingDrafts,
2920
3940
  upsertToken,
2921
3941
  utf8ToBase64,
3942
+ validateDocsTask,
2922
3943
  validateIntegrations,
2923
3944
  validateLayered,
2924
3945
  validatePendingDrafts,
2925
3946
  validateProposal,
2926
3947
  validateSet,
2927
- bB as valueIn,
3948
+ cd as valueIn,
2928
3949
  u as valueToCss,
2929
3950
  valuesEqual,
2930
3951
  v as variantKeyOf,
2931
- bC as variantName,
2932
- bD as versionTimeline,
3952
+ ce as variantName,
3953
+ cf as versionTimeline,
3954
+ cg as visualImpactProblems,
3955
+ ch as whoCanShipSentence,
3956
+ withRequestExploration,
2933
3957
  writeDraftResults
2934
3958
  };
2935
3959
  //# sourceMappingURL=index.js.map