@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/bridge.config.schema.json +74 -5
- package/dist/audit/index.js +1 -1
- package/dist/change-drafts.d.ts +29 -0
- package/dist/{component-spec-B0V_D3gM.js → component-spec-cvrSvIZj.js} +628 -140
- package/dist/component-spec-cvrSvIZj.js.map +1 -0
- package/dist/config.d.ts +16 -2
- package/dist/css-overrides.d.ts +11 -0
- package/dist/docs-ai.d.ts +116 -0
- package/dist/docs-draft.d.ts +30 -4
- package/dist/draft-exchange.d.ts +135 -14
- package/dist/explorations-github.d.ts +43 -5
- package/dist/explorations.d.ts +40 -28
- package/dist/github-client.d.ts +125 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1955 -647
- package/dist/index.js.map +1 -1
- package/dist/{layered-DGate4nv.js → layered-BnbmToGs.js} +10 -10
- package/dist/{layered-DGate4nv.js.map → layered-BnbmToGs.js.map} +1 -1
- package/dist/node/index.js +3 -3
- package/dist/previews-github.d.ts +22 -0
- package/dist/previews.d.ts +102 -0
- package/dist/protection-github.d.ts +25 -0
- package/dist/protection.d.ts +139 -0
- package/dist/{report-tNzTriGF.js → report-Bf2HEr9t.js} +2 -2
- package/dist/{report-tNzTriGF.js.map → report-Bf2HEr9t.js.map} +1 -1
- package/dist/requests-github.d.ts +21 -0
- package/dist/requests.d.ts +16 -1
- package/dist/rights.d.ts +63 -0
- package/dist/teams.d.ts +149 -0
- package/dist/token-combine.d.ts +78 -0
- package/dist/token-merge.d.ts +72 -0
- package/package.json +1 -1
- package/dist/component-spec-B0V_D3gM.js.map +0 -1
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
|
|
2
|
-
import {
|
|
3
|
-
import { a as collectRefs, i as isAlias, b as aliasTarget, v as valuesEqual, s as selectTokenFiles, L as LayeredTokenSet,
|
|
4
|
-
import { D,
|
|
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
|
|
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
|
|
105
|
+
for (const p of findReferenceProblems(set.byPath, axes)) {
|
|
106
106
|
issues.push(
|
|
107
|
-
|
|
107
|
+
p.kind === "broken" ? {
|
|
108
108
|
code: "broken-reference",
|
|
109
109
|
severity: "error",
|
|
110
|
-
path:
|
|
111
|
-
mode:
|
|
112
|
-
message: `${
|
|
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:
|
|
117
|
-
mode:
|
|
118
|
-
message: `${
|
|
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
|
|
143
|
-
if (
|
|
142
|
+
for (const e2 of edits) {
|
|
143
|
+
if (e2.tier !== "semantic") continue;
|
|
144
144
|
for (const o2 of set.overrides) {
|
|
145
|
-
if (o2.path !==
|
|
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:
|
|
149
|
+
path: e2.path,
|
|
150
150
|
mode: `${o2.brand}.${o2.theme}`,
|
|
151
|
-
message: `${
|
|
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
|
|
168
|
+
for (const c10 of typeChanges) {
|
|
169
169
|
blocking.push({
|
|
170
170
|
code: "type-change",
|
|
171
171
|
severity: "error",
|
|
172
|
-
path:
|
|
173
|
-
message: `${
|
|
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
|
|
184
|
-
const tmp = prev[
|
|
185
|
-
prev[
|
|
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
|
|
257
|
-
if (!
|
|
502
|
+
let d2 = docs.get(file);
|
|
503
|
+
if (!d2) {
|
|
258
504
|
const text = originals[file];
|
|
259
|
-
|
|
260
|
-
docs.set(file,
|
|
505
|
+
d2 = text === void 0 ? emptyTokenFile(file) : parseTokenFile(file, text);
|
|
506
|
+
docs.set(file, d2);
|
|
261
507
|
}
|
|
262
|
-
return
|
|
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
|
|
274
|
-
if (target.values[mode] === void 0) removeToken(
|
|
275
|
-
else if (renamed) moveToken(
|
|
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
|
|
284
|
-
const existing = readTokenAt(
|
|
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(
|
|
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,
|
|
301
|
-
const text = serializeTokenFile(
|
|
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
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
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
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
381
|
-
return { componentsDir: dir, modules, byVar, globalVars };
|
|
623
|
+
return parts;
|
|
382
624
|
}
|
|
383
|
-
function
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
512
|
-
if (attempt >= attempts || !(
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
880
|
-
const pending = checks.some((
|
|
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((
|
|
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(
|
|
1374
|
+
function toCommitSummary(c10) {
|
|
927
1375
|
return {
|
|
928
|
-
sha:
|
|
929
|
-
parents: (
|
|
930
|
-
date:
|
|
931
|
-
author:
|
|
932
|
-
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((
|
|
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
|
-
(
|
|
1441
|
+
(e2) => e2.type === "blob" && e2.path.startsWith(prefix) && (e2.path.endsWith(".tokens.json") || e2.path === prefix + checksFile)
|
|
992
1442
|
);
|
|
993
|
-
if (!wanted.some((
|
|
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
|
|
999
|
-
const text = await client.getBlobText(
|
|
1000
|
-
const rel =
|
|
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
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
return
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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 (
|
|
1058
|
-
const prs = await client.listPullRequestsForCommit(
|
|
1059
|
-
const pr = prs.find((
|
|
1060
|
-
if (!pr) return
|
|
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 !==
|
|
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 { ...
|
|
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((
|
|
1084
|
-
const picked = new Set(selectTokenFiles(config, blobs.map((
|
|
1085
|
-
const wanted = blobs.filter((
|
|
1086
|
-
const texts = await pool(wanted, async (
|
|
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((
|
|
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
|
|
1096
|
-
return
|
|
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
|
|
1118
|
-
const cl = files.find((
|
|
1119
|
-
const pkg = files.find((
|
|
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
|
|
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 (
|
|
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
|
|
1200
|
-
return
|
|
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
|
|
1205
|
-
let lin = oklchToLinear(L,
|
|
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
|
-
|
|
1208
|
-
lin = oklchToLinear(L,
|
|
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((
|
|
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((
|
|
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((
|
|
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 (
|
|
1438
|
-
issues.push({ level: "error", code: "invalid-color", message:
|
|
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((
|
|
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 (
|
|
1566
|
-
issues.push({ level: "error", code: "config", 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((
|
|
1580
|
-
const results = combos.flatMap((
|
|
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((
|
|
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((
|
|
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
|
|
1657
|
-
const contrastLine =
|
|
1658
|
-
const failRows =
|
|
1659
|
-
const tightest = [...
|
|
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((
|
|
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(
|
|
1754
|
-
return PROSE_KEYS.filter((k3) => isEmpty(
|
|
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
|
|
1760
|
-
const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1828
|
-
|
|
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 = (
|
|
1831
|
-
function acceptDocsDraft(
|
|
1832
|
-
|
|
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(
|
|
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 (
|
|
1838
|
-
for (const [k3, v2] of Object.entries(
|
|
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((
|
|
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(
|
|
2986
|
+
function explorationProblems(e2) {
|
|
1902
2987
|
const out = [];
|
|
1903
|
-
const name =
|
|
1904
|
-
const slug =
|
|
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 (!
|
|
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(
|
|
1913
|
-
const slug =
|
|
1914
|
-
const uc = useCaseLabel(
|
|
2997
|
+
function explorationNote(e2) {
|
|
2998
|
+
const slug = e2.slug || explorationSlug(e2.name);
|
|
2999
|
+
const uc = useCaseLabel(e2.useCase);
|
|
1915
3000
|
return [
|
|
1916
|
-
`# Exploration: ${
|
|
3001
|
+
`# Exploration: ${e2.name.trim()}`,
|
|
1917
3002
|
"",
|
|
1918
|
-
|
|
3003
|
+
e2.purpose.trim(),
|
|
1919
3004
|
"",
|
|
1920
3005
|
`- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
|
|
1921
3006
|
...uc ? [`- Use case: ${uc}`] : [],
|
|
1922
|
-
...
|
|
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
|
|
1929
|
-
const
|
|
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(
|
|
1943
|
-
const slug =
|
|
3017
|
+
function explorationPrBody(e2, ctx) {
|
|
3018
|
+
const slug = e2.slug || explorationSlug(e2.name);
|
|
1944
3019
|
const branch = `${EXPLORATION_PREFIX}${slug}`;
|
|
1945
|
-
const uc = useCaseLabel(
|
|
1946
|
-
const experimental = (
|
|
3020
|
+
const uc = useCaseLabel(e2.useCase);
|
|
3021
|
+
const experimental = (e2.experimental ?? []).map((x) => x.trim()).filter(Boolean);
|
|
1947
3022
|
const lines = [
|
|
1948
|
-
`## Exploration: ${
|
|
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.** ${
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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:
|
|
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
|
|
1998
|
-
return
|
|
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(
|
|
2005
|
-
return isExplorationBranch(
|
|
3068
|
+
function isExplorationPull(p) {
|
|
3069
|
+
return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
|
|
2006
3070
|
}
|
|
2007
|
-
function explorationStatus(
|
|
2008
|
-
const merged = Boolean(
|
|
2009
|
-
if (
|
|
2010
|
-
if (
|
|
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: "
|
|
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((
|
|
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) => ``), ""] : [],
|
|
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((
|
|
2108
|
-
if (prs.some((
|
|
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((
|
|
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 = (...
|
|
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.
|
|
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
|
|
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(
|
|
3328
|
+
const problems = validatePendingDrafts(p);
|
|
2351
3329
|
if (problems.length) throw new Error(problems.join(" "));
|
|
2352
|
-
return
|
|
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
|
|
2361
|
-
if (
|
|
2362
|
-
if (!isDraftSessionId(
|
|
2363
|
-
if (!str(
|
|
2364
|
-
if (
|
|
2365
|
-
if (
|
|
2366
|
-
if (!Array.isArray(
|
|
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(
|
|
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 "
|
|
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((
|
|
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 (
|
|
2387
|
-
if (!Array.isArray(
|
|
2388
|
-
else
|
|
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(
|
|
2403
|
-
return `${JSON.stringify(
|
|
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
|
|
2426
|
-
const
|
|
2427
|
-
|
|
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 ${
|
|
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
|
-
|
|
2432
|
-
|
|
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 =
|
|
2435
|
-
return { ...
|
|
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((
|
|
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.
|
|
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,
|
|
2454
|
-
const problems = validatePendingDrafts(
|
|
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(
|
|
2457
|
-
return
|
|
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((
|
|
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
|
|
2469
|
-
const id =
|
|
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((
|
|
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, (
|
|
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((
|
|
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
|
|
2511
|
-
|
|
2512
|
-
const
|
|
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:
|
|
2515
|
-
url:
|
|
2516
|
-
branch:
|
|
3650
|
+
number: p.number,
|
|
3651
|
+
url: p.url,
|
|
3652
|
+
branch: p.headRef,
|
|
2517
3653
|
slug,
|
|
2518
|
-
name:
|
|
2519
|
-
purpose:
|
|
2520
|
-
...
|
|
2521
|
-
status: explorationStatus({ state:
|
|
2522
|
-
draft:
|
|
2523
|
-
author:
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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((
|
|
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,
|
|
2534
|
-
const problems = explorationProblems(
|
|
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 =
|
|
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 = { ...
|
|
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
|
-
|
|
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
|
|
2569
|
-
if (!isExplorationPull({ state:
|
|
2570
|
-
return
|
|
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
|
|
2579
|
-
const status = explorationStatus({ state:
|
|
2580
|
-
if (status !== "active") throw new GitHubError(422, status === "closed" ? `Exploration #${
|
|
2581
|
-
const files = await gh.listPullRequestFiles(
|
|
2582
|
-
const plan = promotionPlan({ title:
|
|
2583
|
-
const out = { ...plan, number:
|
|
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:
|
|
2589
|
-
|
|
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(
|
|
2593
|
-
await x.removeLabel(
|
|
2594
|
-
if (!(
|
|
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(
|
|
2597
|
-
await x.markReadyForReview(
|
|
2598
|
-
out.warnings.push(`Still a draft: ${
|
|
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
|
|
2605
|
-
await gh.updatePullRequest(
|
|
2606
|
-
return { number:
|
|
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
|
-
|
|
2610
|
-
|
|
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
|
-
|
|
3796
|
+
ak as BULK_LIMIT,
|
|
2615
3797
|
k as COLOR_FIELDS,
|
|
2616
3798
|
C as COMPONENT_SPECS_VERSION,
|
|
2617
3799
|
CONFIG_FILE,
|
|
2618
|
-
|
|
2619
|
-
|
|
3800
|
+
al as CONTRAST_CELL_MAX,
|
|
3801
|
+
am as CONTRAST_CHECKS_FILE,
|
|
2620
3802
|
ConfigError,
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
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
|
-
|
|
3838
|
+
as as JsonParseError,
|
|
2649
3839
|
LIGHTNESS,
|
|
2650
3840
|
LayeredTokenSet,
|
|
2651
3841
|
NEUTRAL_ANCHORS,
|
|
3842
|
+
at as OWNER_GROUPS,
|
|
2652
3843
|
PIN_TOLERANCE,
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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
|
-
|
|
3860
|
+
aA as RISK_TIER,
|
|
2666
3861
|
SIMILARITY_THRESHOLD,
|
|
2667
3862
|
SINGLE_MODE,
|
|
2668
|
-
|
|
3863
|
+
aB as SPEC_FIELDS,
|
|
2669
3864
|
STEPS,
|
|
2670
3865
|
TIER_AXIS,
|
|
2671
3866
|
TIER_NAMES,
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
-
|
|
2678
|
-
|
|
3876
|
+
aG as affectedContrast,
|
|
3877
|
+
d as aliasOf,
|
|
2679
3878
|
aliasTarget,
|
|
2680
3879
|
anatomyLabel,
|
|
2681
3880
|
anchoredRamp,
|
|
2682
|
-
|
|
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
|
-
|
|
3892
|
+
assignRequestOnGitHub,
|
|
3893
|
+
aL as assignVersions,
|
|
2687
3894
|
base64ToUtf8,
|
|
2688
|
-
|
|
3895
|
+
aM as baseBranchOf,
|
|
2689
3896
|
bellChroma,
|
|
2690
|
-
|
|
2691
|
-
|
|
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
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
-
|
|
3914
|
+
choiceFor,
|
|
3915
|
+
classifyAuth,
|
|
3916
|
+
aU as classifyLayered,
|
|
2705
3917
|
classifyRisk,
|
|
3918
|
+
cleanDocsText,
|
|
2706
3919
|
cleanDraftText,
|
|
2707
|
-
|
|
3920
|
+
aV as clearRawDeep,
|
|
2708
3921
|
closeExplorationOnGitHub,
|
|
2709
3922
|
closingIssueNumbers,
|
|
2710
3923
|
coerceValue,
|
|
2711
3924
|
collectHistory,
|
|
2712
3925
|
collectRefs,
|
|
2713
|
-
|
|
2714
|
-
|
|
3926
|
+
k2 as colorToCss,
|
|
3927
|
+
l as colorsEqual,
|
|
3928
|
+
combineChanges,
|
|
3929
|
+
combineConflicts,
|
|
2715
3930
|
c as comboKey,
|
|
2716
|
-
|
|
2717
|
-
|
|
3931
|
+
aW as commitMessage,
|
|
3932
|
+
m as compactJson,
|
|
2718
3933
|
componentForPath,
|
|
2719
|
-
|
|
3934
|
+
aX as composeChangeset,
|
|
2720
3935
|
composeFiles,
|
|
2721
3936
|
composeNewBrandBody,
|
|
2722
3937
|
composePendingDrafts,
|
|
2723
|
-
|
|
2724
|
-
|
|
3938
|
+
aY as composePrBody,
|
|
3939
|
+
aZ as composeProposalBody,
|
|
2725
3940
|
composeRequestIssue,
|
|
2726
|
-
|
|
2727
|
-
|
|
3941
|
+
a_ as composite,
|
|
3942
|
+
a$ as contextFor,
|
|
2728
3943
|
contextLabel,
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3944
|
+
b0 as contrastDelta,
|
|
3945
|
+
b1 as contrastDeltaTable,
|
|
3946
|
+
b2 as contrastLevel,
|
|
3947
|
+
b3 as contrastPair,
|
|
3948
|
+
b4 as contrastRatio,
|
|
2734
3949
|
contrastScope,
|
|
2735
|
-
|
|
3950
|
+
b5 as countChanges,
|
|
3951
|
+
createDocsDraftSession,
|
|
2736
3952
|
createPendingDrafts,
|
|
2737
3953
|
cssModuleClasses,
|
|
3954
|
+
cssOf,
|
|
3955
|
+
cssVarOf,
|
|
2738
3956
|
cssVarsIn,
|
|
2739
|
-
|
|
3957
|
+
declineComment,
|
|
3958
|
+
declineReasonProblems,
|
|
3959
|
+
declineRequestOnGitHub,
|
|
3960
|
+
decodePicks,
|
|
3961
|
+
defaultChoiceFor,
|
|
3962
|
+
b6 as defaultContext,
|
|
2740
3963
|
defaultFile,
|
|
2741
3964
|
defaultScopes,
|
|
2742
|
-
|
|
2743
|
-
|
|
3965
|
+
b7 as defaultTitle,
|
|
3966
|
+
b8 as defineConfig,
|
|
2744
3967
|
deletePendingDrafts,
|
|
2745
3968
|
demoHistory,
|
|
2746
|
-
|
|
2747
|
-
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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
|
-
|
|
4022
|
+
bl as formatNumber,
|
|
4023
|
+
bm as formatOwnerEntry,
|
|
2782
4024
|
formatValue,
|
|
2783
4025
|
friendlyMessage,
|
|
2784
4026
|
fromHex,
|
|
2785
4027
|
fromPlain,
|
|
2786
4028
|
generateRamp,
|
|
2787
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
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
|
-
|
|
4051
|
+
bu as isGuardrail,
|
|
2806
4052
|
isHex,
|
|
4053
|
+
isMissingPermission,
|
|
2807
4054
|
isMultiMode,
|
|
2808
|
-
|
|
4055
|
+
isPlanRefusal,
|
|
4056
|
+
isProseKey,
|
|
4057
|
+
bv as isTeamEntry,
|
|
4058
|
+
bw as isTokenFile,
|
|
4059
|
+
isTokenRejected,
|
|
4060
|
+
bx as isVercelComment,
|
|
2809
4061
|
jiraIssueFields,
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4095
|
+
bC as nodesEqual,
|
|
4096
|
+
bD as normalizeRights,
|
|
2838
4097
|
oklch,
|
|
2839
4098
|
oklchToLinear,
|
|
2840
|
-
|
|
4099
|
+
bE as overrideKey,
|
|
4100
|
+
bF as ownerEntryProblems,
|
|
2841
4101
|
packageDirCandidates,
|
|
2842
|
-
|
|
4102
|
+
bG as pairKey,
|
|
2843
4103
|
parseChangelog,
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
4104
|
+
bH as parseChangesetText,
|
|
4105
|
+
parseColor,
|
|
4106
|
+
bI as parseComponentSpecs,
|
|
2847
4107
|
parseContrastChecks,
|
|
2848
|
-
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
4127
|
+
bL as planRestore,
|
|
4128
|
+
bM as planRevert,
|
|
4129
|
+
bN as planToJson,
|
|
4130
|
+
planTokenMerge,
|
|
2863
4131
|
e as prNumberFromSubject,
|
|
2864
4132
|
prefillDocs,
|
|
2865
|
-
|
|
4133
|
+
bO as previewComment,
|
|
2866
4134
|
previewLinks,
|
|
2867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4157
|
+
removedSide,
|
|
4158
|
+
bT as renderedColor,
|
|
2879
4159
|
requestBrief,
|
|
2880
4160
|
requestImagePath,
|
|
2881
4161
|
requestIssueTitle,
|
|
2882
4162
|
requestStatus,
|
|
2883
|
-
|
|
4163
|
+
requiredCheckNames,
|
|
4164
|
+
requiredPermission,
|
|
4165
|
+
resolve,
|
|
4166
|
+
resolveBranchPreviews,
|
|
2884
4167
|
r as rgbaToDtcg,
|
|
2885
|
-
|
|
4168
|
+
rgbaToHex,
|
|
4169
|
+
bU as rightsProblems,
|
|
2886
4170
|
round3,
|
|
2887
|
-
|
|
4171
|
+
rulesSettingsUrl,
|
|
4172
|
+
rulesetTargetsDefault,
|
|
4173
|
+
bV as runLayeredContrast,
|
|
2888
4174
|
sameScopes,
|
|
2889
|
-
|
|
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
|
-
|
|
4193
|
+
bY as snapshotUrl,
|
|
4194
|
+
bZ as specHash,
|
|
2899
4195
|
startExplorationOnGitHub,
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
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
|
-
|
|
4206
|
+
c5 as titleOf,
|
|
2906
4207
|
t2 as toAlias,
|
|
2907
|
-
|
|
4208
|
+
c6 as toChangeSummary,
|
|
2908
4209
|
toConfigJson,
|
|
2909
4210
|
toHex,
|
|
2910
|
-
|
|
4211
|
+
c7 as toMap,
|
|
2911
4212
|
toOklch,
|
|
2912
4213
|
toPlain,
|
|
2913
|
-
|
|
4214
|
+
c8 as tokenBump,
|
|
2914
4215
|
t as tokenCssVar,
|
|
2915
|
-
|
|
2916
|
-
|
|
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
|
-
|
|
4232
|
+
cd as valueIn,
|
|
2928
4233
|
u as valueToCss,
|
|
2929
4234
|
valuesEqual,
|
|
2930
4235
|
v as variantKeyOf,
|
|
2931
|
-
|
|
2932
|
-
|
|
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
|