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