@bridge-ds/core 0.2.0 → 0.4.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,7 +1,7 @@
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";
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
- 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";
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 flattenTokens, B as serializeTokenFile, D as removeToken, E as upsertToken, F as threeWayDiff, G as emptyTokenFile, H as parseTokenFile, I as EXT_ID, J as EXT_SCOPES, K as resolve, L as moveToken, M as effectiveScopes, N as sameScopes, O as defaultScopes, P as readTokenAt, Q as modeFile, R as defaultFile, U as DEFAULT_PROTECTED_BRANCHES, a as assertSafeBranch, V as ProtectedBranchError, f as CONFIG_FILE, l as loadConfig, W as ConfigError, X as DEFAULT_TOKEN_PATHS, c as collectHistory, p as packageDirCandidates, b as parseChangelog, r as releasesFromTags, m as mergeReleases, d as buildReport, Y as applyTokenEdits, Z as buildHistoryEntry, _ as resolveBranchPreviews, $ as tokensPath, a0 as coerceValue, a1 as toConfigJson, a2 as parseContrastChecks, a3 as checkLayeredPair, a4 as classifyRisk, a5 as formatChangeset, a6 as RISK_LABELS, a7 as parseJson, a8 as serializeJson, a9 as plainEqual, aa as toPlain, ab as getEntry, ac as fromPlain, ad as previewLinks, ae as slugify, af as readPreviewComment, ag as findPreviewComment, ah as previewsSettled } from "./component-spec-cvrSvIZj.js";
2
+ import { ai, aj, ak, k, C, 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, aM, aN, aO, i, 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, bn, bo, h, bp, j, bq, br, bs, bt, bu, bv, bw, bx, by, bz, g, bA, bB, bC, bD, bE, bF, bG, bH, bI, bJ, bK, bL, bM, bN, e, 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-cvrSvIZj.js";
3
+ import { a as collectRefs, i as isAlias, b as aliasTarget, v as valuesEqual, p as parseColor, e as rgbaToHex, f as formatValue, s as selectTokenFiles, L as LayeredTokenSet, j as validateLayered } from "./layered-BnbmToGs.js";
4
+ import { D, d, k as k2, l, c, m, n, g as g2, h as h2, o, q, r, t as t2, u } from "./layered-BnbmToGs.js";
5
5
  const issueKey = (i2) => `${i2.code}|${i2.path ?? ""}|${i2.mode ?? ""}|${i2.code === "file-problem" ? i2.message : ""}`;
6
6
  const modeName = (mode) => mode === SINGLE_MODE ? "all modes" : modeDisplayName(mode);
7
7
  const isPalette = (t3) => t3.tier === "brand" && t3.path.startsWith("palette.");
@@ -54,7 +54,7 @@ function checkValue(set, owner, value, mode, where, issues) {
54
54
  }
55
55
  function validateSet(set, pairs) {
56
56
  const issues = [];
57
- for (const p2 of set.problems) issues.push({ code: "file-problem", severity: "error", message: p2 });
57
+ for (const p of set.problems) issues.push({ code: "file-problem", severity: "error", message: p });
58
58
  for (const t3 of set.tokens.values()) {
59
59
  if (t3.scopes?.includes("ALL_SCOPES")) {
60
60
  issues.push({
@@ -102,20 +102,20 @@ function validateSet(set, pairs) {
102
102
  }
103
103
  }
104
104
  const axes = { brand: modesOf$1(set.layout, "brand"), theme: modesOf$1(set.layout, "semantic"), density: modesOf$1(set.layout, "density") };
105
- for (const p2 of findReferenceProblems(set.byPath, axes)) {
105
+ for (const p of findReferenceProblems(set.byPath, axes)) {
106
106
  issues.push(
107
- p2.kind === "broken" ? {
107
+ p.kind === "broken" ? {
108
108
  code: "broken-reference",
109
109
  severity: "error",
110
- path: p2.path,
111
- mode: p2.mode,
112
- message: `${p2.path} points to ${p2.target}, which does not exist (${modeName(p2.mode)}).`
110
+ path: p.path,
111
+ mode: p.mode,
112
+ message: `${p.path} points to ${p.target}, which does not exist (${modeName(p.mode)}).`
113
113
  } : {
114
114
  code: "alias-cycle",
115
115
  severity: "error",
116
- path: p2.path,
117
- mode: p2.mode,
118
- message: `${p2.path} is part of a loop of references: ${p2.chain.join(" -> ")} (${modeName(p2.mode)}).`
116
+ path: p.path,
117
+ mode: p.mode,
118
+ message: `${p.path} is part of a loop of references: ${p.chain.join(" -> ")} (${modeName(p.mode)}).`
119
119
  }
120
120
  );
121
121
  }
@@ -139,16 +139,16 @@ function validateSet(set, pairs) {
139
139
  }
140
140
  function shadowedEdits(set, edits) {
141
141
  const out = [];
142
- for (const e3 of edits) {
143
- if (e3.tier !== "semantic") continue;
142
+ for (const e2 of edits) {
143
+ if (e2.tier !== "semantic") continue;
144
144
  for (const o2 of set.overrides) {
145
- if (o2.path !== e3.path || !e3.modes.includes(o2.theme)) continue;
145
+ if (o2.path !== e2.path || !e2.modes.includes(o2.theme)) continue;
146
146
  out.push({
147
147
  code: "override-shadowed",
148
148
  severity: "warning",
149
- path: e3.path,
149
+ path: e2.path,
150
150
  mode: `${o2.brand}.${o2.theme}`,
151
- message: `${e3.path} is overridden for ${modeDisplayName(o2.brand)} in ${o2.theme} (${o2.file}), so this edit does not change ${modeDisplayName(o2.brand)} ${o2.theme}. Brand overrides stay code-only.`
151
+ message: `${e2.path} is overridden for ${modeDisplayName(o2.brand)} in ${o2.theme} (${o2.file}), so this edit does not change ${modeDisplayName(o2.brand)} ${o2.theme}. Brand overrides stay code-only.`
152
152
  });
153
153
  }
154
154
  }
@@ -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 };
@@ -180,9 +180,9 @@ function levenshtein(a, b) {
180
180
  for (let i2 = 1; i2 <= a.length; i2++) {
181
181
  let diag = prev[0];
182
182
  prev[0] = i2;
183
- for (let j3 = 1; j3 <= b.length; j3++) {
184
- const tmp = prev[j3];
185
- prev[j3] = Math.min(prev[j3] + 1, prev[j3 - 1] + 1, diag + (a[i2 - 1] === b[j3 - 1] ? 0 : 1));
183
+ for (let j2 = 1; j2 <= b.length; j2++) {
184
+ const tmp = prev[j2];
185
+ prev[j2] = Math.min(prev[j2] + 1, prev[j2 - 1] + 1, diag + (a[i2 - 1] === b[j2 - 1] ? 0 : 1));
186
186
  diag = tmp;
187
187
  }
188
188
  }
@@ -249,17 +249,263 @@ function applyConfirmedRenames(tokens, confirmed) {
249
249
  function idExtensionFor(t3) {
250
250
  return t3.id === pathId(t3.path) ? void 0 : t3.id;
251
251
  }
252
+ const idOf$1 = (f) => {
253
+ const id = f.extensions?.[EXT_ID];
254
+ return typeof id === "string" && id ? id : pathId(f.path);
255
+ };
256
+ function logical(f) {
257
+ const scopes = f.extensions?.[EXT_SCOPES];
258
+ return {
259
+ id: idOf$1(f),
260
+ path: f.path,
261
+ type: f.type ?? "unknown",
262
+ // One file holds one tier: the tier only feeds default scopes, the same on every side.
263
+ tier: "primitive",
264
+ values: { [SINGLE_MODE]: f.value },
265
+ description: f.description,
266
+ scopes: Array.isArray(scopes) ? scopes.map(String) : void 0,
267
+ files: { [SINGLE_MODE]: f.file }
268
+ };
269
+ }
270
+ function readSide(file, text, problems, label) {
271
+ const side = { doc: void 0, flat: /* @__PURE__ */ new Map(), logical: /* @__PURE__ */ new Map() };
272
+ if (text === void 0) return side;
273
+ try {
274
+ side.doc = parseTokenFile(file, text);
275
+ } catch (err) {
276
+ problems.push(`${file} (${label}) is not valid JSON: ${err.message}`);
277
+ return side;
278
+ }
279
+ for (const f of flattenTokens(side.doc)) {
280
+ const id = idOf$1(f);
281
+ side.flat.set(id, f);
282
+ side.logical.set(id, logical(f));
283
+ }
284
+ return side;
285
+ }
286
+ function diffFile(input, file, problems) {
287
+ const base = readSide(file, input.base[file], problems, "shared start");
288
+ const mine = readSide(file, input.mine[file], problems, "this change");
289
+ const theirs = readSide(file, input.theirs[file], problems, "latest library");
290
+ const r2 = threeWayDiff(base.logical, mine.logical, theirs.logical);
291
+ return { file, base, mine, theirs, local: r2.local, remote: r2.remote, conflicts: r2.conflicts };
292
+ }
293
+ function removedSide(c10) {
294
+ if (!c10.base) return null;
295
+ if (!c10.mine && c10.theirs) return "mine";
296
+ if (!c10.theirs && c10.mine) return "theirs";
297
+ return null;
298
+ }
299
+ const defaultChoiceFor = (c10) => removedSide(c10) ? null : "mine";
300
+ function planTokenMerge(input) {
301
+ const problems = [];
302
+ const files = [...new Set(input.files)].sort();
303
+ const conflicts = [];
304
+ let mineOnly = 0;
305
+ let theirsOnly = 0;
306
+ for (const file of files) {
307
+ const d2 = diffFile(input, file, problems);
308
+ mineOnly += d2.local.length;
309
+ theirsOnly += d2.remote.length;
310
+ for (const e2 of d2.conflicts) {
311
+ const c10 = { base: d2.base.flat.get(e2.id), mine: d2.mine.flat.get(e2.id), theirs: d2.theirs.flat.get(e2.id) };
312
+ conflicts.push({ key: `${file}#${e2.id}`, file, path: (c10.mine ?? c10.theirs ?? c10.base).path, ...c10, removedOn: removedSide(c10), defaultChoice: defaultChoiceFor(c10) });
313
+ }
314
+ }
315
+ return { files, conflicts, mineOnly, theirsOnly, problems };
316
+ }
317
+ const choiceFor = (c10, choices) => choices[c10.key] ?? c10.defaultChoice;
318
+ function missingChoices(plan, choices) {
319
+ return plan.conflicts.filter((c10) => choiceFor(c10, choices) === null);
320
+ }
321
+ function takeMine(doc, mine, existing) {
322
+ if (!mine) return existing ? removeToken(doc, existing.path) : false;
323
+ let changed = false;
324
+ if (existing && existing.path !== mine.path) changed = removeToken(doc, existing.path) || changed;
325
+ const extensions = {};
326
+ for (const k3 of Object.keys(existing?.path === mine.path ? existing.extensions ?? {} : {})) extensions[k3] = void 0;
327
+ Object.assign(extensions, mine.extensions ?? {});
328
+ return upsertToken(doc, mine.path, { type: mine.type, value: mine.value, description: mine.description, extensions }) || changed;
329
+ }
330
+ function applyTokenMerge(input, choices) {
331
+ const plan = planTokenMerge(input);
332
+ if (plan.problems.length) throw new Error(plan.problems[0]);
333
+ const missing = missingChoices(plan, choices);
334
+ if (missing.length) throw new Error(`Pick a value for ${missing.map((c10) => c10.path).join(", ")} first.`);
335
+ const byKey = new Map(plan.conflicts.map((c10) => [c10.key, c10]));
336
+ const kept = { mine: 0, theirs: 0 };
337
+ const files = {};
338
+ for (const file of plan.files) {
339
+ const d2 = diffFile(input, file, []);
340
+ const doc = d2.theirs.doc ?? emptyTokenFile(file);
341
+ for (const e2 of d2.local) takeMine(doc, d2.mine.flat.get(e2.id), d2.theirs.flat.get(e2.id) ?? d2.base.flat.get(e2.id));
342
+ for (const e2 of d2.conflicts) {
343
+ const side = choiceFor(byKey.get(`${file}#${e2.id}`), choices);
344
+ kept[side]++;
345
+ if (side === "mine") takeMine(doc, d2.mine.flat.get(e2.id), d2.theirs.flat.get(e2.id));
346
+ }
347
+ const empty = flattenTokens(doc).length === 0;
348
+ files[file] = empty && (input.theirs[file] === void 0 || input.mine[file] === void 0) ? null : serializeTokenFile(doc);
349
+ }
350
+ return { files, kept };
351
+ }
352
+ const idOf = (f) => {
353
+ const id = f.extensions?.[EXT_ID];
354
+ return typeof id === "string" && id ? id : pathId(f.path);
355
+ };
356
+ function tokensOf(file, text, problems, label) {
357
+ const out = /* @__PURE__ */ new Map();
358
+ if (text === void 0) return out;
359
+ try {
360
+ for (const t3 of flattenTokens(parseTokenFile(file, text))) out.set(idOf(t3), t3);
361
+ } catch (err) {
362
+ problems.push(`${file} (${label}) is not valid JSON: ${err.message}`);
363
+ }
364
+ return out;
365
+ }
366
+ const shape = (t3) => t3 ? JSON.stringify([t3.path, t3.type ?? null, t3.value, t3.description ?? null]) : "removed";
367
+ const look = (t3) => t3 ? JSON.stringify([t3.path, t3.type ?? null, t3.value]) : "removed";
368
+ function changeEdits(src) {
369
+ const problems = [];
370
+ const edits = [];
371
+ for (const file of [...new Set(src.files)].sort()) {
372
+ const before = tokensOf(file, src.base[file], problems, "start");
373
+ const after = tokensOf(file, src.head[file], problems, `change ${src.number}`);
374
+ for (const id of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
375
+ const a = before.get(id);
376
+ const b = after.get(id);
377
+ if (shape(a) === shape(b)) continue;
378
+ edits.push({ key: `${file}#${id}`, file, path: (b ?? a).path, change: src.number, token: b });
379
+ }
380
+ }
381
+ return { number: src.number, edits, problems };
382
+ }
383
+ function byToken(changes) {
384
+ const out = /* @__PURE__ */ new Map();
385
+ for (const c10 of changes) for (const e2 of c10.edits) out.set(e2.key, [...out.get(e2.key) ?? [], e2]);
386
+ return out;
387
+ }
388
+ function optionsOf(edits) {
389
+ const out = [];
390
+ for (const e2 of edits) {
391
+ const l2 = look(e2.token);
392
+ const hit = out.find((o2) => o2.look === l2);
393
+ if (hit) hit.option.changes.push(e2.change);
394
+ else out.push({ look: l2, option: { changes: [e2.change], token: e2.token } });
395
+ }
396
+ return out.map((o2) => o2.option);
397
+ }
398
+ function combineConflicts(changes) {
399
+ const out = [];
400
+ for (const [key, edits] of byToken(changes)) {
401
+ const options = optionsOf(edits);
402
+ if (options.length > 1) out.push({ key, file: edits[0].file, path: edits[0].path, options });
403
+ }
404
+ return out;
405
+ }
406
+ function pickedOption(c10, picks) {
407
+ const n2 = picks[c10.key];
408
+ return n2 === void 0 ? null : c10.options.find((o2) => o2.changes.includes(n2)) ?? null;
409
+ }
410
+ const unpicked = (conflicts, picks) => conflicts.filter((c10) => !pickedOption(c10, picks));
411
+ function combineChanges(latest, changes, picks = {}, opts = {}) {
412
+ const on = opts.include ? changes.filter((c10) => opts.include.includes(c10.number)) : changes;
413
+ const problems = on.flatMap((c10) => c10.problems);
414
+ const groups = byToken(on);
415
+ const conflicts = [];
416
+ const missing = [];
417
+ const from = {};
418
+ const docs = /* @__PURE__ */ new Map();
419
+ const docOf = (file) => {
420
+ if (docs.has(file)) return docs.get(file);
421
+ const text = latest[file];
422
+ let doc;
423
+ try {
424
+ doc = text === void 0 ? emptyTokenFile(file) : parseTokenFile(file, text);
425
+ } catch (err) {
426
+ problems.push(`${file} (latest library) is not valid JSON: ${err.message}`);
427
+ return null;
428
+ }
429
+ docs.set(file, doc);
430
+ return doc;
431
+ };
432
+ for (const [key, edits] of groups) {
433
+ const options = optionsOf(edits);
434
+ let use = options[0];
435
+ if (options.length > 1) {
436
+ const conflict = { key, file: edits[0].file, path: edits[0].path, options };
437
+ conflicts.push(conflict);
438
+ use = pickedOption(conflict, picks);
439
+ if (!use) {
440
+ missing.push(conflict);
441
+ use = opts.unpicked === "first" ? options[0] : null;
442
+ }
443
+ }
444
+ if (!use) continue;
445
+ const doc = docOf(edits[0].file);
446
+ if (!doc) continue;
447
+ const existing = flattenTokens(doc).find((t3) => idOf(t3) === key.slice(key.indexOf("#") + 1));
448
+ takeMine(doc, use.token, existing);
449
+ const path = use.token?.path ?? edits[0].path;
450
+ from[path] = [.../* @__PURE__ */ new Set([...from[path] ?? [], ...use.changes])];
451
+ }
452
+ const files = { ...latest };
453
+ for (const [file, doc] of docs) {
454
+ if (latest[file] === void 0 && flattenTokens(doc).length === 0) continue;
455
+ files[file] = serializeTokenFile(doc);
456
+ }
457
+ const tokens = new Set([...groups.values()].map((e2) => e2[0].path)).size;
458
+ return { files, included: on.map((c10) => c10.number), edits: groups.size, tokens, conflicts, missing, from, problems };
459
+ }
460
+ function encodePicks(picks) {
461
+ return Object.entries(picks).filter(([, n2]) => Number.isSafeInteger(n2) && n2 > 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, n2]) => `${n2}:${key}`).join("~");
462
+ }
463
+ function decodePicks(raw) {
464
+ const out = {};
465
+ for (const part of (raw ?? "").split("~")) {
466
+ const i2 = part.indexOf(":");
467
+ if (i2 < 1) continue;
468
+ const n2 = Number(part.slice(0, i2));
469
+ const key = part.slice(i2 + 1);
470
+ if (/^\d{1,7}$/.test(part.slice(0, i2)) && n2 > 0 && key.includes("#")) out[key] = n2;
471
+ }
472
+ return out;
473
+ }
474
+ function cssOf(type, value) {
475
+ if (value === void 0) return void 0;
476
+ if (type === "color") {
477
+ const hex = value && typeof value === "object" ? value.hex : void 0;
478
+ const c10 = parseColor(value);
479
+ if (!c10) return void 0;
480
+ if (c10.a < 1) return `rgb(${Math.round(c10.r * 255)} ${Math.round(c10.g * 255)} ${Math.round(c10.b * 255)} / ${c10.a})`;
481
+ return typeof hex === "string" ? hex.toLowerCase() : rgbaToHex(c10);
482
+ }
483
+ return formatValue(type, value);
484
+ }
485
+ const cssVarOf = (path) => `--${path.replace(/\./g, "-")}`;
486
+ function previewOverrides(head, proposed, ctx) {
487
+ const out = {};
488
+ const hctx = { ...ctx, overrides: head.overrideIndex };
489
+ const pctx = { ...ctx, overrides: proposed.overrideIndex };
490
+ for (const t3 of proposed.byPath.values()) {
491
+ const a = resolve(head.byPath, t3.path, hctx);
492
+ const b = resolve(proposed.byPath, t3.path, pctx);
493
+ const css = cssOf(t3.type, b.value);
494
+ if (css !== void 0 && JSON.stringify(a.value) !== JSON.stringify(b.value)) out[cssVarOf(t3.path)] = css;
495
+ }
496
+ return out;
497
+ }
252
498
  const modesOf = (t3) => Object.keys(t3.values);
253
499
  function composeFiles(head, originals, changes) {
254
500
  const docs = /* @__PURE__ */ new Map();
255
501
  const doc = (file) => {
256
- let d = docs.get(file);
257
- if (!d) {
502
+ let d2 = docs.get(file);
503
+ if (!d2) {
258
504
  const text = originals[file];
259
- d = text === void 0 ? emptyTokenFile(file) : parseTokenFile(file, text);
260
- docs.set(file, d);
505
+ d2 = text === void 0 ? emptyTokenFile(file) : parseTokenFile(file, text);
506
+ docs.set(file, d2);
261
507
  }
262
- return d;
508
+ return d2;
263
509
  };
264
510
  const fileFor = (t3, mode, current) => current?.files[mode] ?? t3.files[mode] ?? (isMultiMode(t3.tier) ? modeFile(head.layout, t3.tier, mode) : void 0) ?? defaultFile(t3.tier, t3.path, head.options.dirs);
265
511
  for (const { target, current } of changes) {
@@ -270,9 +516,9 @@ function composeFiles(head, originals, changes) {
270
516
  const renamed = !!current && current.path !== target.path;
271
517
  if (current) {
272
518
  for (const mode of modesOf(current)) {
273
- const d = doc(fileFor(current, mode));
274
- if (target.values[mode] === void 0) removeToken(d, current.path);
275
- else if (renamed) moveToken(d, current.path, target.path);
519
+ const d2 = doc(fileFor(current, mode));
520
+ if (target.values[mode] === void 0) removeToken(d2, current.path);
521
+ else if (renamed) moveToken(d2, current.path, target.path);
276
522
  }
277
523
  }
278
524
  const targetScopes = effectiveScopes(target);
@@ -280,14 +526,14 @@ function composeFiles(head, originals, changes) {
280
526
  const scopesChanged = !current || !sameScopes(targetScopes, effectiveScopes(current));
281
527
  const descChanged = (target.description ?? "").trim() !== (current?.description ?? "").trim();
282
528
  for (const mode of modesOf(target)) {
283
- const d = doc(fileFor(target, mode, current));
284
- const existing = readTokenAt(d, target.path);
529
+ const d2 = doc(fileFor(target, mode, current));
530
+ const existing = readTokenAt(d2, target.path);
285
531
  const currentValue = current?.values[mode];
286
532
  const value = currentValue !== void 0 && valuesEqual(target.type, target.values[mode], currentValue) ? currentValue : target.values[mode];
287
533
  const extensions = {};
288
534
  if (scopesChanged) extensions[EXT_SCOPES] = scopesExt;
289
535
  if (renamed || !existing) extensions[EXT_ID] = idExtensionFor(target);
290
- upsertToken(d, target.path, {
536
+ upsertToken(d2, target.path, {
291
537
  type: target.type,
292
538
  value,
293
539
  description: descChanged || !existing ? target.description : existing.description,
@@ -297,144 +543,198 @@ function composeFiles(head, originals, changes) {
297
543
  }
298
544
  const files = {};
299
545
  const allFiles = { ...originals };
300
- for (const [file, d] of docs) {
301
- const text = serializeTokenFile(d);
546
+ for (const [file, d2] of docs) {
547
+ const text = serializeTokenFile(d2);
302
548
  allFiles[file] = text;
303
549
  if (text !== originals[file]) files[file] = text;
304
550
  }
305
551
  return { files, allFiles };
306
552
  }
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];
553
+ const PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
554
+ const protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
555
+ const ADMIN_ROLE_ID = 5;
556
+ function protectionRuleset(config, opts) {
557
+ const branch = config?.repo?.defaultBranch ?? "main";
558
+ const approvals = Math.max(1, Math.floor(Number.isFinite(opts.approvals) ? opts.approvals : 1));
559
+ const checks = [...new Set(opts.checks.map((c10) => c10.trim()).filter(Boolean))];
560
+ const rules = [
561
+ { type: "deletion" },
562
+ { type: "non_fast_forward" },
563
+ {
564
+ type: "pull_request",
565
+ parameters: {
566
+ required_approving_review_count: approvals,
567
+ dismiss_stale_reviews_on_push: true,
568
+ require_code_owner_review: false,
569
+ require_last_push_approval: false,
570
+ required_review_thread_resolution: false
571
+ }
572
+ }
573
+ ];
574
+ if (checks.length) {
575
+ rules.push({
576
+ type: "required_status_checks",
577
+ parameters: {
578
+ strict_required_status_checks_policy: false,
579
+ required_status_checks: checks.map((context) => ({ context }))
580
+ }
581
+ });
582
+ }
583
+ return {
584
+ name: protectionRulesetName(branch),
585
+ target: "branch",
586
+ enforcement: "active",
587
+ conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
588
+ rules,
589
+ bypass_actors: opts.adminBypass ? [{ actor_id: ADMIN_ROLE_ID, actor_type: "RepositoryRole", bypass_mode: "pull_request" }] : []
590
+ };
313
591
  }
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];
592
+ const isVercel = (r2) => /vercel/i.test(r2.app ?? "") || /^vercel\b/i.test(r2.name);
593
+ const ran = (r2) => r2.conclusion !== "skipped" && r2.conclusion !== "cancelled" && r2.conclusion !== "stale";
594
+ function requiredCheckNames(defaultBranchRuns, pullRequestRuns) {
595
+ const names2 = [...new Set(defaultBranchRuns.filter((r2) => !isVercel(r2) && ran(r2)).map((r2) => r2.name))];
596
+ if (!pullRequestRuns?.length) return names2;
597
+ const onPulls = new Set(pullRequestRuns.filter((r2) => !isVercel(r2)).map((r2) => r2.name));
598
+ return names2.filter((n2) => onPulls.has(n2));
318
599
  }
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;
600
+ function refPattern(pattern) {
601
+ const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
602
+ return new RegExp(`^${src}$`);
335
603
  }
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;
604
+ function rulesetTargetsDefault(ruleset, defaultBranch) {
605
+ if (ruleset.target && ruleset.target !== "branch") return false;
606
+ const ref = `refs/heads/${defaultBranch}`;
607
+ const hit = (p) => p === "~ALL" || p === "~DEFAULT_BRANCH" || p === ref || p === defaultBranch || refPattern(p.startsWith("refs/") ? p : `refs/heads/${p}`).test(ref);
608
+ const cond = ruleset.conditions?.ref_name;
609
+ if (!cond) return false;
610
+ if ((cond.exclude ?? []).some(hit)) return false;
611
+ return (cond.include ?? []).some(hit);
342
612
  }
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();
358
- }
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
- }
371
- }
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);
613
+ const count = (v2) => typeof v2 === "number" ? v2 : 0;
614
+ function rulesetParts(ruleset) {
615
+ const parts = /* @__PURE__ */ new Set();
616
+ if (ruleset.enforcement !== "active") return parts;
617
+ for (const rule of ruleset.rules ?? []) {
618
+ if (rule.type === "deletion") parts.add("deletion");
619
+ if (rule.type === "non_fast_forward") parts.add("history");
620
+ if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
621
+ if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
379
622
  }
380
- for (const k3 of Object.keys(byVar)) byVar[k3].sort();
381
- return { componentsDir: dir, modules, byVar, globalVars };
623
+ return parts;
382
624
  }
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 });
391
- };
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
- }
625
+ function classicParts(p) {
626
+ const parts = /* @__PURE__ */ new Set();
627
+ if (count(p.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
628
+ if (((p.required_status_checks?.contexts?.length ?? 0) || (p.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
629
+ if (!p.allow_force_pushes?.enabled) parts.add("history");
630
+ if (!p.allow_deletions?.enabled) parts.add("deletion");
631
+ return parts;
632
+ }
633
+ const rulesetUrl = (r2) => r2.html_url ?? r2._links?.html?.href;
634
+ function protectionState(rulesets, branchProtection, defaultBranch) {
635
+ const bridge = rulesets?.find((r2) => r2.name === protectionRulesetName(defaultBranch));
636
+ const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
637
+ const covered = /* @__PURE__ */ new Set();
638
+ let url;
639
+ for (const r2 of rulesets ?? []) {
640
+ if (!rulesetTargetsDefault(r2, defaultBranch)) continue;
641
+ const parts = rulesetParts(r2);
642
+ if (parts.size && !url) url = rulesetUrl(r2);
643
+ parts.forEach((p) => covered.add(p));
408
644
  }
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 };
645
+ if (branchProtection) classicParts(branchProtection).forEach((p) => covered.add(p));
646
+ const missing = PROTECTION_PARTS.filter((p) => !covered.has(p));
647
+ const withUrl = url ? { url } : {};
648
+ if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
649
+ const unreadable = rulesets == null || branchProtection === void 0;
650
+ if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
651
+ return { kind: "partial", missing, ...extra, ...withUrl };
412
652
  }
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);
653
+ const PROTECTION_PART_PHRASE = {
654
+ review: "changes can skip review",
655
+ checks: "checks do not have to pass",
656
+ history: "past changes can be rewritten",
657
+ deletion: "the library can be deleted"
658
+ };
659
+ const joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
660
+ function protectionSentence(state) {
661
+ switch (state.kind) {
662
+ case "protected":
663
+ return "Protected: every change goes through review.";
664
+ case "unprotected":
665
+ return "Your library can be changed without review.";
666
+ case "partial": {
667
+ const s = joinAnd(state.missing.map((p) => PROTECTION_PART_PHRASE[p]));
668
+ return `Partly protected: ${s}.`;
669
+ }
670
+ default:
671
+ return "Protection status unknown: GitHub did not let this token read it.";
426
672
  }
427
- return { used, direct };
428
673
  }
429
674
  class GitHubError extends Error {
430
- constructor(status, message, detail) {
675
+ constructor(status, message, detail, info = {}) {
431
676
  super(message);
432
677
  this.status = status;
433
678
  this.detail = detail;
434
679
  this.name = "GitHubError";
680
+ this.auth = info.auth;
681
+ this.permission = info.permission;
435
682
  }
436
683
  status;
437
684
  detail;
685
+ /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
686
+ auth;
687
+ /** With auth "missing-permission": the permission the call needed, when known. */
688
+ permission;
689
+ }
690
+ const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
691
+ const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
692
+ const PERMISSION_LABELS = {
693
+ issues: "Issues",
694
+ pull_requests: "Pull requests",
695
+ contents: "Contents",
696
+ checks: "Checks",
697
+ statuses: "Commit statuses",
698
+ metadata: "Metadata",
699
+ actions: "Actions",
700
+ deployments: "Deployments",
701
+ workflows: "Workflows",
702
+ administration: "Administration"
703
+ };
704
+ function permissionLabel(permission) {
705
+ return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c10) => c10.toUpperCase());
706
+ }
707
+ const TOKEN_SETTINGS_URL = "https://github.com/settings/personal-access-tokens";
708
+ const TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
709
+ function missingPermissionMessage(permission, action) {
710
+ const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
711
+ if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
712
+ return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
713
+ }
714
+ function requiredPermission(path, acceptedHeader) {
715
+ if (acceptedHeader) {
716
+ const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
717
+ if (first.length) return first[0];
718
+ }
719
+ const p = path.replace(/[?#].*$/, "");
720
+ if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
721
+ if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
722
+ if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
723
+ if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
724
+ if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
725
+ if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
726
+ if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
727
+ if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
728
+ return void 0;
729
+ }
730
+ 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;
731
+ function classifyAuth(status, detail, opts = {}) {
732
+ if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
733
+ if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
734
+ if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
735
+ return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
736
+ }
737
+ return {};
438
738
  }
439
739
  const DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r2) => setTimeout(r2, ms)) };
440
740
  const isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
@@ -497,7 +797,7 @@ class GitHubClient {
497
797
  }
498
798
  }
499
799
  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.`);
800
+ 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
801
  }
502
802
  get repoPath() {
503
803
  return `/repos/${this.owner}/${this.name}`;
@@ -508,8 +808,8 @@ class GitHubClient {
508
808
  try {
509
809
  return await this.once(method, path, body);
510
810
  } catch (err) {
511
- const e3 = err;
512
- if (attempt >= attempts || !(e3 instanceof GitHubError) || !isTransient(e3.status, e3.detail ?? "")) throw err;
811
+ const e2 = err;
812
+ if (attempt >= attempts || !(e2 instanceof GitHubError) || !isTransient(e2.status, e2.detail ?? "")) throw err;
513
813
  await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
514
814
  }
515
815
  }
@@ -532,7 +832,7 @@ class GitHubClient {
532
832
  let detail = "";
533
833
  try {
534
834
  const data = await res.json();
535
- detail = [data.message, ...(data.errors ?? []).map((e3) => typeof e3 === "string" ? e3 : e3.message)].filter(Boolean).join(" ");
835
+ detail = [data.message, ...(data.errors ?? []).map((e2) => typeof e2 === "string" ? e2 : e2.message)].filter(Boolean).join(" ");
536
836
  } catch {
537
837
  detail = "";
538
838
  }
@@ -546,7 +846,12 @@ class GitHubClient {
546
846
  detail
547
847
  );
548
848
  }
549
- throw new GitHubError(res.status, friendlyMessage(res.status, detail), detail);
849
+ const info = classifyAuth(res.status, detail, {
850
+ path,
851
+ acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
852
+ withToken: Boolean(this.token)
853
+ });
854
+ throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
550
855
  }
551
856
  return await res.json();
552
857
  }
@@ -611,7 +916,7 @@ class GitHubClient {
611
916
  /** Pull requests a commit belongs to (merged ones first): how a commit on main finds its PR. */
612
917
  async listPullRequestsForCommit(sha) {
613
918
  const r2 = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/pulls`);
614
- return r2.map((p2) => ({ ...toSummary(p2), body: p2.body ?? "", ...p2.merged_at ? { mergedAt: p2.merged_at } : {}, ...p2.merge_commit_sha ? { mergeCommitSha: p2.merge_commit_sha } : {} })).sort((a, b) => Number(Boolean(b.mergedAt)) - Number(Boolean(a.mergedAt)));
919
+ return r2.map((p) => ({ ...toSummary(p), body: p.body ?? "", ...p.merged_at ? { mergedAt: p.merged_at } : {}, ...p.merge_commit_sha ? { mergeCommitSha: p.merge_commit_sha } : {} })).sort((a, b) => Number(Boolean(b.mergedAt)) - Number(Boolean(a.mergedAt)));
615
920
  }
616
921
  /** Tags with the commit they point to, newest first as GitHub lists them. */
617
922
  async listTags(perPage = 30) {
@@ -692,10 +997,29 @@ class GitHubClient {
692
997
  variables: { id: pr.node_id }
693
998
  });
694
999
  if (r2?.errors?.length) {
695
- const detail = r2.errors.map((e3) => e3.message).filter(Boolean).join(" ");
1000
+ const detail = r2.errors.map((e2) => e2.message).filter(Boolean).join(" ");
696
1001
  throw new GitHubError(422, `GitHub refused to mark #${n2} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
697
1002
  }
698
1003
  }
1004
+ /**
1005
+ * Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
1006
+ * `draft` either), same token. No-op when it is already a draft.
1007
+ */
1008
+ async convertToDraft(n2) {
1009
+ this.assertToken("Turning a pull request back into a draft");
1010
+ const pr = await this.request("GET", `${this.repoPath}/pulls/${n2}`);
1011
+ if (pr.draft === true) return;
1012
+ if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n2} is ${pr.state}: only an open one can go back to draft.`);
1013
+ if (!pr.node_id) throw new GitHubError(422, `Pull request #${n2} has no node id: convert it to draft on GitHub.`);
1014
+ const r2 = await this.request("POST", "/graphql", {
1015
+ query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
1016
+ variables: { id: pr.node_id }
1017
+ });
1018
+ if (r2?.errors?.length) {
1019
+ const detail = r2.errors.map((e2) => e2.message).filter(Boolean).join(" ");
1020
+ throw new GitHubError(422, `GitHub refused to turn #${n2} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
1021
+ }
1022
+ }
699
1023
  async createPullRequest(input) {
700
1024
  this.assertWritableBranch(input.head, input.base);
701
1025
  const r2 = await this.request(
@@ -725,6 +1049,11 @@ class GitHubClient {
725
1049
  async addComment(issueNumber, body) {
726
1050
  await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
727
1051
  }
1052
+ /** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
1053
+ async listIssueComments(issueNumber) {
1054
+ const r2 = await this.request("GET", `${this.repoPath}/issues/${issueNumber}/comments?per_page=100`);
1055
+ return r2.map((c10) => ({ id: c10.id, body: c10.body ?? "", author: c10.user?.login ?? "unknown", updatedAt: c10.updated_at }));
1056
+ }
728
1057
  // --- Issues (component change requests) ---------------------------------------------------
729
1058
  async createIssue(input) {
730
1059
  const r2 = await this.request("POST", `${this.repoPath}/issues`, {
@@ -745,6 +1074,67 @@ class GitHubClient {
745
1074
  async getIssue(n2) {
746
1075
  return toIssue(await this.request("GET", `${this.repoPath}/issues/${n2}`));
747
1076
  }
1077
+ /** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
1078
+ async updateIssue(n2, patch) {
1079
+ this.assertToken("Editing an issue");
1080
+ return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n2}`, patch));
1081
+ }
1082
+ /** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
1083
+ async addAssignees(n2, logins) {
1084
+ this.assertToken("Assigning an issue");
1085
+ const r2 = await this.request("POST", `${this.repoPath}/issues/${n2}/assignees`, { assignees: logins });
1086
+ return (r2.assignees ?? []).map((a) => a.login);
1087
+ }
1088
+ /** People with at least `permission` on the repository (reading the list needs push access). First 100. */
1089
+ async listCollaborators(permission = "push") {
1090
+ const r2 = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
1091
+ return r2.map((c10) => ({ login: c10.login, avatarUrl: c10.avatar_url ?? "" }));
1092
+ }
1093
+ /** Whether a token is sent with each request (TeamReader). */
1094
+ get hasToken() {
1095
+ return Boolean(this.token);
1096
+ }
1097
+ /** Every page of a list endpoint (per_page 100), up to `maxPages`. */
1098
+ async paginate(path, maxPages = 20) {
1099
+ const out = [];
1100
+ const sep = path.includes("?") ? "&" : "?";
1101
+ for (let page = 1; page <= maxPages; page++) {
1102
+ const r2 = await this.request("GET", `${path}${sep}per_page=100&page=${page}`);
1103
+ out.push(...r2);
1104
+ if (r2.length < 100) break;
1105
+ }
1106
+ return out;
1107
+ }
1108
+ /** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
1109
+ async listTeamMembers(org, slug) {
1110
+ const r2 = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members`);
1111
+ return r2.map((m2) => m2.login);
1112
+ }
1113
+ /** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
1114
+ async getTeamMembership(org, slug, login) {
1115
+ try {
1116
+ const r2 = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/memberships/${encodeURIComponent(login)}`);
1117
+ return r2.state === "active" ? "active" : "pending";
1118
+ } catch (err) {
1119
+ if (err instanceof GitHubError && err.status === 404) return null;
1120
+ throw err;
1121
+ }
1122
+ }
1123
+ /** "User" or "Organization" for a login, null when GitHub does not know it. */
1124
+ async getAccountType(login) {
1125
+ try {
1126
+ const r2 = await this.request("GET", `/users/${encodeURIComponent(login)}`);
1127
+ return r2.type === "Organization" ? "Organization" : r2.type === "User" ? "User" : null;
1128
+ } catch (err) {
1129
+ if (err instanceof GitHubError && err.status === 404) return null;
1130
+ throw err;
1131
+ }
1132
+ }
1133
+ /** The organization's teams this token can see. */
1134
+ async listOrgTeams(org) {
1135
+ const r2 = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
1136
+ return r2.map((t3) => ({ slug: t3.slug, name: t3.name }));
1137
+ }
748
1138
  /** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
749
1139
  async putBase64File(input) {
750
1140
  this.assertWritableBranch(input.branch);
@@ -806,7 +1196,7 @@ class GitHubClient {
806
1196
  const q2 = ref ? `?ref=${encodeURIComponent(ref)}` : "";
807
1197
  const r2 = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q2}`);
808
1198
  if (!Array.isArray(r2)) return [];
809
- return r2.map((e3) => ({ name: e3.name, path: e3.path, type: e3.type, sha: e3.sha, size: e3.size ?? 0 }));
1199
+ return r2.map((e2) => ({ name: e2.name, path: e2.path, type: e2.type, sha: e2.sha, size: e2.size ?? 0 }));
810
1200
  } catch (err) {
811
1201
  if (err instanceof GitHubError && err.status === 404) return [];
812
1202
  throw err;
@@ -859,7 +1249,7 @@ class GitHubClient {
859
1249
  async listFiles(ref) {
860
1250
  const r2 = await this.request("GET", `${this.repoPath}/git/trees/${encodeRef(ref)}?recursive=1`);
861
1251
  if (r2.truncated) throw new GitHubError(413, "The repository tree is too large to read in one request.");
862
- return r2.tree.filter((e3) => e3.type === "blob").map((e3) => e3.path);
1252
+ return r2.tree.filter((e2) => e2.type === "blob").map((e2) => e2.path);
863
1253
  }
864
1254
  // --- Pull requests, reviews, checks ---------------------------------------------------------
865
1255
  async listPullRequests(state = "open") {
@@ -873,13 +1263,34 @@ class GitHubClient {
873
1263
  this.listCheckRuns(sha).catch(() => [])
874
1264
  ]);
875
1265
  const checks = [
876
- ...runs.map((c2) => ({ name: c2.name, status: c2.status, conclusion: c2.conclusion })),
1266
+ ...runs.map((c10) => ({ name: c10.name, status: c10.status, conclusion: c10.conclusion })),
877
1267
  ...combined.statuses.map((s) => ({ name: s.context, status: s.state === "pending" ? "in_progress" : "completed", conclusion: s.state === "pending" ? null : s.state }))
878
1268
  ];
879
- const failing = checks.some((c2) => c2.conclusion && !["success", "neutral", "skipped"].includes(c2.conclusion));
880
- const pending = checks.some((c2) => c2.status !== "completed");
1269
+ const failing = checks.some((c10) => c10.conclusion && !["success", "neutral", "skipped"].includes(c10.conclusion));
1270
+ const pending = checks.some((c10) => c10.status !== "completed");
881
1271
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
882
1272
  }
1273
+ /** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
1274
+ async listCommitStatuses(sha) {
1275
+ const r2 = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
1276
+ return r2.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
1277
+ }
1278
+ /**
1279
+ * GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
1280
+ * preview). Latest per environment only, so it costs one read plus one per environment.
1281
+ */
1282
+ async listDeployments(sha) {
1283
+ const list = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
1284
+ const latest = /* @__PURE__ */ new Map();
1285
+ for (const d2 of list) if (!latest.has(d2.environment)) latest.set(d2.environment, d2);
1286
+ return Promise.all(
1287
+ [...latest.values()].map(async (d2) => {
1288
+ const st = await this.request("GET", `${this.repoPath}/deployments/${d2.id}/statuses?per_page=1`).catch(() => []);
1289
+ const s = st[0];
1290
+ return { id: d2.id, environment: d2.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
1291
+ })
1292
+ );
1293
+ }
883
1294
  async getPullRequest(n2) {
884
1295
  const r2 = await this.request("GET", `${this.repoPath}/pulls/${n2}`);
885
1296
  return { ...toSummary(r2), body: r2.body ?? "", mergeable: r2.mergeable ?? null };
@@ -906,7 +1317,44 @@ class GitHubClient {
906
1317
  "GET",
907
1318
  `${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
908
1319
  );
909
- return r2.check_runs.map((c2) => ({ id: c2.id, name: c2.name, status: c2.status, conclusion: c2.conclusion, url: c2.html_url }));
1320
+ 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 } : {} }));
1321
+ }
1322
+ /** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
1323
+ async listRulesets() {
1324
+ const r2 = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
1325
+ return r2.map(withRulesetUrl);
1326
+ }
1327
+ /** One ruleset with its rules. */
1328
+ async getRuleset(id) {
1329
+ return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
1330
+ }
1331
+ /** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
1332
+ async createRuleset(payload) {
1333
+ this.assertToken("Protecting the library");
1334
+ return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
1335
+ }
1336
+ /** Replaces a repository ruleset (same permissions as createRuleset). */
1337
+ async updateRuleset(id, payload) {
1338
+ this.assertToken("Protecting the library");
1339
+ return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
1340
+ }
1341
+ /**
1342
+ * Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
1343
+ * token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
1344
+ * whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
1345
+ */
1346
+ async getBranchProtection(branch) {
1347
+ try {
1348
+ return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
1349
+ } catch (err) {
1350
+ if (!(err instanceof GitHubError)) throw err;
1351
+ if (err.status === 404) return null;
1352
+ if (err.status === 403 || err.status === 401) {
1353
+ const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
1354
+ return b?.protected === false ? null : void 0;
1355
+ }
1356
+ throw err;
1357
+ }
910
1358
  }
911
1359
  /** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
912
1360
  async createCheckRun(input) {
@@ -923,13 +1371,13 @@ class GitHubClient {
923
1371
  await this.request("DELETE", `${this.repoPath}/issues/${issueNumber}/labels/${encodeURIComponent(label)}`);
924
1372
  }
925
1373
  }
926
- function toCommitSummary(c2) {
1374
+ function toCommitSummary(c10) {
927
1375
  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 ?? ""
1376
+ sha: c10.sha,
1377
+ parents: (c10.parents ?? []).map((p) => p.sha),
1378
+ date: c10.commit?.committer?.date ?? c10.commit?.author?.date ?? "",
1379
+ author: c10.author?.login ?? c10.commit?.author?.name ?? "unknown",
1380
+ message: c10.commit?.message ?? ""
933
1381
  };
934
1382
  }
935
1383
  function toSummary(r2) {
@@ -948,7 +1396,9 @@ function toSummary(r2) {
948
1396
  ...typeof r2.body === "string" ? { body: r2.body } : {}
949
1397
  };
950
1398
  }
951
- function friendlyMessage(status, detail) {
1399
+ function friendlyMessage(status, detail, info = {}) {
1400
+ if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
1401
+ if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
952
1402
  switch (status) {
953
1403
  case 401:
954
1404
  return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
@@ -975,7 +1425,7 @@ async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
975
1425
  const warnings = [];
976
1426
  let config;
977
1427
  let configText;
978
- const configEntry = entries.find((e3) => e3.type === "blob" && e3.path === CONFIG_FILE);
1428
+ const configEntry = entries.find((e2) => e2.type === "blob" && e2.path === CONFIG_FILE);
979
1429
  if (configEntry) {
980
1430
  try {
981
1431
  configText = await client.getBlobText(configEntry.sha);
@@ -988,16 +1438,16 @@ async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
988
1438
  const checksFile = config?.tokens.contrastChecks ?? DEFAULT_TOKEN_PATHS.contrastChecks;
989
1439
  const prefix = tokensPath2 + "/";
990
1440
  const wanted = entries.filter(
991
- (e3) => e3.type === "blob" && e3.path.startsWith(prefix) && (e3.path.endsWith(".tokens.json") || e3.path === prefix + checksFile)
1441
+ (e2) => e2.type === "blob" && e2.path.startsWith(prefix) && (e2.path.endsWith(".tokens.json") || e2.path === prefix + checksFile)
992
1442
  );
993
- if (!wanted.some((e3) => e3.path.endsWith(".tokens.json"))) throw new Error(`No *.tokens.json files found under ${tokensPath2} at ${commitSha.slice(0, 7)}.`);
1443
+ if (!wanted.some((e2) => e2.path.endsWith(".tokens.json"))) throw new Error(`No *.tokens.json files found under ${tokensPath2} at ${commitSha.slice(0, 7)}.`);
994
1444
  const files = {};
995
1445
  let contrastChecks;
996
1446
  const queue = [...wanted];
997
1447
  const worker = async () => {
998
- for (let e3 = queue.shift(); e3; e3 = queue.shift()) {
999
- const text = await client.getBlobText(e3.sha);
1000
- const rel = e3.path.slice(prefix.length);
1448
+ for (let e2 = queue.shift(); e2; e2 = queue.shift()) {
1449
+ const text = await client.getBlobText(e2.sha);
1450
+ const rel = e2.path.slice(prefix.length);
1001
1451
  if (rel === checksFile) contrastChecks = text;
1002
1452
  else files[rel] = text;
1003
1453
  }
@@ -1005,6 +1455,10 @@ async function loadRepoFiles(client, commitSha, fallbackTokensPath) {
1005
1455
  await Promise.all([worker(), worker(), worker(), worker()]);
1006
1456
  return { commitSha, treeSha, files, contrastChecks, config, ...config ? { configText } : {}, tokensPath: tokensPath2, warnings };
1007
1457
  }
1458
+ function withRulesetUrl(r2) {
1459
+ const href = r2._links?.html?.href;
1460
+ return href && !r2.html_url ? { ...r2, html_url: href } : r2;
1461
+ }
1008
1462
  function toIssue(r2) {
1009
1463
  return {
1010
1464
  number: r2.number,
@@ -1019,47 +1473,197 @@ function toIssue(r2) {
1019
1473
  updatedAt: r2.updated_at
1020
1474
  };
1021
1475
  }
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);
1029
- }
1030
- return t3;
1031
- };
1032
- const root = config.tokens.root.replace(/\/$/, "");
1033
- const prefix = root === "." || !root ? "" : `${root}/`;
1034
- const limit = Math.max(1, opts.concurrency ?? 4);
1035
- const pool = async (items, fn) => {
1036
- const out = new Array(items.length);
1037
- let i2 = 0;
1038
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
1039
- for (let k3 = i2++; k3 < items.length; k3 = i2++) out[k3] = await fn(items[k3]);
1040
- }));
1041
- return out;
1042
- };
1043
- return {
1044
- tree,
1045
- async listCommits({ ref, paths, limit: max }) {
1046
- const commits = [];
1047
- for (let page = 1; commits.length < max && page <= 10; page++) {
1048
- 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() });
1052
- }
1053
- if (batch.length < Math.min(100, max)) break;
1054
- }
1055
- const list = commits.slice(0, max);
1476
+ async function loadProtection(client, defaultBranch) {
1477
+ 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);
1478
+ const classic = await client.getBranchProtection(defaultBranch).catch(() => void 0);
1479
+ return protectionState(rulesets, classic, defaultBranch);
1480
+ }
1481
+ async function loadRequiredChecks(client, defaultBranch) {
1482
+ const onDefault = await client.listCheckRuns(defaultBranch).catch(() => []);
1483
+ const merged = await client.listPullRequests("closed").then((prs) => prs.find((p) => p.state === "merged" && p.baseRef === defaultBranch)).catch(() => void 0);
1484
+ const onPull = merged?.headSha ? await client.listCheckRuns(merged.headSha).catch(() => []) : [];
1485
+ return requiredCheckNames(onDefault, onPull);
1486
+ }
1487
+ async function protectLibrary(client, defaultBranch, opts = {}) {
1488
+ const checks = opts.checks ?? await loadRequiredChecks(client, defaultBranch);
1489
+ const payload = protectionRuleset({ repo: { defaultBranch } }, { checks, approvals: opts.approvals ?? 1, adminBypass: opts.adminBypass ?? true });
1490
+ const id = opts.state?.bridgeRulesetId;
1491
+ return id !== void 0 ? client.updateRuleset(id, payload) : client.createRuleset(payload);
1492
+ }
1493
+ const PLAN_REFUSED = /upgrade to github (pro|team)|make this repository public|not available for private repositories/i;
1494
+ const isPlanRefusal = (err) => err instanceof GitHubError && (err.status === 403 || err.status === 422) && PLAN_REFUSED.test(`${err.detail ?? ""} ${err.message}`);
1495
+ const rulesSettingsUrl = (owner, repo) => `https://github.com/${owner}/${repo}/settings/rules`;
1496
+ function protectRefusal(err) {
1497
+ if (!(err instanceof GitHubError)) return null;
1498
+ 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.";
1499
+ 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.";
1500
+ 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.";
1501
+ 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.";
1502
+ return null;
1503
+ }
1504
+ const VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
1505
+ const IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
1506
+ function cssVarsIn(text) {
1507
+ const out = /* @__PURE__ */ new Set();
1508
+ for (const m2 of text.matchAll(VAR_RE)) out.add(m2[1]);
1509
+ return [...out];
1510
+ }
1511
+ function importsIn(text) {
1512
+ const out = /* @__PURE__ */ new Set();
1513
+ for (const m2 of text.matchAll(IMPORT_RE)) out.add(m2[1] ?? m2[2]);
1514
+ return [...out];
1515
+ }
1516
+ const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
1517
+ const isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
1518
+ const isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
1519
+ const isCss = (p) => /\.(css|scss|less)$/.test(p);
1520
+ const isScript = (p) => /\.[cm]?[jt]sx?$/.test(p);
1521
+ function componentOf(from, spec, componentsDir) {
1522
+ if (!spec.startsWith(".")) return null;
1523
+ const parts = from.split("/").slice(0, -1);
1524
+ for (const seg of spec.split("/")) {
1525
+ if (seg === "." || seg === "") continue;
1526
+ if (seg === "..") parts.pop();
1527
+ else parts.push(seg);
1528
+ }
1529
+ const abs = parts.join("/");
1530
+ if (!abs.startsWith(`${componentsDir}/`)) return null;
1531
+ return abs.slice(componentsDir.length + 1).split("/")[0] || null;
1532
+ }
1533
+ function componentForPath(path, componentsDir) {
1534
+ const dir = norm(componentsDir);
1535
+ const p = norm(path);
1536
+ if (!p.startsWith(`${dir}/`)) return null;
1537
+ const rest = p.slice(dir.length + 1).split("/");
1538
+ return rest.length >= 2 ? rest[0] : null;
1539
+ }
1540
+ function buildUsageIndex(files, componentsDir) {
1541
+ const dir = norm(componentsDir);
1542
+ const modules = {};
1543
+ const vars = /* @__PURE__ */ new Map();
1544
+ const imports = /* @__PURE__ */ new Map();
1545
+ const storyImports = /* @__PURE__ */ new Map();
1546
+ const globalVars = {};
1547
+ const get = (m2, k3) => m2.get(k3) ?? m2.set(k3, /* @__PURE__ */ new Set()).get(k3);
1548
+ for (const f of files) {
1549
+ const path = norm(f.path);
1550
+ const id = componentForPath(path, dir);
1551
+ if (!id) {
1552
+ if (isCss(path)) {
1553
+ const v2 = cssVarsIn(f.text);
1554
+ if (v2.length) globalVars[path] = v2.sort();
1555
+ }
1556
+ continue;
1557
+ }
1558
+ const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
1559
+ mod.files.push(path);
1560
+ if (isTest(path)) continue;
1561
+ for (const v2 of cssVarsIn(f.text)) get(vars, id).add(v2);
1562
+ if (!isScript(path)) continue;
1563
+ for (const spec of importsIn(f.text)) {
1564
+ const dep = componentOf(path, spec, dir);
1565
+ if (!dep || dep === id) continue;
1566
+ get(isStory(path) ? storyImports : imports, id).add(dep);
1567
+ }
1568
+ }
1569
+ const byVar = {};
1570
+ for (const mod of Object.values(modules)) {
1571
+ mod.files.sort();
1572
+ mod.vars = [...vars.get(mod.id) ?? []].sort();
1573
+ mod.imports = [...imports.get(mod.id) ?? []].sort();
1574
+ mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d2) => !mod.imports.includes(d2)).sort();
1575
+ for (const v2 of mod.vars) (byVar[v2] ??= []).push(mod.id);
1576
+ }
1577
+ for (const k3 of Object.keys(byVar)) byVar[k3].sort();
1578
+ return { componentsDir: dir, modules, byVar, globalVars };
1579
+ }
1580
+ function impactedModules(index, input) {
1581
+ const reasons = /* @__PURE__ */ new Map();
1582
+ const add = (id, reason, indirect) => {
1583
+ const r2 = reasons.get(id);
1584
+ if (r2) {
1585
+ if (!r2.reasons.includes(reason)) r2.reasons.push(reason);
1586
+ r2.indirect &&= indirect;
1587
+ } else reasons.set(id, { reasons: [reason], indirect });
1588
+ };
1589
+ const changedVars = new Set(input.vars ?? []);
1590
+ for (const v2 of changedVars) for (const id of index.byVar[v2] ?? []) add(id, `uses ${v2}`, false);
1591
+ for (const f of input.files ?? []) {
1592
+ const id = componentForPath(f, index.componentsDir);
1593
+ if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
1594
+ }
1595
+ const renderedBy = /* @__PURE__ */ new Map();
1596
+ for (const m2 of Object.values(index.modules)) for (const d2 of m2.imports) (renderedBy.get(d2) ?? renderedBy.set(d2, []).get(d2)).push(m2.id);
1597
+ const queue = [...reasons.keys()];
1598
+ while (queue.length) {
1599
+ const id = queue.shift();
1600
+ for (const parent of renderedBy.get(id) ?? []) {
1601
+ const known = reasons.has(parent);
1602
+ add(parent, `renders ${id}`, true);
1603
+ if (!known) queue.push(parent);
1604
+ }
1605
+ }
1606
+ const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v2) => changedVars.has(v2)) })).filter((g3) => g3.vars.length > 0);
1607
+ 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));
1608
+ return { modules, globalHits };
1609
+ }
1610
+ function tokenUsage(tokens, reads) {
1611
+ const readVars = /* @__PURE__ */ new Set();
1612
+ for (const [v2, mods] of Object.entries(reads.byVar ?? {})) if (mods.length) readVars.add(v2);
1613
+ for (const vars of Object.values(reads.globalVars ?? {})) for (const v2 of vars) readVars.add(v2);
1614
+ const byPath = new Map(tokens.map((t3) => [t3.path, t3]));
1615
+ const direct = new Set(tokens.filter((t3) => readVars.has(t3.cssVar)).map((t3) => t3.path));
1616
+ const used = /* @__PURE__ */ new Set();
1617
+ const queue = [...direct];
1618
+ while (queue.length) {
1619
+ const p = queue.pop();
1620
+ if (used.has(p)) continue;
1621
+ used.add(p);
1622
+ for (const r2 of byPath.get(p)?.refs ?? []) if (!used.has(r2) && byPath.has(r2)) queue.push(r2);
1623
+ }
1624
+ return { used, direct };
1625
+ }
1626
+ function gitHubHistoryBackend(client, config, opts = {}) {
1627
+ const trees = /* @__PURE__ */ new Map();
1628
+ const tree = (sha) => {
1629
+ let t3 = trees.get(sha);
1630
+ if (!t3) {
1631
+ t3 = client.getCommitTree(sha).then((ts) => client.getTree(ts, true)).then((r2) => r2.entries);
1632
+ trees.set(sha, t3);
1633
+ }
1634
+ return t3;
1635
+ };
1636
+ const root = config.tokens.root.replace(/\/$/, "");
1637
+ const prefix = root === "." || !root ? "" : `${root}/`;
1638
+ const limit = Math.max(1, opts.concurrency ?? 4);
1639
+ const pool = async (items, fn) => {
1640
+ const out = new Array(items.length);
1641
+ let i2 = 0;
1642
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
1643
+ for (let k3 = i2++; k3 < items.length; k3 = i2++) out[k3] = await fn(items[k3]);
1644
+ }));
1645
+ return out;
1646
+ };
1647
+ return {
1648
+ tree,
1649
+ async listCommits({ ref, paths, limit: max }) {
1650
+ const commits = [];
1651
+ for (let page = 1; commits.length < max && page <= 10; page++) {
1652
+ const batch = await client.listCommits({ sha: ref, path: paths[0], perPage: Math.min(100, max), page });
1653
+ for (const c10 of batch) {
1654
+ const [subject, ...rest] = c10.message.split("\n");
1655
+ commits.push({ sha: c10.sha, parents: c10.parents, date: c10.date, author: c10.author, subject: subject ?? "", body: rest.join("\n").trim() });
1656
+ }
1657
+ if (batch.length < Math.min(100, max)) break;
1658
+ }
1659
+ const list = commits.slice(0, max);
1056
1660
  const merges = /* @__PURE__ */ new Map();
1057
- const enriched = await pool(list, async (c2) => {
1058
- const prs = await client.listPullRequestsForCommit(c2.sha).catch(() => []);
1059
- const pr = prs.find((p2) => p2.mergedAt && p2.baseRef === ref) ?? prs.find((p2) => p2.mergedAt);
1060
- if (!pr) return c2;
1661
+ const enriched = await pool(list, async (c10) => {
1662
+ const prs = await client.listPullRequestsForCommit(c10.sha).catch(() => []);
1663
+ const pr = prs.find((p) => p.mergedAt && p.baseRef === ref) ?? prs.find((p) => p.mergedAt);
1664
+ if (!pr) return c10;
1061
1665
  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) {
1666
+ if (pr.mergeCommitSha && pr.mergeCommitSha !== c10.sha) {
1063
1667
  let m2 = merges.get(pr.mergeCommitSha);
1064
1668
  if (!m2) {
1065
1669
  m2 = client.getCommitInfo(pr.mergeCommitSha).then(
@@ -1074,26 +1678,26 @@ function gitHubHistoryBackend(client, config, opts = {}) {
1074
1678
  const merge = await m2;
1075
1679
  if (merge) return { ...merge, pr: prInfo };
1076
1680
  }
1077
- return { ...c2, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
1681
+ return { ...c10, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
1078
1682
  });
1079
1683
  return enriched;
1080
1684
  },
1081
1685
  async tokenFiles(sha) {
1082
1686
  const entries = await tree(sha);
1083
- const blobs = entries.filter((e3) => e3.type === "blob" && e3.path.startsWith(prefix));
1084
- const picked = new Set(selectTokenFiles(config, blobs.map((e3) => e3.path)));
1085
- const wanted = blobs.filter((e3) => picked.has(prefix ? e3.path.slice(prefix.length) : e3.path));
1086
- const texts = await pool(wanted, async (e3) => [prefix ? e3.path.slice(prefix.length) : e3.path, await client.getBlobText(e3.sha)]);
1687
+ const blobs = entries.filter((e2) => e2.type === "blob" && e2.path.startsWith(prefix));
1688
+ const picked = new Set(selectTokenFiles(config, blobs.map((e2) => e2.path)));
1689
+ const wanted = blobs.filter((e2) => picked.has(prefix ? e2.path.slice(prefix.length) : e2.path));
1690
+ const texts = await pool(wanted, async (e2) => [prefix ? e2.path.slice(prefix.length) : e2.path, await client.getBlobText(e2.sha)]);
1087
1691
  return new Map(texts);
1088
1692
  },
1089
1693
  async changesetPaths(sha) {
1090
1694
  const dir = config.changesets?.dir.replace(/\/$/, "");
1091
1695
  if (!dir) return [];
1092
- return (await tree(sha)).filter((e3) => e3.type === "blob" && e3.path.startsWith(`${dir}/`)).map((e3) => e3.path);
1696
+ return (await tree(sha)).filter((e2) => e2.type === "blob" && e2.path.startsWith(`${dir}/`)).map((e2) => e2.path);
1093
1697
  },
1094
1698
  async readFile(sha, path) {
1095
- const e3 = (await tree(sha)).find((x) => x.type === "blob" && x.path === path);
1096
- return e3 ? client.getBlobText(e3.sha) : null;
1699
+ const e2 = (await tree(sha)).find((x) => x.type === "blob" && x.path === path);
1700
+ return e2 ? client.getBlobText(e2.sha) : null;
1097
1701
  }
1098
1702
  };
1099
1703
  }
@@ -1114,9 +1718,9 @@ async function loadGitHubHistory(client, config, opts = {}) {
1114
1718
  if (head) {
1115
1719
  const files = await backend.tree(head).catch(() => []);
1116
1720
  for (const dir of packageDirCandidates(config)) {
1117
- const p2 = (f) => dir ? `${dir}/${f}` : f;
1118
- const cl = files.find((e3) => e3.path === p2("CHANGELOG.md"));
1119
- const pkg = files.find((e3) => e3.path === p2("package.json"));
1721
+ const p = (f) => dir ? `${dir}/${f}` : f;
1722
+ const cl = files.find((e2) => e2.path === p("CHANGELOG.md"));
1723
+ const pkg = files.find((e2) => e2.path === p("package.json"));
1120
1724
  if (!cl && !pkg) continue;
1121
1725
  if (cl) changelog = parseChangelog(await client.getBlobText(cl.sha));
1122
1726
  if (pkg) {
@@ -1164,17 +1768,47 @@ function demoHistory(config, current) {
1164
1768
  }
1165
1769
  const entries = [];
1166
1770
  kept.forEach((step, i2) => {
1167
- const e3 = buildHistoryEntry({
1771
+ const e2 = buildHistoryEntry({
1168
1772
  config,
1169
1773
  commit: { sha: `demo${String(step.pr).padStart(36, "0")}`, parents: [`demo${String(step.pr - 1).padStart(36, "0")}`], date: step.date, author: step.author, subject: step.title, body: step.why, pr: { number: step.pr, title: step.title, author: step.author, mergedAt: step.date, body: step.why } },
1170
1774
  before: states[i2],
1171
1775
  after: states[i2 + 1],
1172
1776
  demo: true
1173
1777
  });
1174
- if (e3) entries.push(e3);
1778
+ if (e2) entries.push(e2);
1175
1779
  });
1176
1780
  return { ...buildReport({ source: "demo", label: "Demo history (fixture data)", ref: "demo", entries, releases: DEMO_RELEASES, truncated: false }), demo: true };
1177
1781
  }
1782
+ async function branchPreviewUrls(gh, ref, config, opts = {}) {
1783
+ const apps = config?.previews?.apps ?? {};
1784
+ if (!Object.values(apps).some(Boolean)) return [];
1785
+ let pr;
1786
+ let branch = opts.branch;
1787
+ let sha = opts.sha;
1788
+ if (typeof ref === "number") {
1789
+ pr = ref;
1790
+ if (!branch || !sha) {
1791
+ const p = await gh.getPullRequest(ref);
1792
+ branch ??= p.headRef;
1793
+ sha ??= p.headSha;
1794
+ }
1795
+ } else {
1796
+ sha = ref;
1797
+ if (!branch || !opts.comments) {
1798
+ const open = (await gh.listPullRequestsForCommit(ref).catch(() => [])).find((p) => p.state === "open");
1799
+ if (open) {
1800
+ pr = open.number;
1801
+ branch ??= open.headRef;
1802
+ }
1803
+ }
1804
+ }
1805
+ const [comments, statuses, deployments] = await Promise.all([
1806
+ opts.comments ? Promise.resolve(opts.comments) : pr !== void 0 ? gh.listIssueComments(pr).catch(() => []) : Promise.resolve([]),
1807
+ sha ? gh.listCommitStatuses(sha).catch(() => []) : Promise.resolve([]),
1808
+ sha ? gh.listDeployments(sha).catch(() => []) : Promise.resolve([])
1809
+ ]);
1810
+ return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
1811
+ }
1178
1812
  const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
1179
1813
  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
1814
  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 +1830,16 @@ function oklchToLinear(L, C2, h3) {
1196
1830
  ];
1197
1831
  }
1198
1832
  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;
1833
+ const c10 = Math.min(1, Math.max(0, x));
1834
+ return c10 <= 31308e-7 ? 12.92 * c10 : 1.055 * c10 ** (1 / 2.4) - 0.055;
1201
1835
  };
1202
1836
  const decode = (x) => x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
1203
1837
  function oklch(L, C2, h3) {
1204
- let c2 = C2;
1205
- let lin = oklchToLinear(L, c2, h3);
1838
+ let c10 = C2;
1839
+ let lin = oklchToLinear(L, c10, h3);
1206
1840
  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);
1841
+ c10 *= 0.95;
1842
+ lin = oklchToLinear(L, c10, h3);
1209
1843
  }
1210
1844
  return lin.map(encode);
1211
1845
  }
@@ -1286,12 +1920,12 @@ const brandDisplayName = (slug) => slug.split("-").filter(Boolean).map((w) => w[
1286
1920
  function brandLayers(config) {
1287
1921
  const out = {};
1288
1922
  for (const l2 of config.tokens.layers) {
1289
- const ph = (l2.files.match(/\{([^}]+)\}/g) ?? []).map((p2) => p2.slice(1, -1));
1923
+ const ph = (l2.files.match(/\{([^}]+)\}/g) ?? []).map((p) => p.slice(1, -1));
1290
1924
  if (!ph.includes("brand")) continue;
1291
1925
  if (ph.length === 1 && !out.brand) out.brand = l2.files;
1292
1926
  else if (ph.length === 2 && !out.override) {
1293
1927
  out.override = l2.files;
1294
- out.overrideAxis = ph.find((p2) => p2 !== "brand");
1928
+ out.overrideAxis = ph.find((p) => p !== "brand");
1295
1929
  }
1296
1930
  }
1297
1931
  return out;
@@ -1426,7 +2060,7 @@ function planNewBrand(source, input) {
1426
2060
  const doc = JSON.parse(templateText);
1427
2061
  const before = new LayeredTokenSet(config, files);
1428
2062
  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);
2063
+ const actionStep = Number(/^palette\.primary\.(\d+)$/.exec(chain.find((c10) => /^palette\.primary\.\d+$/.test(c10)) ?? "")?.[1] ?? 600);
1430
2064
  const anchors = {};
1431
2065
  const safeRamp = (role, hue, kind, pin) => {
1432
2066
  try {
@@ -1434,8 +2068,8 @@ function planNewBrand(source, input) {
1434
2068
  if (r2.issue) issues.push({ ...r2.issue, path: `palette.${role}` });
1435
2069
  anchors[role] = r2.anchor;
1436
2070
  return r2.ramp;
1437
- } catch (e3) {
1438
- issues.push({ level: "error", code: "invalid-color", message: e3.message, path: `palette.${role}` });
2071
+ } catch (e2) {
2072
+ issues.push({ level: "error", code: "invalid-color", message: e2.message, path: `palette.${role}` });
1439
2073
  return void 0;
1440
2074
  }
1441
2075
  };
@@ -1520,7 +2154,7 @@ function planNewBrand(source, input) {
1520
2154
  const primDir = config.tokens.tiers.primitive?.[0]?.replace(/\/$/, "") ?? config.tokens.primitives;
1521
2155
  const primFile = `${primDir}/${slug}.tokens.json`;
1522
2156
  if (prims.size && files.has(primFile)) issues.push({ level: "error", code: "slug-file-exists", message: `${tokensPath(config, primFile)} already exists. Pick another name.` });
1523
- for (const r2 of prims.keys()) if (before.allPaths().some((p2) => p2.startsWith(`color.${r2}.`))) issues.push({ level: "error", code: "ramp-exists", message: `A primitive ramp color.${r2} already exists. Pick another name.` });
2157
+ for (const r2 of prims.keys()) if (before.allPaths().some((p) => p.startsWith(`color.${r2}.`))) issues.push({ level: "error", code: "ramp-exists", message: `A primitive ramp color.${r2} already exists. Pick another name.` });
1524
2158
  doc.$description = `${name}: created with Bridge, started from ${blank ? "a blank brand" : brandDisplayName(from)}.`;
1525
2159
  doc.$extensions = { ...isObj(doc.$extensions) ? doc.$extensions : {}, "dev.bridge.brand": { name, from, ...Object.keys(anchors).length ? { anchors } : {} } };
1526
2160
  const { $description, $extensions, ...rest } = doc;
@@ -1562,8 +2196,8 @@ function planNewBrand(source, input) {
1562
2196
  try {
1563
2197
  configText = addBrandToConfigText(baseConfigText, slug);
1564
2198
  nextConfig = issues.some((i2) => i2.code.startsWith("slug-")) ? config : loadConfig(configText);
1565
- } catch (e3) {
1566
- issues.push({ level: "error", code: "config", message: e3.message });
2199
+ } catch (e2) {
2200
+ issues.push({ level: "error", code: "config", message: e2.message });
1567
2201
  }
1568
2202
  const after = new LayeredTokenSet(nextConfig, nextFiles);
1569
2203
  const key = (i2) => `${i2.code}|${i2.path ?? ""}|${i2.message}`;
@@ -1576,10 +2210,10 @@ function planNewBrand(source, input) {
1576
2210
  }
1577
2211
  const pairs = parseContrastChecks(source.contrastText);
1578
2212
  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)));
2213
+ const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c10) => c10.brand === slug) : [];
2214
+ const results = combos.flatMap((c10) => pairs.map((p) => checkLayeredPair(after, p, c10)));
1581
2215
  const failing = results.filter((r2) => !r2.pass);
1582
- const themes = [...new Set(combos.map((c2) => c2.theme).filter((t3) => !!t3))];
2216
+ const themes = [...new Set(combos.map((c10) => c10.theme).filter((t3) => !!t3))];
1583
2217
  const ramps = {};
1584
2218
  if (inGate) {
1585
2219
  const combo = after.normalizeCombo({ brand: slug });
@@ -1591,7 +2225,7 @@ function planNewBrand(source, input) {
1591
2225
  if (JSON.stringify(r2) !== JSON.stringify(ramps.primary)) ramps[role] = r2;
1592
2226
  }
1593
2227
  }
1594
- const addedTokens = after.defs.filter((d) => created.includes(d.file)).map((d) => ({ kind: "added", path: d.path }));
2228
+ const addedTokens = after.defs.filter((d2) => created.includes(d2.file)).map((d2) => ({ kind: "added", path: d2.path }));
1595
2229
  const risk = classifyRisk({ tokenChanges: addedTokens, files: [{ path: CONFIG_FILE, status: "modified" }], config });
1596
2230
  const planned = [
1597
2231
  ...created.map((f) => ({ path: tokensPath(config, f), action: "create", content: nextFiles.get(f) })),
@@ -1653,10 +2287,10 @@ function newBrandSection(plan) {
1653
2287
  `| --- | ${roles.map(() => "---").join(" | ")} |`,
1654
2288
  ...STEPS.map((s) => `| ${s}${s === plan.actionStep ? " (actions)" : ""} | ${roles.map((r2) => code(plan.ramps[r2].find((x) => x.step === s)?.hex ?? "")).join(" | ")} |`)
1655
2289
  ].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);
2290
+ const c10 = plan.contrast;
2291
+ 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 ")}.`;
2292
+ 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} |`);
2293
+ 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
2294
  return [
1661
2295
  "## New brand",
1662
2296
  "",
@@ -1743,21 +2377,21 @@ function cssModuleClasses(css) {
1743
2377
  const cell$1 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
1744
2378
  function propsTable(name, props, inherits) {
1745
2379
  if (!props.length) return inherits ? `${name} has no props of its own. It accepts every native <${inherits}> attribute.` : "";
1746
- const rows = props.map((p2) => `| \`${p2.name}\`${p2.required ? " (required)" : ""} | \`${cell$1(p2.type)}\` | ${p2.defaultValue ? `\`${cell$1(p2.defaultValue)}\`` : ""} | ${cell$1(p2.description ?? "")} |`);
2380
+ const rows = props.map((p) => `| \`${p.name}\`${p.required ? " (required)" : ""} | \`${cell$1(p.type)}\` | ${p.defaultValue ? `\`${cell$1(p.defaultValue)}\`` : ""} | ${cell$1(p.description ?? "")} |`);
1747
2381
  const tail = inherits ? `
1748
2382
 
1749
2383
  Also accepts every native <${inherits}> attribute.` : "";
1750
2384
  return ["| Prop | Type | Default | Description |", "| --- | --- | --- | --- |", ...rows].join("\n") + tail;
1751
2385
  }
1752
2386
  const isEmpty = (v2) => v2 === void 0 || v2 === null || typeof v2 === "string" && !v2.trim() || Array.isArray(v2) && v2.length === 0;
1753
- function missingProse(d) {
1754
- return PROSE_KEYS.filter((k3) => isEmpty(d?.[k3]));
2387
+ function missingProse(d2) {
2388
+ return PROSE_KEYS.filter((k3) => isEmpty(d2?.[k3]));
1755
2389
  }
1756
2390
  function prefillDocs(input) {
1757
2391
  const existing = input.existing ?? null;
1758
2392
  const anatomy = {};
1759
- for (const c2 of input.cssClasses) anatomy[c2] = existing?.anatomy?.[c2] ?? anatomyLabel(c2);
1760
- 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"}).`);
2393
+ for (const c10 of input.cssClasses) anatomy[c10] = existing?.anatomy?.[c10] ?? anatomyLabel(c10);
2394
+ const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p) => `${p.foreground} on ${p.background}: ${p.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p.min}:1 (${p.ratio >= p.min ? "passes" : "fails"}).`);
1761
2395
  const missing = missingProse(existing);
