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