1762
2396
  const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
1763
2397
  const draft = {
@@ -1773,6 +2407,7 @@ function prefillDocs(input) {
1773
2407
  contentGuidelines: existing?.contentGuidelines ?? [],
1774
2408
  anatomy,
1775
2409
  ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
2410
+ ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
1776
2411
  ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
1777
2412
  };
1778
2413
  return {
@@ -1787,7 +2422,17 @@ function prefillDocs(input) {
1787
2422
  draft
1788
2423
  };
1789
2424
  }
1790
- function docsPrompt(prefill, conventions = {}) {
2425
+ const SECTION_SHAPE = {
2426
+ summary: '"summary": "one or two sentences"',
2427
+ whenToUse: '"whenToUse": ["..."]',
2428
+ whenNotToUse: '"whenNotToUse": ["..."]',
2429
+ dos: '"dos": [{ "title": "...", "text": "..." }]',
2430
+ donts: '"donts": [{ "title": "...", "text": "..." }]',
2431
+ accessibility: '"accessibility": ["..."]',
2432
+ contentGuidelines: '"contentGuidelines": ["..."]'
2433
+ };
2434
+ const docsSectionShapes = (keys) => `{ ${keys.map((k3) => SECTION_SHAPE[k3]).join(", ")} }`;
2435
+ function docsPrompt(prefill, conventions = {}, options = {}) {
1791
2436
  const facts = [
1792
2437
  `Component: ${prefill.component}`,
1793
2438
  prefill.propsTable ? `Props (parsed from TypeScript):
@@ -1802,15 +2447,33 @@ ${prefill.propsTable}` : "Props: none of its own.",
1802
2447
  const block = (title, text) => text?.trim() ? `<${title}>
1803
2448
  ${text.trim()}
1804
2449
  </${title}>` : "";
2450
+ const keys = options.keys;
2451
+ const check = keys ? (options.check ?? []).filter((k3) => keys.includes(k3)) : [];
2452
+ const write = keys ? keys.filter((k3) => !check.includes(k3)) : [];
2453
+ const task = keys ? [
2454
+ ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
2455
+ ...check.length ? [
2456
+ `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
2457
+ "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.",
2458
+ "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."
2459
+ ] : [],
2460
+ ...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."] : []
2461
+ ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
2462
+ const shape2 = keys ? [
2463
+ `- ${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.`,
2464
+ `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
2465
+ ] : [
2466
+ `- 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.`,
2467
+ "- Answer with the full docs.json as one JSON object, nothing else."
2468
+ ];
1805
2469
  return [
1806
- `Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`,
2470
+ ...task,
1807
2471
  "",
1808
2472
  "Rules:",
1809
2473
  "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
1810
2474
  "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
1811
2475
  '- 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.",
2476
+ ...shape2,
1814
2477
  "",
1815
2478
  "Facts computed from the code:",
1816
2479
  facts.join("\n\n"),
@@ -1819,23 +2482,35 @@ ${text.trim()}
1819
2482
  ${JSON.stringify(prefill.draft, null, 2)}`,
1820
2483
  "",
1821
2484
  block("source", conventions.source),
2485
+ block("styles", conventions.styles),
1822
2486
  block("agents-md", conventions.agents),
1823
2487
  block("voice", conventions.voice),
1824
2488
  block("example-docs-json", conventions.example)
1825
2489
  ].filter((x, i2, all) => x !== "" || all[i2 - 1] !== "").join("\n");
1826
2490
  }
1827
- function markClaudeDraft(d) {
1828
- return { ...d, draftedBy: "claude", needsReview: true };
2491
+ function markClaudeDraft(d2, sections) {
2492
+ if (!sections) {
2493
+ const { draftedSections: _all, ...rest } = d2;
2494
+ return { ...rest, draftedBy: "claude", needsReview: true };
2495
+ }
2496
+ const waiting = d2.needsReview === true ? d2.draftedSections ?? (d2.draftedBy ? [...PROSE_KEYS] : []) : [];
2497
+ const union = PROSE_KEYS.filter((k3) => waiting.includes(k3) || sections.includes(k3));
2498
+ return { ...d2, draftedBy: "claude", needsReview: true, draftedSections: union };
2499
+ }
2500
+ function sectionsNeedingReview(d2) {
2501
+ if (!d2 || d2.needsReview !== true) return [];
2502
+ return d2.draftedSections ? PROSE_KEYS.filter((k3) => d2.draftedSections.includes(k3)) : [...PROSE_KEYS];
1829
2503
  }
1830
- const needsDocsReview = (d) => Boolean(d && d.needsReview === true);
1831
- function acceptDocsDraft(d) {
1832
- return { ...d, needsReview: false };
2504
+ const needsDocsReview = (d2) => Boolean(d2 && d2.needsReview === true);
2505
+ function acceptDocsDraft(d2) {
2506
+ const { draftedSections: _done, ...rest } = d2;
2507
+ return { ...rest, needsReview: false };
1833
2508
  }
1834
- function formatDocsJson(d) {
1835
- const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k3) => k3 !== "summary"), "draftedBy", "needsReview"];
2509
+ function formatDocsJson(d2) {
2510
+ const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k3) => k3 !== "summary"), "draftedBy", "draftedSections", "needsReview"];
1836
2511
  const out = {};
1837
- for (const k3 of order) if (d[k3] !== void 0) out[k3] = d[k3];
1838
- for (const [k3, v2] of Object.entries(d)) if (!(k3 in out) && v2 !== void 0) out[k3] = v2;
2512
+ for (const k3 of order) if (d2[k3] !== void 0) out[k3] = d2[k3];
2513
+ for (const [k3, v2] of Object.entries(d2)) if (!(k3 in out) && v2 !== void 0) out[k3] = v2;
1839
2514
  return `${JSON.stringify(out, null, 2)}
1840
2515
  `;
1841
2516
  }
@@ -1843,7 +2518,7 @@ function patchNode(old, next) {
1843
2518
  if (plainEqual(toPlain(old), next)) return old;
1844
2519
  if (old.kind === "object" && next && typeof next === "object" && !Array.isArray(next)) {
1845
2520
  const n2 = next;
1846
- const kept = old.entries.filter((e3) => n2[e3.key] !== void 0).map((e3) => ({ key: e3.key, value: patchNode(e3.value, n2[e3.key]) }));
2521
+ const kept = old.entries.filter((e2) => n2[e2.key] !== void 0).map((e2) => ({ key: e2.key, value: patchNode(e2.value, n2[e2.key]) }));
1847
2522
  const added = Object.keys(n2).filter((k3) => n2[k3] !== void 0 && !getEntry(old, k3)).map((k3) => ({ key: k3, value: fromPlain(n2[k3], void 0, old.inline) }));
1848
2523
  return { kind: "object", inline: old.inline, entries: [...kept, ...added] };
1849
2524
  }
@@ -1867,6 +2542,416 @@ function acceptDocsText(text) {
1867
2542
  return patchDocsJson(text, acceptDocsDraft(JSON.parse(text)));
1868
2543
  }
1869
2544
  const DOCS_CHANGE_BUMP = "patch";
2545
+ function cleanDraftText(text) {
2546
+ return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
2547
+ }
2548
+ const values = (s) => (s ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
2549
+ const article = (word) => /^[aeiou]/i.test(word) ? "an" : "a";
2550
+ const andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
2551
+ const plural = (n2, word, many = `${word}s`) => `${n2} ${n2 === 1 ? word : many}`;
2552
+ const capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2553
+ const lower = (s) => s.charAt(0).toLowerCase() + s.slice(1);
2554
+ function propShape(s) {
2555
+ if (!s) return void 0;
2556
+ const m2 = /^(.*?), default (.*)$/.exec(s);
2557
+ return m2 ? { type: m2[1], def: m2[2] } : { type: s, def: "" };
2558
+ }
2559
+ const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
2560
+ function shortVariant(key) {
2561
+ const pairs = key.split(", ").map((p) => {
2562
+ const at2 = p.indexOf("=");
2563
+ return at2 < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at2).trim(), v: p.slice(at2 + 1).trim() };
2564
+ });
2565
+ const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
2566
+ if (!kept.length) return key;
2567
+ const rank = (k3) => {
2568
+ const i2 = VARIANT_FIRST.indexOf(k3.toLowerCase());
2569
+ return i2 < 0 ? VARIANT_FIRST.length : i2;
2570
+ };
2571
+ return kept.map((p, i2) => ({ ...p, i: i2 })).sort((a, b) => rank(a.k) - rank(b.k) || a.i - b.i).map((p) => p.v).join(" ");
2572
+ }
2573
+ const shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
2574
+ function parseStructureChange(c10) {
2575
+ const m2 = /^(New|Removed|Moved) layer "(.*)"(?: \((.*)\))?$/.exec(c10.field);
2576
+ if (!m2) return void 0;
2577
+ const op = m2[1] === "New" ? "added" : m2[1] === "Removed" ? "removed" : "moved";
2578
+ const where = `in ${shortVariants(m2[3])}`;
2579
+ if (op === "moved") return { op, name: m2[2], where, at: c10.after, from: c10.before };
2580
+ const shape2 = (op === "added" ? c10.after : c10.before) ?? "";
2581
+ const sm = /^(\S+)(?: (.*?))?, ((?:after|before) ".*"|alone)$/.exec(shape2);
2582
+ return { op, name: m2[2], where, ...sm ? { type: sm[1], ...sm[2] ? { main: sm[2] } : {}, at: sm[3] } : {} };
2583
+ }
2584
+ function structurePhrase(r2) {
2585
+ const type = r2.type ? `${r2.type} ` : "";
2586
+ const main = r2.main ? ` (${r2.main})` : "";
2587
+ const at2 = r2.at && r2.at !== "alone" ? ` ${r2.at}` : "";
2588
+ if (r2.op === "added") return `New ${type}layer "${r2.name}"${main}${at2} ${r2.where}`;
2589
+ if (r2.op === "removed") return `Removed ${type}layer "${r2.name}"${main}${at2} ${r2.where}`;
2590
+ return `Moved layer "${r2.name}" ${r2.at ?? ""}${r2.from ? ` (was ${r2.from})` : ""} ${r2.where}`.replace(/\s+/g, " ");
2591
+ }
2592
+ const bare = (at2) => (at2 ?? "").replace(/"/g, "");
2593
+ function firstChange(component, kind, c10, all) {
2594
+ switch (kind) {
2595
+ case "new-component": {
2596
+ const axes = all.filter((x) => x.field.startsWith("Variant "));
2597
+ const props = all.filter((x) => x.field.startsWith("Prop "));
2598
+ const parts = [axes.length ? plural(axes.length, "variant axis", "variant axes") : "", props.length ? plural(props.length, "prop") : ""].filter(Boolean);
2599
+ return {
2600
+ title: `Add the ${component} component`,
2601
+ summary: `${component} is not in code yet.${parts.length ? ` The Figma component has ${andList(parts)}.` : ""}`
2602
+ };
2603
+ }
2604
+ case "new-variant": {
2605
+ if (!c10) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
2606
+ const axis = c10.field.replace(/^Variant /, "");
2607
+ const before = values(c10.before);
2608
+ const after = values(c10.after);
2609
+ const added = after.filter((x) => !before.includes(x));
2610
+ const removed = before.filter((x) => !after.includes(x));
2611
+ const word = axis.toLowerCase();
2612
+ 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)}.` };
2613
+ if (!after.length) return { title: `Remove the ${axis} variant axis`, summary: `${component} no longer has the ${axis} variant axis (${andList(before)}).` };
2614
+ if (added.length === 1 && !removed.length) return { title: `Add ${article(added[0])} ${added[0]} ${word}`, summary: `${component} gets a new ${axis} value, ${added[0]}.` };
2615
+ if (added.length && !removed.length) return { title: `Add ${andList(added)} to ${axis}`, summary: `${component} gets ${plural(added.length, `new ${axis} value`)}: ${andList(added)}.` };
2616
+ if (removed.length === 1 && !added.length) return { title: `Remove the ${removed[0]} ${word}`, summary: `${component} no longer has the ${axis} value ${removed[0]}.` };
2617
+ if (removed.length && !added.length) return { title: `Remove ${andList(removed)} from ${axis}`, summary: `${component} no longer has the ${axis} values ${andList(removed)}.` };
2618
+ return { title: `Change the ${axis} values`, summary: `${axis} on ${component} goes from ${andList(before)} to ${andList(after)}.` };
2619
+ }
2620
+ case "prop-change": {
2621
+ if (!c10) return { title: `Change a ${component} prop`, summary: `A prop of ${component} changed in Figma.` };
2622
+ const name = c10.field.replace(/^Prop /, "");
2623
+ const b = propShape(c10.before);
2624
+ const a = propShape(c10.after);
2625
+ if (!b && a) return { title: `Add the ${name} prop`, summary: `${component} gets a new ${a.type} prop, ${name}${a.def ? `, default ${a.def}` : ""}.` };
2626
+ if (b && !a) return { title: `Remove the ${name} prop`, summary: `${component} no longer has the ${name} prop.` };
2627
+ 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"}.` };
2628
+ return { title: `Change the ${name} prop`, summary: `${name} on ${component} goes from ${c10.before ?? "none"} to ${c10.after ?? "none"}.` };
2629
+ }
2630
+ case "token-binding": {
2631
+ if (!c10) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
2632
+ const m2 = /^(.*?)(?: \((.*)\))?$/.exec(c10.field);
2633
+ const part = lower(m2[1]);
2634
+ const where = m2[2] ? ` in ${m2[2]}` : "";
2635
+ const after = c10.after && c10.after !== "none" ? c10.after : "";
2636
+ const before = c10.before && c10.before !== "none" ? c10.before : "";
2637
+ if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where} no longer uses ${before || "a token"}.` };
2638
+ return {
2639
+ title: `Use ${after} for the ${part}`,
2640
+ summary: `The ${part} of ${component}${where} ${before ? `moves from ${before} to ${after}` : `now uses ${after}`}.`
2641
+ };
2642
+ }
2643
+ case "structure": {
2644
+ const r2 = c10 ? parseStructureChange(c10) : void 0;
2645
+ if (!r2) return { title: "Change the layers to match Figma", summary: `The layers of ${component} changed in Figma.` };
2646
+ if (r2.op === "added") {
2647
+ const at2 = r2.at && r2.at !== "alone" ? ` ${bare(r2.at)}` : "";
2648
+ 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}.` };
2649
+ }
2650
+ if (r2.op === "removed") return { title: `Remove the ${r2.name} layer`, summary: `${component} no longer has the ${r2.name} layer ${r2.where}.` };
2651
+ 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}.` };
2652
+ }
2653
+ case "visual":
2654
+ return {
2655
+ title: "Update the look to match Figma",
2656
+ 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.`
2657
+ };
2658
+ default:
2659
+ return { title: "", summary: "" };
2660
+ }
2661
+ }
2662
+ function draftRequestText(component, kind, changes) {
2663
+ const name = component.trim() || "the component";
2664
+ if (!changes.length && kind !== "new-component") return { title: "", summary: "" };
2665
+ const d2 = firstChange(name, kind, changes[0], changes);
2666
+ const rest = kind === "new-component" ? 0 : changes.length - 1;
2667
+ const summary = rest > 0 ? `${d2.summary} ${capital(plural(rest, "other change"))} ${rest === 1 ? "is" : "are"} listed below.` : d2.summary;
2668
+ return { title: cleanDraftText(d2.title), summary: cleanDraftText(summary) };
2669
+ }
2670
+ const DRAFT_LINE_MAX = 120;
2671
+ function clipText(text, max) {
2672
+ const t3 = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
2673
+ if (t3.length <= max) return t3;
2674
+ const cut = t3.slice(0, max + 1);
2675
+ const at2 = cut.lastIndexOf(" ");
2676
+ return (at2 > max * 0.5 ? cut.slice(0, at2) : t3.slice(0, max)).replace(/[\s,;:(]+$/, "");
2677
+ }
2678
+ function sanitizeDraftLine(raw) {
2679
+ if (typeof raw !== "string") return "";
2680
+ return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
2681
+ }
2682
+ function changePhrase(c10) {
2683
+ let m2;
2684
+ if (m2 = /^Variant (.+)$/.exec(c10.field)) {
2685
+ const axis = m2[1];
2686
+ const before2 = values(c10.before);
2687
+ const after2 = values(c10.after);
2688
+ if (!before2.length) return `New ${axis} variant axis: ${after2.join(", ")}`;
2689
+ if (!after2.length) return `${axis} variant axis removed (${before2.join(", ")})`;
2690
+ const added = after2.filter((x) => !before2.includes(x));
2691
+ const removed = before2.filter((x) => !after2.includes(x));
2692
+ const parts = [added.length ? `${andList(added)} added to ${axis}` : "", removed.length ? `${andList(removed)} removed from ${axis}` : ""].filter(Boolean);
2693
+ return parts.join("; ") || `${axis} values reordered: ${after2.join(", ")}`;
2694
+ }
2695
+ if (m2 = /^Prop (.+)$/.exec(c10.field)) {
2696
+ const name = m2[1];
2697
+ const b = propShape(c10.before);
2698
+ const a = propShape(c10.after);
2699
+ if (!b && a) return `New ${a.type} prop ${name}${a.def ? `, default ${a.def}` : ""}`;
2700
+ if (b && !a) return `${name} prop removed`;
2701
+ if (b && a && b.type === a.type) return `${name} default goes from ${b.def || "empty"} to ${a.def || "empty"}`;
2702
+ return `${name} prop goes from ${c10.before ?? "none"} to ${c10.after ?? "none"}`;
2703
+ }
2704
+ 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"}`;
2705
+ if (m2 = /^Look of (.+)$/.exec(c10.field)) return `Looks different in ${m2[1]}; no variable explains it`;
2706
+ const layer = parseStructureChange(c10);
2707
+ if (layer) return structurePhrase(layer);
2708
+ m2 = /^(.*?)(?: \((.*)\))?$/.exec(c10.field);
2709
+ const where = m2[2] ? ` in ${m2[2]}` : "";
2710
+ const before = c10.before && c10.before !== "none" ? c10.before : "";
2711
+ const after = c10.after && c10.after !== "none" ? c10.after : "";
2712
+ if (!after) return `${m2[1]} no longer uses ${before || "a token"}${where}`;
2713
+ if (!before) return `${m2[1]} now uses ${after}${where}`;
2714
+ return `${m2[1]} uses ${after} instead of ${before}${where}`;
2715
+ }
2716
+ function draftChangeLine(kind, changes) {
2717
+ if (kind === "new-component") {
2718
+ const axes = changes.filter((x) => x.field.startsWith("Variant ")).length;
2719
+ const props = changes.filter((x) => x.field.startsWith("Prop ")).length;
2720
+ const parts = [axes ? plural(axes, "variant axis", "variant axes") : "", props ? plural(props, "prop") : ""].filter(Boolean);
2721
+ return `New component${parts.length ? ` with ${andList(parts)}` : ""}, not in code yet`;
2722
+ }
2723
+ if (!changes.length) return "";
2724
+ const first = changePhrase(changes[0]);
2725
+ const rest = changes.length - 1;
2726
+ const tail = rest ? ` (and ${rest} more)` : "";
2727
+ return capital(clipText(cleanDraftText(first), DRAFT_LINE_MAX - tail.length)) + tail;
2728
+ }
2729
+ const tokenOf = (e2) => e2.figma ?? e2.head ?? e2.base;
2730
+ const pathOf = (e2) => tokenOf(e2)?.path ?? e2.path;
2731
+ function pushChangeRows(entries) {
2732
+ return entries.map((e2) => {
2733
+ const before = e2.head ?? e2.base;
2734
+ const after = e2.figma;
2735
+ const show = (t3) => {
2736
+ if (!t3) return void 0;
2737
+ const modes = Object.keys(t3.values);
2738
+ if (modes.length <= 1) return formatValue(t3.type, t3.values[modes[0] ?? ""]);
2739
+ return modes.map((m2) => `${modeDisplayName(m2)} ${formatValue(t3.type, t3.values[m2])}`).join("; ");
2740
+ };
2741
+ const renamed = e2.local?.kind === "renamed" && before && after && before.path !== after.path;
2742
+ return { field: renamed ? `${before.path} -> ${after.path}` : pathOf(e2), before: show(before), after: show(after) };
2743
+ });
2744
+ }
2745
+ function names(paths) {
2746
+ if (paths.length <= 3) return andList(paths);
2747
+ return `${paths.length} tokens, including ${andList(paths.slice(0, 2))}`;
2748
+ }
2749
+ function valueModes(e2) {
2750
+ return (e2.local?.fields ?? []).filter((f) => f.field === "value" && f.mode && f.mode !== "value").map((f) => modeDisplayName(f.mode));
2751
+ }
2752
+ function draftPushDescription(entries, components = []) {
2753
+ const by2 = { changed: [], added: [], renamed: [], removed: [] };
2754
+ for (const e2 of entries) by2[e2.local?.kind ?? "changed"].push(e2);
2755
+ const clauses = [];
2756
+ if (by2.changed.length) {
2757
+ const only = by2.changed.length === 1 ? by2.changed[0] : void 0;
2758
+ const fields = new Set((only?.local?.fields ?? []).map((f) => f.field));
2759
+ const modes = only ? valueModes(only) : [];
2760
+ if (only && fields.size === 1 && fields.has("description")) clauses.push(`updates the description of ${pathOf(only)}`);
2761
+ else if (only && modes.length) clauses.push(`changes ${pathOf(only)} in ${andList(modes)}`);
2762
+ else clauses.push(`changes ${names(by2.changed.map(pathOf))}`);
2763
+ }
2764
+ if (by2.added.length) clauses.push(`adds ${names(by2.added.map(pathOf))}`);
2765
+ if (by2.renamed.length) {
2766
+ const r2 = by2.renamed[0];
2767
+ const from = (r2.head ?? r2.base)?.path;
2768
+ clauses.push(by2.renamed.length === 1 && from && from !== pathOf(r2) ? `renames ${from} to ${pathOf(r2)}` : `renames ${names(by2.renamed.map(pathOf))}`);
2769
+ }
2770
+ if (by2.removed.length) clauses.push(`removes ${names(by2.removed.map(pathOf))}`);
2771
+ const sentences2 = [];
2772
+ if (clauses.length) sentences2.push(`${capital(andList(clauses))}.`);
2773
+ const fresh = components.filter((c10) => c10.kind === "new-component").map((c10) => c10.component);
2774
+ const changed = [...new Set(components.filter((c10) => c10.kind !== "new-component").map((c10) => c10.component))];
2775
+ const comp = [];
2776
+ if (changed.length) comp.push(`${changed.length === 1 ? "a change" : "changes"} to ${andList(changed)}`);
2777
+ if (fresh.length) comp.push(`the new ${andList(fresh)} component${fresh.length === 1 ? "" : "s"}`);
2778
+ if (comp.length) sentences2.push(`Requests ${andList(comp)}.`);
2779
+ return cleanDraftText(sentences2.join(" "));
2780
+ }
2781
+ const DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
2782
+ const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
2783
+ const DOCS_SELECTION_MODES = {
2784
+ clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
2785
+ shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
2786
+ voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
2787
+ };
2788
+ const isDocsSelectionMode = (m2) => typeof m2 === "string" && m2 in DOCS_SELECTION_MODES;
2789
+ const isProseKey = (k3) => typeof k3 === "string" && PROSE_KEYS.includes(k3);
2790
+ function validateDocsTask(x) {
2791
+ if (!x || typeof x !== "object") return ["The docs task is missing."];
2792
+ const t3 = x;
2793
+ if (t3.kind === "sections") {
2794
+ const keys = t3.keys;
2795
+ if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
2796
+ if (t3.check !== void 0 && (!Array.isArray(t3.check) || !t3.check.every(isProseKey))) return ["check must list prose sections."];
2797
+ return [];
2798
+ }
2799
+ if (t3.kind === "selection") {
2800
+ const out = [];
2801
+ if (!isProseKey(t3.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
2802
+ if (typeof t3.text !== "string" || !t3.text.trim()) out.push("The selection has no text.");
2803
+ if (!isDocsSelectionMode(t3.mode)) out.push("The selection mode must be clearer, shorter or voice.");
2804
+ return out;
2805
+ }
2806
+ return ['The docs task kind must be "sections" or "selection".'];
2807
+ }
2808
+ const docsTaskKeys = (t3) => t3.kind === "sections" ? t3.keys : [t3.key];
2809
+ function docsSelectionPrompt(prefill, conventions, sel) {
2810
+ const base = docsPrompt(prefill, conventions, { keys: [sel.key] });
2811
+ const facts = base.slice(base.indexOf("Facts computed from the code:"));
2812
+ return [
2813
+ `Rewrite one passage of the "${sel.key}" section of ${prefill.component}.docs.json for the Bridge design system.`,
2814
+ DOCS_SELECTION_MODES[sel.mode],
2815
+ "",
2816
+ "Rules:",
2817
+ "- 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.",
2818
+ "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words, no Markdown.",
2819
+ "- Rewrite only the passage: it is pasted back in place of the selection, inside the rest of the section.",
2820
+ '- Answer with one JSON object, nothing else: { "replacement": "the rewritten passage" }.',
2821
+ "",
2822
+ `<passage>
2823
+ ${sel.text.trim()}
2824
+ </passage>`,
2825
+ "",
2826
+ facts
2827
+ ].join("\n");
2828
+ }
2829
+ function docsTaskPrompt(prefill, conventions, task) {
2830
+ if (task.kind === "selection") return docsSelectionPrompt(prefill, conventions, task);
2831
+ return docsPrompt(prefill, conventions, { keys: task.keys, ...task.rewrite ? { rewrite: true } : {}, ...task.check?.length ? { check: task.check } : {} });
2832
+ }
2833
+ function parseDocsReply(text) {
2834
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
2835
+ const body = fenced ? fenced[1] : text;
2836
+ const start = body.indexOf("{");
2837
+ const end = body.lastIndexOf("}");
2838
+ if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
2839
+ const json = JSON.parse(body.slice(start, end + 1));
2840
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
2841
+ return json;
2842
+ }
2843
+ const cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
2844
+ const isExample = (x) => {
2845
+ if (typeof x === "string") return true;
2846
+ if (!x || typeof x !== "object" || Array.isArray(x)) return false;
2847
+ const n2 = x;
2848
+ if (typeof n2.c !== "string" || !n2.c) return false;
2849
+ if (n2.p !== void 0 && (!n2.p || typeof n2.p !== "object" || Array.isArray(n2.p))) return false;
2850
+ return n2.children === void 0 || Array.isArray(n2.children) && n2.children.every(isExample);
2851
+ };
2852
+ const textList = (x) => {
2853
+ if (typeof x === "string") x = [x];
2854
+ if (!Array.isArray(x)) return void 0;
2855
+ const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
2856
+ return out.length ? out : void 0;
2857
+ };
2858
+ const guidanceList = (x, examples) => {
2859
+ if (!Array.isArray(x)) return void 0;
2860
+ const out = [];
2861
+ for (const g3 of x) {
2862
+ if (!g3 || typeof g3 !== "object") continue;
2863
+ const r2 = g3;
2864
+ const title = typeof r2.title === "string" ? cleanDocsText(r2.title) : "";
2865
+ const text = typeof r2.text === "string" ? cleanDocsText(r2.text) : "";
2866
+ if (!title || !text) continue;
2867
+ out.push({ title, text, ...examples && r2.example !== void 0 && isExample(r2.example) ? { example: r2.example } : {} });
2868
+ }
2869
+ return out.length ? out : void 0;
2870
+ };
2871
+ function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
2872
+ const out = {};
2873
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
2874
+ const r2 = raw;
2875
+ for (const k3 of keys) {
2876
+ const v2 = r2[k3];
2877
+ if (k3 === "summary") {
2878
+ const s = typeof v2 === "string" ? cleanDocsText(v2) : "";
2879
+ if (s) out.summary = s;
2880
+ } else if (k3 === "dos" || k3 === "donts") {
2881
+ const g3 = guidanceList(v2, opts.examples ?? false);
2882
+ if (g3) out[k3] = g3;
2883
+ } else {
2884
+ const l2 = textList(v2);
2885
+ if (l2) out[k3] = l2;
2886
+ }
2887
+ }
2888
+ return out;
2889
+ }
2890
+ function sanitizeDocsReplacement(raw) {
2891
+ if (typeof raw !== "string") return "";
2892
+ return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
2893
+ }
2894
+ function parseDocsTaskAnswer(task, text) {
2895
+ if (task.kind === "selection") {
2896
+ let raw = text;
2897
+ try {
2898
+ const json = parseDocsReply(text);
2899
+ raw = json.replacement ?? json.text ?? json[task.key];
2900
+ } catch {
2901
+ raw = text.replace(/```[a-z]*\s*|```/gi, "");
2902
+ }
2903
+ const replacement = sanitizeDocsReplacement(raw);
2904
+ if (!replacement) throw new Error("Claude sent no rewritten passage. Try again.");
2905
+ return { sections: {}, replacement };
2906
+ }
2907
+ return docsSectionsAnswer(task, parseDocsReply(text));
2908
+ }
2909
+ function docsSectionsAnswer(task, raw) {
2910
+ const check = (task.check ?? []).filter((k3) => task.keys.includes(k3));
2911
+ const sections = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
2912
+ return { sections, checked: check.filter((k3) => sections[k3] === void 0) };
2913
+ }
2914
+ function docsSectionsResult(task, raw) {
2915
+ const sections = sanitizeDocsSections(raw, task.keys);
2916
+ if (!Object.keys(sections).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
2917
+ return sections;
2918
+ }
2919
+ function applyDocsSections(docs, sections) {
2920
+ const keys = PROSE_KEYS.filter((k3) => sections[k3] !== void 0);
2921
+ if (!keys.length) return docs;
2922
+ return markClaudeDraft({ ...docs, ...sections }, keys);
2923
+ }
2924
+ function mergeDocsDraft(prefillDraft, reply) {
2925
+ const empty = PROSE_KEYS.filter((k3) => {
2926
+ const v2 = prefillDraft[k3];
2927
+ return v2 === void 0 || (typeof v2 === "string" ? !v2.trim() : Array.isArray(v2) && !v2.length);
2928
+ });
2929
+ const sections = sanitizeDocsSections(reply, empty, { examples: true });
2930
+ const merged = { ...prefillDraft, ...sections };
2931
+ if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
2932
+ merged.anatomy = prefillDraft.anatomy;
2933
+ const drafted = PROSE_KEYS.filter((k3) => sections[k3] !== void 0);
2934
+ return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
2935
+ }
2936
+ function anthropicRequest(input) {
2937
+ const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
2938
+ if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
2939
+ if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
2940
+ const body = {
2941
+ model: input.model || DOCS_DRAFT_MODEL,
2942
+ max_tokens: input.maxTokens ?? 16e3,
2943
+ ...input.fallback ? { fallbacks: "default" } : {},
2944
+ ...input.system ? { system: input.system } : {},
2945
+ messages: [{ role: "user", content: input.prompt }]
2946
+ };
2947
+ return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
2948
+ }
2949
+ function anthropicReplyText(msg) {
2950
+ const m2 = msg ?? {};
2951
+ if (m2.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
2952
+ if (m2.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
2953
+ return (m2.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
2954
+ }
1870
2955
  const EXPLORATION_LABEL = "exploration";
1871
2956
  const EXPLORATION_PREFIX = "explore/";
1872
2957
  const EXPLORATION_NOTES_DIR = ".bridge/explorations";
@@ -1898,58 +2983,48 @@ function explorationSlugFromBranch(ref) {
1898
2983
  return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
1899
2984
  }
1900
2985
  const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
1901
- function explorationProblems(e3) {
2986
+ function explorationProblems(e2) {
1902
2987
  const out = [];
1903
- const name = e3.name.trim();
1904
- const slug = e3.slug?.trim() || explorationSlug(name);
2988
+ const name = e2.name.trim();
2989
+ const slug = e2.slug?.trim() || explorationSlug(name);
1905
2990
  if (!name) out.push('Name the exploration, for example "Denser data tables".');
1906
2991
  else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the branch name.");
1907
2992
  else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the branch explore/<name>.");
1908
- if (!e3.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
2993
+ if (!e2.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
1909
2994
  return out;
1910
2995
  }
1911
2996
  const useCaseLabel = (u2) => EXPLORATION_USE_CASES.find((x) => x.value === u2)?.label;
1912
- function explorationNote(e3) {
1913
- const slug = e3.slug || explorationSlug(e3.name);
1914
- const uc = useCaseLabel(e3.useCase);
2997
+ function explorationNote(e2) {
2998
+ const slug = e2.slug || explorationSlug(e2.name);
2999
+ const uc = useCaseLabel(e2.useCase);
1915
3000
  return [
1916
- `# Exploration: ${e3.name.trim()}`,
3001
+ `# Exploration: ${e2.name.trim()}`,
1917
3002
  "",
1918
- e3.purpose.trim(),
3003
+ e2.purpose.trim(),
1919
3004
  "",
1920
3005
  `- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
1921
3006
  ...uc ? [`- Use case: ${uc}`] : [],
1922
- ...e3.startedAt ? [`- Started: ${e3.startedAt.slice(0, 10)}${e3.author ? ` by @${e3.author}` : ""}`] : [],
3007
+ ...e2.startedAt ? [`- Started: ${e2.startedAt.slice(0, 10)}${e2.author ? ` by @${e2.author}` : ""}`] : [],
1923
3008
  "",
1924
3009
  "This file marks the branch as an exploration. It is never merged as is: promoting the pull request to review removes it.",
1925
3010
  ""
1926
3011
  ].join("\n");
1927
3012
  }
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
- function explorationPrTitle(e3) {
1939
- const n2 = e3.name.trim().replace(/\.$/, "");
3013
+ function explorationPrTitle(e2) {
3014
+ const n2 = e2.name.trim().replace(/\.$/, "");
1940
3015
  return `Exploration: ${n2.charAt(0).toLowerCase()}${n2.slice(1)}`;
1941
3016
  }
1942
- function explorationPrBody(e3, ctx) {
1943
- const slug = e3.slug || explorationSlug(e3.name);
3017
+ function explorationPrBody(e2, ctx) {
3018
+ const slug = e2.slug || explorationSlug(e2.name);
1944
3019
  const branch = `${EXPLORATION_PREFIX}${slug}`;
1945
- const uc = useCaseLabel(e3.useCase);
1946
- const experimental = (e3.experimental ?? []).map((x) => x.trim()).filter(Boolean);
3020
+ const uc = useCaseLabel(e2.useCase);
3021
+ const experimental = (e2.experimental ?? []).map((x) => x.trim()).filter(Boolean);
1947
3022
  const lines = [
1948
- `## Exploration: ${e3.name.trim()}`,
3023
+ `## Exploration: ${e2.name.trim()}`,
1949
3024
  "",
1950
3025
  `> Not mergeable as is. This draft pull request is a sandbox on \`${branch}\`: promote it to a normal review when the idea is validated, or close it.`,
1951
3026
  "",
1952
- `**Purpose.** ${e3.purpose.trim()}`,
3027
+ `**Purpose.** ${e2.purpose.trim()}`,
1953
3028
  ...uc ? ["", `**Use case.** ${uc}`] : [],
1954
3029
  "",
1955
3030
  "### What is experimental",
@@ -1960,20 +3035,9 @@ function explorationPrBody(e3, ctx) {
1960
3035
  "### Try it in a product staging branch",
1961
3036
  ""
1962
3037
  ];
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
- }
3038
+ lines.push(
3039
+ '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.'
3040
+ );
1977
3041
  lines.push("", "### Previews", "");
1978
3042
  const links = previewLinks(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
1979
3043
  if (links.length) lines.push("| App | Preview |", "| --- | --- |", ...links.map((l2) => `| ${l2.label} | ${l2.url} |`));
@@ -1985,7 +3049,7 @@ function explorationPrBody(e3, ctx) {
1985
3049
  `- **Promote:** in Bridge Studio (Changes, Explorations, Promote to review) or \`bridge explore promote ${slug}\`. Bridge marks the pull request ready for review, removes the \`${EXPLORATION_LABEL}\` label and the exploration note, and adds the tier labels. Then it goes through the usual review.`,
1986
3050
  "- **Close:** close this pull request. The branch stays for reference; the preview prerelease is removed.",
1987
3051
  "",
1988
- `<!-- ${MARK$1} ${JSON.stringify({ name: e3.name.trim(), purpose: e3.purpose.trim(), slug, ...e3.useCase ? { useCase: e3.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e3.author ? { author: e3.author } : {}, ...e3.startedAt ? { startedAt: e3.startedAt } : {} }).replace(/-->/g, "--\\u003e")} -->`
3052
+ `<!-- ${MARK$1} ${JSON.stringify({ name: e2.name.trim(), purpose: e2.purpose.trim(), slug, ...e2.useCase ? { useCase: e2.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e2.author ? { author: e2.author } : {}, ...e2.startedAt ? { startedAt: e2.startedAt } : {}, ...e2.source ? { source: e2.source } : {}, ...e2.agent ? { agent: e2.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
1989
3053
  );
1990
3054
  return lines.join("\n");
1991
3055
  }
@@ -1994,20 +3058,20 @@ function parseExplorationBody(body) {
1994
3058
  const m2 = new RegExp(`<!-- ${MARK$1} (\\{[\\s\\S]*?\\}) -->`).exec(body);
1995
3059
  if (!m2) return void 0;
1996
3060
  try {
1997
- const e3 = JSON.parse(m2[1]);
1998
- return e3 && typeof e3.name === "string" && typeof e3.purpose === "string" ? e3 : void 0;
3061
+ const e2 = JSON.parse(m2[1]);
3062
+ return e2 && typeof e2.name === "string" && typeof e2.purpose === "string" ? e2 : void 0;
1999
3063
  } catch {
2000
3064
  return void 0;
2001
3065
  }
2002
3066
  }
2003
3067
  const labelNames = (labels) => labels.map((l2) => typeof l2 === "string" ? l2 : l2.name ?? "");
2004
- function isExplorationPull(p2) {
2005
- return isExplorationBranch(p2.headRef) || labelNames(p2.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p2.body));
3068
+ function isExplorationPull(p) {
3069
+ return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
2006
3070
  }
2007
- function explorationStatus(p2) {
2008
- const merged = Boolean(p2.merged || p2.merged_at || p2.state === "merged");
2009
- if (p2.state !== "open" && !merged) return "closed";
2010
- if (p2.state === "open" && labelNames(p2.labels).includes(EXPLORATION_LABEL)) return "active";
3071
+ function explorationStatus(p) {
3072
+ const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
3073
+ if (p.state !== "open" && !merged) return "closed";
3074
+ if (p.state === "open" && labelNames(p.labels).includes(EXPLORATION_LABEL)) return "active";
2011
3075
  return "promoted";
2012
3076
  }
2013
3077
  const isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
@@ -2043,6 +3107,19 @@ function promotionPlan(input) {
2043
3107
  markReady: true
2044
3108
  };
2045
3109
  }
3110
+ function explorationTryIt(input) {
3111
+ const c10 = readPreviewComment(input.comment);
3112
+ if (!c10) return { state: "building" };
3113
+ if (input.headSha && c10.sha7 && !input.headSha.startsWith(c10.sha7)) return { state: "building" };
3114
+ if (!c10.command || c10.source === "none") return { state: "failed" };
3115
+ return { state: "ready", command: c10.command, source: c10.source, sha7: c10.sha7 };
3116
+ }
3117
+ function explorationShareMessage(input) {
3118
+ const name = input.name.trim().replace(/"/g, "'");
3119
+ 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.`;
3120
+ const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
3121
+ return [head, previews.length ? `Previews: ${previews.join(", ")}.` : "", `Pull request: ${input.url}.`].filter(Boolean).join(" ");
3122
+ }
2046
3123
  function explorationRules(branch) {
2047
3124
  return [
2048
3125
  `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
@@ -2053,7 +3130,8 @@ function explorationRules(branch) {
2053
3130
  const REQUEST_KINDS = [
2054
3131
  { value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
2055
3132
  { 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." },
3133
+ { value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
3134
+ { value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
2057
3135
  { value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
2058
3136
  { value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
2059
3137
  { value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
@@ -2068,13 +3146,14 @@ function requestIssueTitle(r2) {
2068
3146
  return `${r2.component}: ${t3.charAt(0).toLowerCase()}${t3.slice(1)}`;
2069
3147
  }
2070
3148
  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 ?? "")} |`)] : [];
3149
+ const rows = r2.changes.length ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r2.changes.map((c10) => `| ${cell(c10.field)} | ${cell(c10.before ?? "")} | ${cell(c10.after ?? "")} |`)] : [];
2072
3150
  const figma = r2.figma?.url ? `[${r2.figma.nodeName ?? "Open in Figma"}](${r2.figma.url})` : r2.figma?.nodeName ? r2.figma.nodeName : "";
2073
3151
  const body = [
2074
3152
  r2.kind === "new-component" ? `## New component request: ${r2.component}` : `## ${kindLabel(r2.kind)} request for ${r2.component}`,
2075
3153
  "",
2076
3154
  r2.summary.trim(),
2077
3155
  "",
3156
+ ...r2.line?.trim() ? [`**What changes:** ${r2.line.trim()}`, ""] : [],
2078
3157
  ...rows.length ? [...rows, ""] : [],
2079
3158
  ...r2.images?.length ? [...r2.images.map((u2, i2) => `![${r2.component} design ${i2 + 1}](${u2})`), ""] : [],
2080
3159
  ...figma ? [`**Design:** ${figma}`, ""] : [],
@@ -2102,10 +3181,36 @@ function parseRequestIssue(body) {
2102
3181
  return void 0;
2103
3182
  }
2104
3183
  }
3184
+ function withRequestExploration(body, branch) {
3185
+ const r2 = parseRequestIssue(body);
3186
+ if (!r2 || !body) return void 0;
3187
+ const block = new RegExp(`<!-- ${MARK} \\{[\\s\\S]*?\\} -->`);
3188
+ let out = body.replace(block, () => `<!-- ${MARK} ${JSON.stringify({ ...r2, exploration: branch })} -->`);
3189
+ 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.`;
3190
+ if (/^\*\*Exploration:\*\* .*$/m.test(out)) out = out.replace(/^\*\*Exploration:\*\* .*$/m, () => line);
3191
+ else if (out.includes("\n## How to implement")) out = out.replace("\n## How to implement", () => `
3192
+ ${line}
3193
+
3194
+ ## How to implement`);
3195
+ else out = out.replace(block, (m2) => `${line}
3196
+
3197
+ ${m2}`);
3198
+ return out;
3199
+ }
3200
+ function declineReasonProblems(reason) {
3201
+ const r2 = reason.trim();
3202
+ if (!r2) return ["Say why, in one or two sentences: the requester reads it."];
3203
+ if (r2.length < 10) return ["Say a bit more, so the requester knows why: at least 10 characters."];
3204
+ return [];
3205
+ }
3206
+ function declineComment(reason, by2) {
3207
+ const quote = reason.trim().split("\n").map((l2) => `> ${l2}`.trimEnd()).join("\n");
3208
+ return [`Declined${by2 ? ` by @${by2.replace(/^@/, "")}` : ""} in Bridge Studio.`, "", quote, "", "<sub>Closed as not planned. Reopen the issue if things change.</sub>"].join("\n");
3209
+ }
2105
3210
  function requestStatus(issue) {
2106
3211
  const prs = issue.linkedPrs ?? [];
2107
- if (issue.state === "open") return prs.some((p2) => p2.state === "open") ? "in-progress" : "requested";
2108
- if (prs.some((p2) => p2.merged) || issue.stateReason === "completed") return "done";
3212
+ if (issue.state === "open") return prs.some((p) => p.state === "open") ? "in-progress" : "requested";
3213
+ if (prs.some((p) => p.merged) || issue.stateReason === "completed") return "done";
2109
3214
  return "declined";
2110
3215
  }
2111
3216
  const REQUEST_STATUS_LABEL = { requested: "Requested", "in-progress": "In progress", done: "Done", declined: "Declined" };
@@ -2127,10 +3232,11 @@ function requestBrief(r2, ctx) {
2127
3232
  `Component: ${r2.component}`,
2128
3233
  `Kind: ${kindLabel(r2.kind)}`,
2129
3234
  `Request: ${r2.title}`,
3235
+ ...r2.line?.trim() ? [`What changes: ${r2.line.trim()}`] : [],
2130
3236
  "",
2131
3237
  r2.summary.trim(),
2132
3238
  "",
2133
- ...r2.changes.length ? ["Changes asked:", ...r2.changes.map((c2) => `- ${c2.field}: ${c2.before ?? "(none)"} -> ${c2.after ?? "(removed)"}`), ""] : [],
3239
+ ...r2.changes.length ? ["Changes asked:", ...r2.changes.map((c10) => `- ${c10.field}: ${c10.before ?? "(none)"} -> ${c10.after ?? "(removed)"}`), ""] : [],
2134
3240
  ...r2.figma?.url ? [`Design: ${r2.figma.url}`, ""] : [],
2135
3241
  ...r2.images?.length ? ["Images:", ...r2.images.map((u2) => `- ${u2}`), ""] : [],
2136
3242
  ...ctx.files?.length ? ["Files of this component:", ...ctx.files.map((f) => `- ${f}`), ""] : [],
@@ -2152,7 +3258,7 @@ function validateIntegrations(i2) {
2152
3258
  }
2153
3259
  function jiraIssueFields(r2, jira, issueUrl) {
2154
3260
  const text = (t3) => ({ type: "text", text: t3 });
2155
- const para = (...c2) => ({ type: "paragraph", content: c2 });
3261
+ const para = (...c10) => ({ type: "paragraph", content: c10 });
2156
3262
  return {
2157
3263
  project: { key: jira.project },
2158
3264
  issuetype: { name: jira.issueType ?? "Task" },
@@ -2163,145 +3269,13 @@ function jiraIssueFields(r2, jira, issueUrl) {
2163
3269
  version: 1,
2164
3270
  content: [
2165
3271
  para(text(r2.summary.trim() || r2.title)),
2166
- ...r2.changes.map((c2) => para(text(`${c2.field}: ${c2.before ?? "(none)"} -> ${c2.after ?? "(removed)"}`))),
3272
+ ...r2.line?.trim() ? [para(text(r2.line.trim()))] : [],
3273
+ ...r2.changes.map((c10) => para(text(`${c10.field}: ${c10.before ?? "(none)"} -> ${c10.after ?? "(removed)"}`))),
2167
3274
  para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
2168
3275
  ]
2169
3276
  }
2170
3277
  };
2171
3278
  }
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
3279
  const DRAFT_EXCHANGE_VERSION = 1;
2306
3280
  const DRAFTS_DIR = ".bridge/drafts";
2307
3281
  const DRAFT_PHRASE = "Draft my Bridge changes";
@@ -2314,12 +3288,16 @@ const DRAFT_VOICE_RULES = [
2314
3288
  "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
3289
  "Keep the designer's intent from their notes.",
2316
3290
  "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."
3291
+ "A request title never repeats the component name: the issue title already starts with it.",
3292
+ `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.`,
3293
+ `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
3294
  ];
2319
3295
  const DRAFT_OUTPUT_CONTRACT = [
2320
- `Call bridge_write_drafts once per session with {"sessionId": "...", "results": [{"id": "<item id>", "title": "...", "summary": "..."}]}, one result per item.`,
3296
+ `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
3297
  `title: one line, at most ${DRAFT_TITLE_MAX} characters, no trailing period.`,
2322
3298
  `summary: at most ${DRAFT_SUMMARY_SENTENCES} sentences, plain text.`,
3299
+ `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.`,
3300
+ `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
3301
  "The designer reviews the text in Figma before anything is sent."
2324
3302
  ];
2325
3303
  function newDraftSessionId(random = Math.random) {
@@ -2339,17 +3317,17 @@ function draftImagePath(sessionId, itemId, label) {
2339
3317
  return `${DRAFTS_DIR}/${sessionId}/${item}-${label}.png`;
2340
3318
  }
2341
3319
  function composePendingDrafts(input) {
2342
- const p2 = {
3320
+ const p = {
2343
3321
  version: DRAFT_EXCHANGE_VERSION,
2344
3322
  sessionId: input.sessionId ?? newDraftSessionId(input.random),
2345
3323
  createdAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
2346
3324
  status: "pending",
2347
- source: "figma",
3325
+ source: input.source ?? "figma",
2348
3326
  items: input.items.map((i2) => ({ ...i2, changes: i2.changes ?? [] }))
2349
3327
  };
2350
- const problems = validatePendingDrafts(p2);
3328
+ const problems = validatePendingDrafts(p);
2351
3329
  if (problems.length) throw new Error(problems.join(" "));
2352
- return p2;
3330
+ return p;
2353
3331
  }
2354
3332
  const KINDS = new Set(REQUEST_KINDS.map((k3) => k3.value));
2355
3333
  const str = (x) => typeof x === "string";
@@ -2357,15 +3335,15 @@ const optStr = (x) => x === void 0 || typeof x === "string";
2357
3335
  function validatePendingDrafts(x) {
2358
3336
  const out = [];
2359
3337
  if (!x || typeof x !== "object" || Array.isArray(x)) return ["A draft session is a JSON object."];
2360
- const p2 = x;
2361
- if (p2.version !== DRAFT_EXCHANGE_VERSION) out.push(`Unsupported draft session version ${String(p2.version)}: update Bridge.`);
2362
- if (!isDraftSessionId(p2.sessionId)) out.push("The session id is missing or invalid.");
2363
- if (!str(p2.createdAt) || Number.isNaN(Date.parse(p2.createdAt))) out.push("createdAt must be an ISO date.");
2364
- 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".');
2366
- if (!Array.isArray(p2.items) || !p2.items.length) out.push("A draft session needs at least one item.");
3338
+ const p = x;
3339
+ if (p.version !== DRAFT_EXCHANGE_VERSION) out.push(`Unsupported draft session version ${String(p.version)}: update Bridge.`);
3340
+ if (!isDraftSessionId(p.sessionId)) out.push("The session id is missing or invalid.");
3341
+ if (!str(p.createdAt) || Number.isNaN(Date.parse(p.createdAt))) out.push("createdAt must be an ISO date.");
3342
+ if (p.status !== "pending" && p.status !== "drafted") out.push('status must be "pending" or "drafted".');
3343
+ if (p.source !== "figma" && p.source !== "studio") out.push('source must be "figma" or "studio".');
3344
+ if (!Array.isArray(p.items) || !p.items.length) out.push("A draft session needs at least one item.");
2367
3345
  const ids = /* @__PURE__ */ new Set();
2368
- for (const [n2, i2] of (Array.isArray(p2.items) ? p2.items : []).entries()) {
3346
+ for (const [n2, i2] of (Array.isArray(p.items) ? p.items : []).entries()) {
2369
3347
  const at2 = `Item ${n2 + 1}`;
2370
3348
  if (!i2 || typeof i2 !== "object") {
2371
3349
  out.push(`${at2} is not an object.`);
@@ -2374,18 +3352,30 @@ function validatePendingDrafts(x) {
2374
3352
  if (!str(i2.id) || !i2.id) out.push(`${at2} has no id.`);
2375
3353
  else if (ids.has(i2.id)) out.push(`${at2} repeats the id "${i2.id}".`);
2376
3354
  else ids.add(i2.id);
2377
- if (i2.scope !== "request" && i2.scope !== "push") out.push(`${at2}: scope must be "request" or "push".`);
3355
+ if (i2.scope !== "request" && i2.scope !== "push" && i2.scope !== "docs") out.push(`${at2}: scope must be "request", "push" or "docs".`);
3356
+ if (i2.scope === "docs") {
3357
+ const d2 = i2.docs;
3358
+ if (!d2 || typeof d2 !== "object" || !str(d2.slug) || !d2.slug || !str(d2.prompt) || !d2.prompt || !optStr(d2.path)) out.push(`${at2}: a docs item needs docs {slug, task, prompt}.`);
3359
+ else out.push(...validateDocsTask(d2.task).map((m2) => `${at2}: ${m2}`));
3360
+ } else if (i2.docs !== void 0) out.push(`${at2}: only a docs item has docs.`);
2378
3361
  if (!optStr(i2.component)) out.push(`${at2}: component must be text.`);
2379
3362
  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}.`);
3363
+ 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
3364
  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
3365
  if (i2.variables !== void 0 && i2.scope !== "push") out.push(`${at2}: only a push item has variables.`);
2383
3366
  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
3367
  if (!i2.draft || !str(i2.draft.title) || !str(i2.draft.summary)) out.push(`${at2}: draft must hold a title and a summary.`);
3368
+ 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
3369
  }
2386
- if (p2.results !== void 0) {
2387
- 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.");
3370
+ if (p.results !== void 0) {
3371
+ if (!Array.isArray(p.results)) out.push("results must be a list.");
3372
+ else
3373
+ for (const r2 of p.results) {
3374
+ if (!r2 || !str(r2.id) || !str(r2.title) || !str(r2.summary)) out.push("Each result needs an id, a title and a summary.");
3375
+ 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.");
3376
+ else if (!optStr(r2.replacement)) out.push("A result replacement must be text.");
3377
+ 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}.");
3378
+ }
2389
3379
  }
2390
3380
  return out;
2391
3381
  }
@@ -2399,8 +3389,8 @@ function parsePendingDrafts(text) {
2399
3389
  }
2400
3390
  return validatePendingDrafts(json).length ? void 0 : json;
2401
3391
  }
2402
- function serializePendingDrafts(p2) {
2403
- return `${JSON.stringify(p2, null, 2)}
3392
+ function serializePendingDrafts(p) {
3393
+ return `${JSON.stringify(p, null, 2)}
2404
3394
  `;
2405
3395
  }
2406
3396
  function clip(text, max) {
@@ -2422,39 +3412,96 @@ function sanitizeDraftText(raw) {
2422
3412
  const summary = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
2423
3413
  return { title, summary };
2424
3414
  }
2425
- function mergeDraftResults(p2, results, at2 = /* @__PURE__ */ new Date()) {
2426
- const ids = new Set(p2.items.map((i2) => i2.id));
2427
- const next = new Map((p2.results ?? []).map((r2) => [r2.id, r2]));
3415
+ function sanitizeDocsResult(item, r2) {
3416
+ const task = item.docs.task;
3417
+ if (task.kind === "selection") {
3418
+ const raw = r2.replacement ?? (r2.docs && typeof r2.docs === "object" ? r2.docs[task.key] : void 0);
3419
+ const replacement = sanitizeDocsReplacement(raw);
3420
+ if (!replacement) throw new Error(`Item "${item.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item.id}", "replacement": "..."}.`);
3421
+ return { replacement };
3422
+ }
3423
+ try {
3424
+ const { sections, checked } = docsSectionsAnswer(task, r2.docs ?? {});
3425
+ return { docs: sections, checked };
3426
+ } catch {
3427
+ 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.`);
3428
+ }
3429
+ }
3430
+ function sanitizeResultLines(item, raw) {
3431
+ if (raw === void 0 || raw === null) return [];
3432
+ if (!Array.isArray(raw)) throw new Error(`lines for "${item.id}" must be a list of {"id", "line", "title"}.`);
3433
+ const known = new Set((item.lines ?? []).map((l2) => l2.id));
3434
+ const out = /* @__PURE__ */ new Map();
3435
+ for (const l2 of raw) {
3436
+ const id = typeof l2?.id === "string" ? l2.id : "";
3437
+ 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."}`);
3438
+ const line = sanitizeDraftLine(l2.line);
3439
+ const title = sanitizeDraftTitle(l2.title);
3440
+ if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
3441
+ }
3442
+ return [...out.values()];
3443
+ }
3444
+ function sanitizeDraftTitle(raw) {
3445
+ if (typeof raw !== "string") return "";
3446
+ const t3 = clip(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_TITLE_MAX);
3447
+ return t3.charAt(0).toUpperCase() + t3.slice(1);
3448
+ }
3449
+ function mergeDraftResults(p, results, at2 = /* @__PURE__ */ new Date()) {
3450
+ const ids = new Set(p.items.map((i2) => i2.id));
3451
+ const next = new Map((p.results ?? []).map((r2) => [r2.id, r2]));
2428
3452
  for (const r2 of results) {
2429
- 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(", ")}.`);
3453
+ if (!ids.has(r2.id)) throw new Error(`Session ${p.sessionId} has no item "${r2.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
3454
+ const item = p.items.find((i2) => i2.id === r2.id);
3455
+ if (item.scope === "docs") {
3456
+ next.set(r2.id, { id: r2.id, title: "", summary: "", ...sanitizeDocsResult(item, r2), by: "claude", at: at2.toISOString() });
3457
+ continue;
3458
+ }
2430
3459
  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() });
3460
+ const lines = sanitizeResultLines(item, r2.lines);
3461
+ if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r2.id}" has no title and no summary, and no line.`);
3462
+ next.set(r2.id, { id: r2.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at2.toISOString() });
2433
3463
  }
2434
- const merged = p2.items.map((i2) => next.get(i2.id)).filter((r2) => !!r2);
2435
- return { ...p2, results: merged, status: merged.length === p2.items.length ? "drafted" : "pending" };
3464
+ const merged = p.items.map((i2) => next.get(i2.id)).filter((r2) => !!r2);
3465
+ return { ...p, results: merged, status: merged.length === p.items.length ? "drafted" : "pending" };
2436
3466
  }
2437
3467
  function draftItemBrief(i2) {
3468
+ if (i2.scope === "docs" && i2.docs) {
3469
+ const t3 = i2.docs.task;
3470
+ const check = t3.kind === "sections" ? (t3.check ?? []).filter((k3) => t3.keys.includes(k3)) : [];
3471
+ const write = t3.kind === "sections" ? t3.keys.filter((k3) => !check.includes(k3)) : [];
3472
+ const what = t3.kind === "selection" ? `rewrite one passage of the ${t3.key} section (${t3.mode}), answer with "replacement"` : [
3473
+ write.length ? `${t3.rewrite ? "rewrite" : "write"} the sections ${write.join(", ")}` : "",
3474
+ 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)` : ""
3475
+ ].filter(Boolean).join("; ") + ', answer with "docs"';
3476
+ 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");
3477
+ }
2438
3478
  const kind = i2.kind ? REQUEST_KINDS.find((k3) => k3.value === i2.kind)?.label : void 0;
2439
3479
  const cell2 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
2440
3480
  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
3481
  if (i2.component) lines.push(`Component: ${i2.component}`);
2442
3482
  if (kind) lines.push(`Kind: ${kind}`);
2443
3483
  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"} |`));
3484
+ 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
3485
  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.");
3486
+ if (!i2.changes.length && !i2.variables?.length && !i2.lines?.length) lines.push("", "No change detected: rely on the notes and the images.");
2447
3487
  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
3488
  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(", ")}`);
3489
+ if (i2.lines?.length) {
3490
+ 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):`);
3491
+ for (const l2 of i2.lines) {
3492
+ 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}` : ""}`);
3493
+ for (const c10 of l2.changes) lines.push(` - ${c10.field}: ${c10.before || "none"} -> ${c10.after || "removed"}`);
3494
+ }
3495
+ }
3496
+ if (i2.images?.length) lines.push("", `Images: ${i2.images.map((m2) => `${m2.label}${m2.line ? ` (line ${m2.line})` : ""} ${m2.url}`).join(", ")}`);
2450
3497
  return lines.join("\n");
2451
3498
  }
2452
3499
  const isConflict = (err) => err instanceof GitHubError ? err.status === 409 || err.status === 422 && /sha/i.test(`${err.detail ?? ""} ${err.message}`) : err?.status === 409;
2453
- async function createPendingDrafts(client, p2, branch = REQUESTS_BRANCH) {
2454
- const problems = validatePendingDrafts(p2);
3500
+ async function createPendingDrafts(client, p, branch = REQUESTS_BRANCH) {
3501
+ const problems = validatePendingDrafts(p);
2455
3502
  if (problems.length) throw new Error(problems.join(" "));
2456
- await client.putFile({ path: draftSessionPath(p2.sessionId), content: serializePendingDrafts(p2), branch, message: `Bridge drafts: ${p2.items.length === 1 ? "1 item" : `${p2.items.length} items`} waiting for Claude` });
2457
- return p2;
3503
+ await client.putFile({ path: draftSessionPath(p.sessionId), content: serializePendingDrafts(p), branch, message: `Bridge drafts: ${p.items.length === 1 ? "1 item" : `${p.items.length} items`} waiting for Claude` });
3504
+ return p;
2458
3505
  }
2459
3506
  async function readPendingDrafts(client, sessionId, branch = REQUESTS_BRANCH) {
2460
3507
  const f = await client.getFile(draftSessionPath(sessionId), branch);
@@ -2463,16 +3510,16 @@ async function readPendingDrafts(client, sessionId, branch = REQUESTS_BRANCH) {
2463
3510
  }
2464
3511
  async function listPendingDrafts(client, opts = {}) {
2465
3512
  const branch = opts.branch ?? REQUESTS_BRANCH;
2466
- const entries = (await client.listDirectory(DRAFTS_DIR, branch)).filter((e3) => e3.type === "file" && /\.json$/.test(e3.name));
3513
+ const entries = (await client.listDirectory(DRAFTS_DIR, branch)).filter((e2) => e2.type === "file" && /\.json$/.test(e2.name));
2467
3514
  const out = [];
2468
- for (const e3 of entries) {
2469
- const id = e3.name.replace(/\.json$/, "");
3515
+ for (const e2 of entries) {
3516
+ const id = e2.name.replace(/\.json$/, "");
2470
3517
  if (!isDraftSessionId(id)) continue;
2471
3518
  const r2 = await readPendingDrafts(client, id, branch).catch(() => void 0);
2472
3519
  if (r2 && r2.drafts.sessionId === id) out.push(r2.drafts);
2473
3520
  }
2474
3521
  const status = opts.status ?? "all";
2475
- return out.filter((p2) => status === "all" || p2.status === status).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
3522
+ return out.filter((p) => status === "all" || p.status === status).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2476
3523
  }
2477
3524
  async function updatePendingDrafts(client, sessionId, change, opts = {}) {
2478
3525
  const branch = opts.branch ?? REQUESTS_BRANCH;
@@ -2490,7 +3537,7 @@ async function updatePendingDrafts(client, sessionId, change, opts = {}) {
2490
3537
  }
2491
3538
  }
2492
3539
  function writeDraftResults(client, sessionId, results, opts = {}) {
2493
- return updatePendingDrafts(client, sessionId, (p2) => mergeDraftResults(p2, results, opts.now), {
3540
+ return updatePendingDrafts(client, sessionId, (p) => mergeDraftResults(p, results, opts.now), {
2494
3541
  ...opts,
2495
3542
  message: `Bridge drafts: ${results.length === 1 ? "1 text" : `${results.length} texts`} drafted by Claude`
2496
3543
  });
@@ -2502,43 +3549,134 @@ async function deletePendingDrafts(client, sessionId, opts = {}) {
2502
3549
  if (!f) return false;
2503
3550
  await client.deleteFile({ path, branch, message: "Bridge drafts: done", sha: f.sha });
2504
3551
  const images = await client.listDirectory(`${DRAFTS_DIR}/${sessionId}`, branch).catch(() => []);
2505
- for (const img of images.filter((e3) => e3.type === "file")) {
3552
+ for (const img of images.filter((e2) => e2.type === "file")) {
2506
3553
  await client.deleteFile({ path: img.path, branch, message: "Bridge drafts: done", sha: img.sha }).catch(() => void 0);
2507
3554
  }
2508
3555
  return true;
2509
3556
  }
2510
- function explorationSummary(p2, owner, repo, s) {
2511
- const e3 = parseExplorationBody(p2.body);
2512
- const slug = p2.headRef?.startsWith(EXPLORATION_PREFIX) ? p2.headRef.slice(EXPLORATION_PREFIX.length) : e3?.slug ?? explorationSlug(p2.title);
3557
+ async function ensureRequestsBranch(client, baseBranch) {
3558
+ if (await client.getBranchSha(REQUESTS_BRANCH)) return;
3559
+ const head = await client.getBranchSha(baseBranch);
3560
+ if (!head) throw new Error(`Branch "${baseBranch}" was not found, so the ${REQUESTS_BRANCH} branch cannot start from it.`);
3561
+ await client.createBranch(REQUESTS_BRANCH, head);
3562
+ }
3563
+ async function createDocsDraftSession(client, input) {
3564
+ await ensureRequestsBranch(client, input.baseBranch);
3565
+ const what = input.task.kind === "selection" ? `Rewrite a passage of ${input.task.key}` : `${input.task.rewrite ? "Rewrite" : "Write"} ${input.task.keys.join(", ")}`;
3566
+ const item = {
3567
+ id: "docs-1",
3568
+ scope: "docs",
3569
+ component: input.component,
3570
+ changes: [],
3571
+ draft: { title: `${input.component} docs`, summary: what },
3572
+ docs: { slug: input.slug, ...input.path ? { path: input.path } : {}, task: input.task, prompt: input.prompt }
3573
+ };
3574
+ return createPendingDrafts(client, composePendingDrafts({ items: [item], source: "studio", now: input.now, random: input.random }));
3575
+ }
3576
+ const DRAFT_POLL_INTERVAL_MS = 5e3;
3577
+ const DRAFT_POLL_TIMEOUT_MS = 10 * 6e4;
3578
+ class DraftPoller {
3579
+ constructor(o2) {
3580
+ this.o = o2;
3581
+ }
3582
+ o;
3583
+ handle;
3584
+ startedAt = 0;
3585
+ running = false;
3586
+ busy = false;
3587
+ get active() {
3588
+ return this.running;
3589
+ }
3590
+ now() {
3591
+ return (this.o.now ?? Date.now)();
3592
+ }
3593
+ schedule() {
3594
+ const set = this.o.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
3595
+ this.handle = set(() => void this.tick(), this.o.intervalMs ?? DRAFT_POLL_INTERVAL_MS);
3596
+ }
3597
+ /** Starts (or restarts, after a timeout) polling; the timeout counts from now. */
3598
+ start() {
3599
+ this.stop();
3600
+ this.running = true;
3601
+ this.startedAt = this.now();
3602
+ this.schedule();
3603
+ }
3604
+ stop() {
3605
+ if (this.handle !== void 0) (this.o.clearTimer ?? ((h3) => clearTimeout(h3)))(this.handle);
3606
+ this.handle = void 0;
3607
+ this.running = false;
3608
+ }
3609
+ async tick() {
3610
+ if (!this.running) return;
3611
+ this.handle = void 0;
3612
+ const done = await this.check();
3613
+ if (done || !this.running) return;
3614
+ if (this.now() - this.startedAt >= (this.o.timeoutMs ?? DRAFT_POLL_TIMEOUT_MS)) {
3615
+ this.stop();
3616
+ this.o.onTimeout();
3617
+ return;
3618
+ }
3619
+ this.schedule();
3620
+ }
3621
+ /** Reads the file once. True when polling is over (answered or gone). */
3622
+ async check() {
3623
+ if (this.busy) return false;
3624
+ this.busy = true;
3625
+ try {
3626
+ const cur = await readPendingDrafts(this.o.client, this.o.sessionId);
3627
+ if (!cur) {
3628
+ this.stop();
3629
+ this.o.onGone?.();
3630
+ return true;
3631
+ }
3632
+ if (cur.drafts.status === "drafted" && cur.drafts.results?.length) {
3633
+ this.stop();
3634
+ this.o.onResults(cur.drafts.results, cur.drafts);
3635
+ return true;
3636
+ }
3637
+ return false;
3638
+ } catch (err) {
3639
+ this.o.onError?.(err instanceof Error ? err.message : String(err));
3640
+ return false;
3641
+ } finally {
3642
+ this.busy = false;
3643
+ }
3644
+ }
3645
+ }
3646
+ function explorationSummary(p, _owner, _repo, s) {
3647
+ const e2 = parseExplorationBody(p.body);
3648
+ const slug = p.headRef?.startsWith(EXPLORATION_PREFIX) ? p.headRef.slice(EXPLORATION_PREFIX.length) : e2?.slug ?? explorationSlug(p.title);
2513
3649
  return {
2514
- number: p2.number,
2515
- url: p2.url,
2516
- branch: p2.headRef,
3650
+ number: p.number,
3651
+ url: p.url,
3652
+ branch: p.headRef,
2517
3653
  slug,
2518
- name: e3?.name ?? p2.title.replace(/^Exploration:\s*/i, ""),
2519
- purpose: e3?.purpose ?? "",
2520
- ...e3?.useCase ? { useCase: e3.useCase } : {},
2521
- status: explorationStatus({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body }),
2522
- draft: p2.draft,
2523
- author: e3?.author ?? p2.author,
2524
- 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 })
3654
+ name: e2?.name ?? p.title.replace(/^Exploration:\s*/i, ""),
3655
+ purpose: e2?.purpose ?? "",
3656
+ ...e2?.useCase ? { useCase: e2.useCase } : {},
3657
+ status: explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body }),
3658
+ draft: p.draft,
3659
+ author: e2?.author ?? p.author,
3660
+ ...e2?.source ? { source: e2.source } : {},
3661
+ ...e2?.agent ? { agent: e2.agent } : {},
3662
+ updatedAt: p.updatedAt,
3663
+ headSha: p.headSha,
3664
+ previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha })
2527
3665
  };
2528
3666
  }
2529
3667
  async function listExplorationsOnGitHub(gh, s, opts = {}) {
2530
3668
  const pulls = await gh.listPullRequests(opts.state ?? "open");
2531
- return pulls.filter((p2) => isExplorationPull({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body })).map((p2) => explorationSummary(p2, gh.owner, gh.name, s));
3669
+ return pulls.filter((p) => isExplorationPull({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body })).map((p) => explorationSummary(p, gh.owner, gh.name, s));
2532
3670
  }
2533
- async function startExplorationOnGitHub(gh, e3, s) {
2534
- const problems = explorationProblems(e3);
3671
+ async function startExplorationOnGitHub(gh, e2, s) {
3672
+ const problems = explorationProblems(e2);
2535
3673
  if (problems.length) throw new GitHubError(422, problems.join(" "));
2536
3674
  if (!gh.canWrite) throw new GitHubError(401, "Starting an exploration needs a GitHub token (contents and pull requests, read and write, on this repository only).");
2537
- const slug = e3.slug || explorationSlug(e3.name);
3675
+ const slug = e2.slug || explorationSlug(e2.name);
2538
3676
  const branch = `${EXPLORATION_PREFIX}${slug}`;
2539
3677
  const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
2540
3678
  if (await x.getBranchSha(branch)) throw new GitHubError(422, `An exploration named "${slug}" already exists (${branch}). Pick another name, or continue that one.`);
2541
- const full = { ...e3, slug, startedAt: e3.startedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
3679
+ const full = { ...e2, slug, startedAt: e2.startedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
2542
3680
  const commit = await x.commitToNewBranch({
2543
3681
  base: s.base,
2544
3682
  branch,
@@ -2549,7 +3687,7 @@ ${full.purpose.trim()}
2549
3687
  Exploration: never merged as is. Promote it to review or close it.`,
2550
3688
  files: [{ path: explorationNotePath(slug), content: explorationNote(full) }]
2551
3689
  });
2552
- const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps };
3690
+ const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
2553
3691
  const pr = await x.createPullRequest({ title: explorationPrTitle(full), head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
2554
3692
  await x.addLabels(pr.number, [EXPLORATION_LABEL]);
2555
3693
  await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
@@ -2559,15 +3697,25 @@ Exploration: never merged as is. Promote it to review or close it.`,
2559
3697
  pr: pr.number,
2560
3698
  url: pr.url,
2561
3699
  commit: commit.sha,
2562
- install: explorationInstall({ owner: gh.owner, repo: gh.name, pr: pr.number, packages: s.packages, sha: commit.sha }),
3700
+ tryIt: { state: "building" },
2563
3701
  previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
2564
3702
  };
2565
3703
  }
3704
+ async function explorationTryItOnGitHub(gh, e2) {
3705
+ const [comments, head] = await Promise.all([gh.listIssueComments(e2.number), gh.getBranchSha(e2.branch).catch(() => void 0)]);
3706
+ return explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
3707
+ }
3708
+ async function explorationTestBuildOnGitHub(gh, e2, config) {
3709
+ const [comments, head] = await Promise.all([gh.listIssueComments(e2.number), gh.getBranchSha(e2.branch).catch(() => void 0)]);
3710
+ const tryIt = explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
3711
+ const previews = await branchPreviewUrls(gh, e2.number, config, { branch: e2.branch, sha: head, comments }).catch(() => []);
3712
+ return { tryIt, previews, settled: tryIt.state !== "building" && previewsSettled(previews) };
3713
+ }
2566
3714
  async function findExplorationOnGitHub(gh, ref) {
2567
3715
  if (typeof ref === "number" || /^#?\d+$/.test(String(ref))) {
2568
- const p2 = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
2569
- if (!isExplorationPull({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body })) throw new GitHubError(422, `Pull request #${p2.number} is not an exploration.`);
2570
- return p2;
3716
+ const p = await gh.getPullRequest(Number(String(ref).replace(/^#/, "")));
3717
+ if (!isExplorationPull({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body })) throw new GitHubError(422, `Pull request #${p.number} is not an exploration.`);
3718
+ return p;
2571
3719
  }
2572
3720
  const branch = `${EXPLORATION_PREFIX}${explorationSlug(String(ref))}`;
2573
3721
  const open = await gh.findOpenPullRequest(branch);
@@ -2575,65 +3723,107 @@ async function findExplorationOnGitHub(gh, ref) {
2575
3723
  return gh.getPullRequest(open.number);
2576
3724
  }
2577
3725
  async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
2578
- const p2 = await findExplorationOnGitHub(gh, ref);
2579
- const status = explorationStatus({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body });
2580
- if (status !== "active") throw new GitHubError(422, status === "closed" ? `Exploration #${p2.number} is closed. Reopen it on GitHub first.` : `#${p2.number} is already promoted to review.`);
2581
- const files = await gh.listPullRequestFiles(p2.number);
2582
- const plan = promotionPlan({ title: p2.title, body: p2.body, labels: p2.labels, files: files.map((f) => ({ path: f.filename, status: f.status })), config: opts.config, requests: opts.requests, date: opts.date });
2583
- const out = { ...plan, number: p2.number, url: p2.url, branch: p2.headRef, warnings: [] };
3726
+ const p = await findExplorationOnGitHub(gh, ref);
3727
+ const status = explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body });
3728
+ if (status !== "active") throw new GitHubError(422, status === "closed" ? `Exploration #${p.number} is closed. Reopen it on GitHub first.` : `#${p.number} is already promoted to review.`);
3729
+ const files = await gh.listPullRequestFiles(p.number);
3730
+ const plan = promotionPlan({ title: p.title, body: p.body, labels: p.labels, files: files.map((f) => ({ path: f.filename, status: f.status })), config: opts.config, requests: opts.requests, date: opts.date });
3731
+ const out = { ...plan, number: p.number, url: p.url, branch: p.headRef, warnings: [] };
2584
3732
  if (opts.dryRun) return out;
2585
3733
  if (!gh.canWrite) throw new GitHubError(401, "Promoting an exploration needs a GitHub token (contents and pull requests, read and write).");
2586
3734
  const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
2587
3735
  if (plan.removeFiles.length) {
2588
- 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) => {
2589
- out.warnings.push(`The exploration note stays on the branch (${e3.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
3736
+ await x.commitToBranch({ branch: p.headRef, message: "chore(explore): promote to review, remove the exploration note", files: plan.removeFiles.map((path) => ({ path, content: null })) }).catch((e2) => {
3737
+ if (e2 instanceof GitHubError && e2.auth === "rejected") throw e2;
3738
+ out.warnings.push(`The exploration note stays on the branch (${e2.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
2590
3739
  });
2591
3740
  }
2592
- await x.addLabels(p2.number, plan.addLabels);
2593
- await x.removeLabel(p2.number, EXPLORATION_LABEL).catch((e3) => {
2594
- if (!(e3 instanceof GitHubError && e3.status === 404)) out.warnings.push(`The exploration label is still on (${e3.message}). Remove it on GitHub.`);
3741
+ await x.addLabels(p.number, plan.addLabels);
3742
+ await x.removeLabel(p.number, EXPLORATION_LABEL).catch((e2) => {
3743
+ if (!(e2 instanceof GitHubError && e2.status === 404)) out.warnings.push(`The exploration label is still on (${e2.message}). Remove it on GitHub.`);
2595
3744
  });
2596
- await x.updatePullRequest(p2.number, { title: plan.title, body: plan.body });
2597
- await x.markReadyForReview(p2.number).catch((e3) => {
2598
- out.warnings.push(`Still a draft: ${e3.message}`);
3745
+ await x.updatePullRequest(p.number, { title: plan.title, body: plan.body });
3746
+ await x.markReadyForReview(p.number).catch((e2) => {
3747
+ out.warnings.push(`Still a draft: ${e2.message}`);
2599
3748
  });
2600
3749
  return out;
2601
3750
  }
2602
3751
  async function closeExplorationOnGitHub(gh, ref) {
2603
3752
  if (!gh.canWrite) throw new GitHubError(401, "Closing an exploration needs a GitHub token (pull requests: write).");
2604
- const p2 = await findExplorationOnGitHub(gh, ref);
2605
- await gh.updatePullRequest(p2.number, { state: "closed" });
2606
- return { number: p2.number, url: p2.url, branch: p2.headRef };
3753
+ const p = await findExplorationOnGitHub(gh, ref);
3754
+ await gh.updatePullRequest(p.number, { state: "closed" });
3755
+ return { number: p.number, url: p.url, branch: p.headRef };
3756
+ }
3757
+ async function linkRequestToExplorationOnGitHub(gh, n2, branch, opts = {}) {
3758
+ if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
3759
+ const issue = await gh.getIssue(n2);
3760
+ const body = withRequestExploration(issue.body, branch);
3761
+ if (!body) throw new GitHubError(422, `#${n2} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
3762
+ await gh.updateIssue(n2, { body });
3763
+ const warnings = [];
3764
+ if (!await gh.addLabels(n2, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n2}.`);
3765
+ let assignees = [];
3766
+ if (opts.assignee) {
3767
+ try {
3768
+ assignees = await gh.addAssignees(n2, [opts.assignee.replace(/^@/, "")]);
3769
+ } catch {
3770
+ warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n2}.`);
3771
+ }
3772
+ }
3773
+ return { number: n2, branch, assignees, warnings };
3774
+ }
3775
+ async function assignRequestOnGitHub(gh, n2, login) {
3776
+ const who = login.trim().replace(/^@/, "");
3777
+ if (!who) throw new GitHubError(422, "Pick who should work on it.");
3778
+ const assignees = await gh.addAssignees(n2, [who]);
3779
+ if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
3780
+ return assignees;
3781
+ }
3782
+ async function declineRequestOnGitHub(gh, n2, reason, by2) {
3783
+ const problems = declineReasonProblems(reason);
3784
+ if (problems.length) throw new GitHubError(422, problems.join(" "));
3785
+ await gh.addComment(n2, declineComment(reason, by2));
3786
+ await gh.updateIssue(n2, { state: "closed", state_reason: "not_planned" });
2607
3787
  }
2608
3788
  export {
2609
- ae as AAA_MIN,
2610
- af as AA_MIN,
3789
+ ai as AAA_MIN,
3790
+ aj as AA_MIN,
3791
+ ADMIN_ROLE_ID,
3792
+ ANTHROPIC_MESSAGES_URL,
2611
3793
  BELL,
2612
3794
  BRAND_FONTS,
2613
3795
  BRAND_SLUG_RE,
2614
- ag as BULK_LIMIT,
3796
+ ak as BULK_LIMIT,
2615
3797
  k as COLOR_FIELDS,
2616
3798
  C as COMPONENT_SPECS_VERSION,
2617
3799
  CONFIG_FILE,
2618
- ah as CONTRAST_CELL_MAX,
2619
- ai as CONTRAST_CHECKS_FILE,
3800
+ al as CONTRAST_CELL_MAX,
3801
+ am as CONTRAST_CHECKS_FILE,
2620
3802
  ConfigError,
2621
- aj as DEFAULT_COMPONENT_SPECS_FILE,
2622
- ak as DEFAULT_GOVERNANCE,
2623
- al as DEFAULT_OVERRIDE,
3803
+ an as DEFAULT_COMPONENT_SPECS_FILE,
3804
+ ao as DEFAULT_GOVERNANCE,
3805
+ ap as DEFAULT_OVERRIDE,
2624
3806
  DEFAULT_PROTECTED_BRANCHES,
3807
+ aq as DEFAULT_RIGHTS,
2625
3808
  DEFAULT_TOKEN_PATHS,
3809
+ ar as DEFAULT_VISUAL_IMPACT,
2626
3810
  DEMO_RELEASES,
2627
3811
  DOCS_CHANGE_BUMP,
3812
+ DOCS_DRAFT_MODEL,
2628
3813
  DOCS_KEYS,
3814
+ DOCS_SELECTION_MODES,
2629
3815
  DRAFTS_DIR,
2630
3816
  DRAFT_EXCHANGE_VERSION,
3817
+ DRAFT_LINE_MAX,
2631
3818
  DRAFT_OUTPUT_CONTRACT,
2632
3819
  DRAFT_PHRASE,
3820
+ DRAFT_POLL_INTERVAL_MS,
3821
+ DRAFT_POLL_TIMEOUT_MS,
2633
3822
  DRAFT_SUMMARY_SENTENCES,
2634
3823
  DRAFT_TITLE_MAX,
2635
3824
  DRAFT_VOICE_RULES,
2636
3825
  D as DTCG_TYPES,
3826
+ DraftPoller,
2637
3827
  EXPLORATION_CHECK,
2638
3828
  EXPLORATION_LABEL,
2639
3829
  EXPLORATION_NOTES_DIR,
@@ -2645,107 +3835,147 @@ export {
2645
3835
  EXT_SCOPES,
2646
3836
  GitHubClient,
2647
3837
  GitHubError,
2648
- am as JsonParseError,
3838
+ as as JsonParseError,
2649
3839
  LIGHTNESS,
2650
3840
  LayeredTokenSet,
2651
3841
  NEUTRAL_ANCHORS,
3842
+ at as OWNER_GROUPS,
2652
3843
  PIN_TOLERANCE,
2653
- an as PREVIEW_APP_LABELS,
2654
- ao as PREVIEW_COMMENT_MARKER,
2655
- ap as PREVIEW_PLACEHOLDERS,
2656
- aq as PRIMITIVE_MODE,
3844
+ au as PREVIEW_APP_LABELS,
3845
+ av as PREVIEW_COMMENT_MARKER,
3846
+ aw as PREVIEW_PLACEHOLDERS,
3847
+ ax as PRIMITIVE_MODE,
2657
3848
  PROSE_KEYS,
3849
+ PROTECTION_PARTS,
3850
+ PROTECTION_PART_PHRASE,
2658
3851
  ProtectedBranchError,
2659
3852
  RADIUS_PRESETS,
2660
3853
  REQUESTS_BRANCH,
2661
3854
  REQUEST_KINDS,
2662
3855
  REQUEST_LABEL,
2663
3856
  REQUEST_STATUS_LABEL,
3857
+ ay as RIGHTS,
3858
+ az as RIGHT_LABEL,
2664
3859
  RISK_LABELS,
2665
- ar as RISK_TIER,
3860
+ aA as RISK_TIER,
2666
3861
  SIMILARITY_THRESHOLD,
2667
3862
  SINGLE_MODE,
2668
- as as SPEC_FIELDS,
3863
+ aB as SPEC_FIELDS,
2669
3864
  STEPS,
2670
3865
  TIER_AXIS,
2671
3866
  TIER_NAMES,
2672
- at as TIER_ORDER,
2673
- au as UNRELEASED,
3867
+ aC as TIER_ORDER,
3868
+ TOKEN_REJECTED_MESSAGE,
3869
+ TOKEN_SETTINGS_URL,
3870
+ aD as TeamDirectory,
3871
+ aE as UNRELEASED,
3872
+ aF as VISUAL_CHECK_LABEL,
2674
3873
  acceptDocsDraft,
2675
3874
  acceptDocsText,
2676
3875
  addBrandToConfigText,
2677
- av as affectedContrast,
2678
- e2 as aliasOf,
3876
+ aG as affectedContrast,
3877
+ d as aliasOf,
2679
3878
  aliasTarget,
2680
3879
  anatomyLabel,
2681
3880
  anchoredRamp,
2682
- aw as appUrl,
3881
+ anthropicReplyText,
3882
+ anthropicRequest,
3883
+ aH as appForProject,
3884
+ aI as appUrl,
2683
3885
  applyConfirmedRenames,
3886
+ applyDocsSections,
2684
3887
  applyTokenEdits,
3888
+ applyTokenMerge,
3889
+ aJ as approverEntries,
3890
+ aK as approverLists,
2685
3891
  assertSafeBranch,
2686
- ax as assignVersions,
3892
+ assignRequestOnGitHub,
3893
+ aL as assignVersions,
2687
3894
  base64ToUtf8,
2688
- ay as baseBranchOf,
3895
+ aM as baseBranchOf,
2689
3896
  bellChroma,
2690
- az as branchName,
2691
- aA as branchSlug,
3897
+ aN as branchName,
3898
+ branchPreviewUrls,
3899
+ aO as branchSlug,
2692
3900
  brandDisplayName,
2693
3901
  brandPreviewVariables,
2694
3902
  buildHistoryEntry,
2695
3903
  buildReport,
2696
3904
  i as buildTokenSet,
2697
3905
  buildUsageIndex,
2698
- aB as changeKind,
2699
- aC as changeTable,
2700
- aD as changedModes,
2701
- aE as changesetBump,
3906
+ aP as canShip,
3907
+ changeEdits,
3908
+ aQ as changeKind,
3909
+ aR as changeTable,
3910
+ aS as changedModes,
3911
+ aT as changesetBump,
2702
3912
  checkBrandSlug,
2703
3913
  checkLayeredPair,
2704
- aF as classifyLayered,
3914
+ choiceFor,
3915
+ classifyAuth,
3916
+ aU as classifyLayered,
2705
3917
  classifyRisk,
3918
+ cleanDocsText,
2706
3919
  cleanDraftText,
2707
- aG as clearRawDeep,
3920
+ aV as clearRawDeep,
2708
3921
  closeExplorationOnGitHub,
2709
3922
  closingIssueNumbers,
2710
3923
  coerceValue,
2711
3924
  collectHistory,
2712
3925
  collectRefs,
2713
- j2 as colorToCss,
2714
- k2 as colorsEqual,
3926
+ k2 as colorToCss,
3927
+ l as colorsEqual,
3928
+ combineChanges,
3929
+ combineConflicts,
2715
3930
  c as comboKey,
2716
- aH as commitMessage,
2717
- l as compactJson,
3931
+ aW as commitMessage,
3932
+ m as compactJson,
2718
3933
  componentForPath,
2719
- aI as composeChangeset,
3934
+ aX as composeChangeset,
2720
3935
  composeFiles,
2721
3936
  composeNewBrandBody,
2722
3937
  composePendingDrafts,
2723
- aJ as composePrBody,
2724
- aK as composeProposalBody,
3938
+ aY as composePrBody,
3939
+ aZ as composeProposalBody,
2725
3940
  composeRequestIssue,
2726
- aL as composite,
2727
- aM as contextFor,
3941
+ a_ as composite,
3942
+ a$ as contextFor,
2728
3943
  contextLabel,
2729
- aN as contrastDelta,
2730
- aO as contrastDeltaTable,
2731
- aP as contrastLevel,
2732
- aQ as contrastPair,
2733
- aR as contrastRatio,
3944
+ b0 as contrastDelta,
3945
+ b1 as contrastDeltaTable,
3946
+ b2 as contrastLevel,
3947
+ b3 as contrastPair,
3948
+ b4 as contrastRatio,
2734
3949
  contrastScope,
2735
- aS as countChanges,
3950
+ b5 as countChanges,
3951
+ createDocsDraftSession,
2736
3952
  createPendingDrafts,
2737
3953
  cssModuleClasses,
3954
+ cssOf,
3955
+ cssVarOf,
2738
3956
  cssVarsIn,
2739
- aT as defaultContext,
3957
+ declineComment,
3958
+ declineReasonProblems,
3959
+ declineRequestOnGitHub,
3960
+ decodePicks,
3961
+ defaultChoiceFor,
3962
+ b6 as defaultContext,
2740
3963
  defaultFile,
2741
3964
  defaultScopes,
2742
- aU as defaultTitle,
2743
- aV as defineConfig,
3965
+ b7 as defaultTitle,
3966
+ b8 as defineConfig,
2744
3967
  deletePendingDrafts,
2745
3968
  demoHistory,
2746
- aW as detectLayout,
2747
- aX as diffLayered,
3969
+ b9 as detectLayout,
3970
+ ba as diffLayered,
2748
3971
  docsPrompt,
3972
+ docsSectionShapes,
3973
+ docsSectionsAnswer,
3974
+ docsSectionsResult,
3975
+ docsSelectionPrompt,
3976
+ docsTaskKeys,
3977
+ docsTaskPrompt,
3978
+ draftChangeLine,
2749
3979
  draftImagePath,
2750
3980
  draftItemBrief,
2751
3981
  draftPushDescription,
@@ -2754,78 +3984,106 @@ export {
2754
3984
  dtcgColor,
2755
3985
  effectiveScopes,
2756
3986
  emptyTokenFile,
3987
+ encodePicks,
3988
+ ensureRequestsBranch,
3989
+ bb as entriesFor,
3990
+ bc as entryLabel,
2757
3991
  evaluatePairs,
3992
+ bd as expandEntries,
3993
+ be as expandOwners,
2758
3994
  explorationBranch,
2759
- explorationInstall,
2760
3995
  explorationNote,
2761
3996
  explorationNotePath,
2762
3997
  explorationPrBody,
2763
3998
  explorationPrTitle,
2764
3999
  explorationProblems,
2765
4000
  explorationRules,
4001
+ explorationShareMessage,
2766
4002
  explorationSlug,
2767
4003
  explorationSlugFromBranch,
2768
4004
  explorationStatus,
2769
4005
  explorationSummary,
2770
- aY as fieldChanges,
2771
- aZ as fileModes,
2772
- a_ as filterHistory,
4006
+ explorationTestBuildOnGitHub,
4007
+ explorationTryIt,
4008
+ explorationTryItOnGitHub,
4009
+ bf as fieldChanges,
4010
+ bg as fileModes,
4011
+ bh as filterHistory,
2773
4012
  findExplorationOnGitHub,
4013
+ findPreviewComment,
2774
4014
  findReferenceProblems,
2775
- a$ as findToken,
2776
- b0 as firstParagraph,
2777
- m as flattenLayerFile,
2778
- b1 as flattenTokens,
4015
+ bi as findToken,
4016
+ bj as findVercelComment,
4017
+ bk as firstParagraph,
4018
+ n as flattenLayerFile,
4019
+ flattenTokens,
2779
4020
  formatChangeset,
2780
4021
  formatDocsJson,
2781
- b2 as formatNumber,
4022
+ bl as formatNumber,
4023
+ bm as formatOwnerEntry,
2782
4024
  formatValue,
2783
4025
  friendlyMessage,
2784
4026
  fromHex,
2785
4027
  fromPlain,
2786
4028
  generateRamp,
2787
- b3 as getChild,
4029
+ bn as getChild,
2788
4030
  getEntry,
2789
4031
  gitHubHistoryBackend,
2790
4032
  g2 as globToRegExp,
4033
+ bo as groupsPhrase,
2791
4034
  h as hashSpec,
2792
4035
  h2 as hexToRgba,
4036
+ bp as holdersOf,
2793
4037
  j as iconComponentName,
2794
4038
  idExtensionFor,
2795
4039
  impactedModules,
2796
4040
  importsIn,
2797
- b4 as indexByPath,
2798
- b5 as inheritedTypeAt,
2799
- installCommand,
4041
+ bq as indexByPath,
4042
+ br as inheritedTypeAt,
4043
+ bs as installCommand,
4044
+ bt as installCommandFromPreviewComment,
2800
4045
  isActiveExploration,
2801
4046
  isAlias,
4047
+ isDocsSelectionMode,
2802
4048
  isDraftSessionId,
2803
4049
  isExplorationBranch,
2804
4050
  isExplorationPull,
2805
- b6 as isGuardrail,
4051
+ bu as isGuardrail,
2806
4052
  isHex,
4053
+ isMissingPermission,
2807
4054
  isMultiMode,
2808
- b7 as isTokenFile,
4055
+ isPlanRefusal,
4056
+ isProseKey,
4057
+ bv as isTeamEntry,
4058
+ bw as isTokenFile,
4059
+ isTokenRejected,
4060
+ bx as isVercelComment,
2809
4061
  jiraIssueFields,
2810
- b8 as kindForMin,
2811
- b9 as layeredImpact,
2812
- n as layeredParityIssues,
4062
+ by as kindForMin,
4063
+ bz as layeredImpact,
4064
+ o as layeredParityIssues,
2813
4065
  g as layoutOptions,
2814
4066
  levenshtein,
4067
+ linkRequestToExplorationOnGitHub,
2815
4068
  listBrands,
2816
4069
  listExplorationsOnGitHub,
2817
4070
  listPendingDrafts,
2818
4071
  loadConfig,
2819
4072
  loadGitHubHistory,
4073
+ loadProtection,
2820
4074
  loadRepoFiles,
2821
- ba as luminance,
4075
+ loadRequiredChecks,
4076
+ bA as luminance,
2822
4077
  markClaudeDraft,
4078
+ mergeDocsDraft,
2823
4079
  mergeDraftResults,
2824
4080
  mergeReleases,
4081
+ missingChoices,
4082
+ missingPermissionMessage,
2825
4083
  missingProse,
2826
4084
  modeDisplayName,
2827
4085
  modeFile,
2828
- bb as modeLabel,
4086
+ bB as modeLabel,
2829
4087
  modesOf$1 as modesOf,
2830
4088
  moveToken,
2831
4089
  nearestStep,
@@ -2834,102 +4092,152 @@ export {
2834
4092
  neutralOffset,
2835
4093
  newBrandSection,
2836
4094
  newDraftSessionId,
2837
- bc as nodesEqual,
4095
+ bC as nodesEqual,
4096
+ bD as normalizeRights,
2838
4097
  oklch,
2839
4098
  oklchToLinear,
2840
- bd as overrideKey,
4099
+ bE as overrideKey,
4100
+ bF as ownerEntryProblems,
2841
4101
  packageDirCandidates,
2842
- be as pairKey,
4102
+ bG as pairKey,
2843
4103
  parseChangelog,
2844
- bf as parseChangesetText,
2845
- p as parseColor,
2846
- bg as parseComponentSpecs,
4104
+ bH as parseChangesetText,
4105
+ parseColor,
4106
+ bI as parseComponentSpecs,
2847
4107
  parseContrastChecks,
2848
- o as parseDimension,
4108
+ q as parseDimension,
4109
+ parseDocsReply,
4110
+ parseDocsTaskAnswer,
2849
4111
  parseExplorationBody,
2850
4112
  parseJson,
4113
+ bJ as parseOwnerEntry,
2851
4114
  parsePendingDrafts,
2852
4115
  parseRepo,
2853
4116
  parseRequestIssue,
4117
+ parseStructureChange,
2854
4118
  parseTokenFile,
4119
+ bK as parseVercelComment,
2855
4120
  patchDocsJson,
2856
4121
  pathId,
2857
4122
  pathSimilarity,
4123
+ permissionLabel,
4124
+ pickedOption,
2858
4125
  plainEqual,
2859
4126
  planNewBrand,
2860
- bh as planRestore,
2861
- bi as planRevert,
2862
- bj as planToJson,
4127
+ bL as planRestore,
4128
+ bM as planRevert,
4129
+ bN as planToJson,
4130
+ planTokenMerge,
2863
4131
  e as prNumberFromSubject,
2864
4132
  prefillDocs,
2865
- bk as previewComment,
4133
+ bO as previewComment,
2866
4134
  previewLinks,
2867
- bl as previewUrl,
4135
+ bP as previewLinksOf,
4136
+ previewOverrides,
4137
+ bQ as previewUrl,
4138
+ previewsSettled,
4139
+ bR as projectOfLabel,
2868
4140
  promoteExplorationOnGitHub,
2869
4141
  promotionPlan,
2870
4142
  propsTable,
2871
- bm as protectedBranchesOf,
4143
+ protectLibrary,
4144
+ protectRefusal,
4145
+ bS as protectedBranchesOf,
4146
+ protectionRuleset,
4147
+ protectionRulesetName,
4148
+ protectionSentence,
4149
+ protectionState,
2872
4150
  pushChangeRows,
2873
4151
  readPendingDrafts,
4152
+ readPreviewComment,
2874
4153
  readTokenAt,
2875
4154
  reconcileIds,
2876
4155
  releasesFromTags,
2877
4156
  removeToken,
2878
- bn as renderedColor,
4157
+ removedSide,
4158
+ bT as renderedColor,
2879
4159
  requestBrief,
2880
4160
  requestImagePath,
2881
4161
  requestIssueTitle,
2882
4162
  requestStatus,
2883
- bo as resolve,
4163
+ requiredCheckNames,
4164
+ requiredPermission,
4165
+ resolve,
4166
+ resolveBranchPreviews,
2884
4167
  r as rgbaToDtcg,
2885
- q as rgbaToHex,
4168
+ rgbaToHex,
4169
+ bU as rightsProblems,
2886
4170
  round3,
2887
- bp as runLayeredContrast,
4171
+ rulesSettingsUrl,
4172
+ rulesetTargetsDefault,
4173
+ bV as runLayeredContrast,
2888
4174
  sameScopes,
2889
- bq as sameToken,
4175
+ bW as sameToken,
4176
+ sanitizeDocsReplacement,
4177
+ sanitizeDocsResult,
4178
+ sanitizeDocsSections,
4179
+ sanitizeDraftLine,
2890
4180
  sanitizeDraftText,
4181
+ sanitizeDraftTitle,
4182
+ sanitizeResultLines,
4183
+ sectionsNeedingReview,
2891
4184
  selectTokenFiles,
2892
4185
  serializeJson,
2893
4186
  serializePendingDrafts,
2894
4187
  serializeTokenFile,
2895
4188
  shadowedEdits,
4189
+ bX as shipPolicy,
4190
+ shortVariant,
4191
+ shortVariants,
2896
4192
  slugify,
2897
- snapshotUrl,
2898
- br as specHash,
4193
+ bY as snapshotUrl,
4194
+ bZ as specHash,
2899
4195
  startExplorationOnGitHub,
2900
- bs as summarizeTokenChange,
2901
- bt as tarballAssetName,
2902
- tarballUrl,
2903
- bu as threeWayDiff,
4196
+ b_ as summarizeTokenChange,
4197
+ takeMine,
4198
+ b$ as tarballAssetName,
4199
+ c0 as tarballUrl,
4200
+ c1 as teamKey,
4201
+ c2 as teamKeyOf,
4202
+ c3 as teamUnreadableMessage,
4203
+ c4 as teamsIn,
4204
+ threeWayDiff,
2904
4205
  tintedNeutral,
2905
- bv as titleOf,
4206
+ c5 as titleOf,
2906
4207
  t2 as toAlias,
2907
- bw as toChangeSummary,
4208
+ c6 as toChangeSummary,
2908
4209
  toConfigJson,
2909
4210
  toHex,
2910
- bx as toMap,
4211
+ c7 as toMap,
2911
4212
  toOklch,
2912
4213
  toPlain,
2913
- by as tokenBump,
4214
+ c8 as tokenBump,
2914
4215
  t as tokenCssVar,
2915
- bz as tokenHistory,
2916
- bA as tokenNode,
4216
+ c9 as tokenHistory,
4217
+ ca as tokenNode,
2917
4218
  tokenUsage,
2918
4219
  tokensPath,
4220
+ unpicked,
4221
+ cb as unreadableShipTeams,
4222
+ cc as unreadableTeams,
2919
4223
  updatePendingDrafts,
2920
4224
  upsertToken,
2921
4225
  utf8ToBase64,
4226
+ validateDocsTask,
2922
4227
  validateIntegrations,
2923
4228
  validateLayered,
2924
4229
  validatePendingDrafts,
2925
4230
  validateProposal,
2926
4231
  validateSet,
2927
- bB as valueIn,
4232
+ cd as valueIn,
2928
4233
  u as valueToCss,
2929
4234
  valuesEqual,
2930
4235
  v as variantKeyOf,
2931
- bC as variantName,
2932
- bD as versionTimeline,
4236
+ ce as variantName,
4237
+ cf as versionTimeline,
4238
+ cg as visualImpactProblems,
4239
+ ch as whoCanShipSentence,
4240
+ withRequestExploration,
2933
4241
  writeDraftResults
2934
4242
  };
2935
4243
  //# sourceMappingURL=index.js.map