@bridge-ds/core 0.20.0 → 0.21.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/audit/index.js +275 -52
- package/dist/audit/index.js.map +1 -1
- package/dist/audit/missing-pieces.d.ts +4 -0
- package/dist/component-spec.d.ts +13 -0
- package/dist/exploration-done-github.d.ts +50 -0
- package/dist/exploration-resume.d.ts +136 -0
- package/dist/explorations-github.d.ts +75 -3
- package/dist/explorations.d.ts +137 -8
- package/dist/figma-fix-loop.d.ts +1 -1
- package/dist/figma-fixes.d.ts +14 -2
- package/dist/github-client.d.ts +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1708 -1283
- package/dist/index.js.map +1 -1
- package/dist/measured-only.d.ts +13 -1
- package/dist/node/index.js +11 -11
- package/dist/previews-github.d.ts +13 -0
- package/dist/previews.d.ts +8 -1
- package/dist/{prod-branch-CJZwMfVD.js → prod-branch-C7tphFAA.js} +255 -192
- package/dist/prod-branch-C7tphFAA.js.map +1 -0
- package/dist/prod-branch-github.d.ts +54 -4
- package/dist/prod-branch.d.ts +42 -1
- package/dist/products.d.ts +17 -0
- package/dist/{repo-source-CshjjC7x.js → repo-source-CEBBBtm1.js} +2165 -1998
- package/dist/repo-source-CEBBBtm1.js.map +1 -0
- package/dist/{report-CE-jHAuw.js → report-B7wNAnSu.js} +38 -38
- package/dist/{report-CE-jHAuw.js.map → report-B7wNAnSu.js.map} +1 -1
- package/dist/review-gate.d.ts +1 -1
- package/dist/try-in-product.d.ts +4 -1
- package/package.json +1 -1
- package/dist/prod-branch-CJZwMfVD.js.map +0 -1
- package/dist/repo-source-CshjjC7x.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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, aj as slugify, 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, _ as previewUrl, M as resolveBranchPreviews, ai as readPreviewComment, 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-C7tphFAA.js";
|
|
2
2
|
function changesetBump(changes) {
|
|
3
3
|
return tokenBump(changes) ?? "patch";
|
|
4
4
|
}
|
|
@@ -1252,6 +1252,10 @@ class GitHubClient {
|
|
|
1252
1252
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
|
|
1253
1253
|
return r.map((f) => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions }));
|
|
1254
1254
|
}
|
|
1255
|
+
/** The commits of a pull request, oldest first, as GitHub lists them (one read, up to 100). */
|
|
1256
|
+
async listPullRequestCommits(n) {
|
|
1257
|
+
return this.request("GET", `${this.repoPath}/pulls/${n}/commits?per_page=100`);
|
|
1258
|
+
}
|
|
1255
1259
|
async updatePullRequest(n, patch) {
|
|
1256
1260
|
await this.request("PATCH", `${this.repoPath}/pulls/${n}`, patch);
|
|
1257
1261
|
}
|
|
@@ -2486,6 +2490,8 @@ const EXPLORATION_PREFIX = "explore/";
|
|
|
2486
2490
|
const EXPLORATION_NOTES_DIR = ".bridge/explorations";
|
|
2487
2491
|
const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
|
|
2488
2492
|
const EXPLORATION_CHECK = "Exploration not merged as is";
|
|
2493
|
+
const EXPLORATION_READY_LABEL = "ready-for-devs";
|
|
2494
|
+
const EXPLORATION_ARCHIVED_LABEL = "archived";
|
|
2489
2495
|
const EXPLORATION_USE_CASES = [
|
|
2490
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." },
|
|
2491
2497
|
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
|
|
@@ -2493,13 +2499,21 @@ const EXPLORATION_USE_CASES = [
|
|
|
2493
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." },
|
|
2494
2500
|
{ value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
|
|
2495
2501
|
];
|
|
2496
|
-
const EXPLORATION_STATUS_LABEL = {
|
|
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
|
+
};
|
|
2497
2510
|
const MARK$2 = "bridge-exploration";
|
|
2498
2511
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2512
|
+
const EXPLORATION_SLUG_MAX = 60;
|
|
2499
2513
|
function explorationSlug(name) {
|
|
2500
2514
|
const s = name.trim().replace(/^explore\//i, "");
|
|
2501
2515
|
if (!/[a-z0-9]/i.test(s)) return "";
|
|
2502
|
-
return slugify(s,
|
|
2516
|
+
return slugify(s, EXPLORATION_SLUG_MAX);
|
|
2503
2517
|
}
|
|
2504
2518
|
function explorationBranch(nameOrBranch) {
|
|
2505
2519
|
return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
|
|
@@ -2511,6 +2525,25 @@ function isExplorationBranch(ref) {
|
|
|
2511
2525
|
function explorationSlugFromBranch(ref) {
|
|
2512
2526
|
return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
|
|
2513
2527
|
}
|
|
2528
|
+
const REPO_NAME = /^[\w.-]+\/[\w.-]+$/;
|
|
2529
|
+
const KEY_NAME = /^[a-z0-9][a-z0-9-]*$/i;
|
|
2530
|
+
function parseExplorationRef(input) {
|
|
2531
|
+
if (typeof input === "number") return { ref: input };
|
|
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]) };
|
|
2541
|
+
}
|
|
2542
|
+
const named = /^([a-z0-9][a-z0-9-]*):(.+)$/i.exec(s);
|
|
2543
|
+
if (named) return { product: named[1].toLowerCase(), ref: /^#?\d+$/.test(named[2].trim()) ? Number(named[2].trim().replace(/^#/, "")) : named[2].trim() };
|
|
2544
|
+
return { ref: s };
|
|
2545
|
+
}
|
|
2546
|
+
const explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
|
|
2514
2547
|
const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
|
|
2515
2548
|
function explorationProblems(e) {
|
|
2516
2549
|
const out = [];
|
|
@@ -2519,7 +2552,6 @@ function explorationProblems(e) {
|
|
|
2519
2552
|
if (!name) out.push('Name the exploration, for example "Denser data tables".');
|
|
2520
2553
|
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
|
|
2521
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.");
|
|
2522
|
-
if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
|
|
2523
2555
|
return out;
|
|
2524
2556
|
}
|
|
2525
2557
|
const useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
|
|
@@ -2529,19 +2561,18 @@ function explorationNote(e) {
|
|
|
2529
2561
|
return [
|
|
2530
2562
|
`# Exploration: ${e.name.trim()}`,
|
|
2531
2563
|
"",
|
|
2532
|
-
e.purpose.trim(),
|
|
2533
|
-
"",
|
|
2564
|
+
...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
|
|
2534
2565
|
`- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
|
|
2535
2566
|
...uc ? [`- Use case: ${uc}`] : [],
|
|
2536
2567
|
...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
|
|
2537
2568
|
"",
|
|
2538
|
-
|
|
2569
|
+
'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
|
|
2539
2570
|
""
|
|
2540
2571
|
].join("\n");
|
|
2541
2572
|
}
|
|
2542
2573
|
function explorationPrTitle(e) {
|
|
2543
|
-
const n = e.name.trim().replace(/\.$/, "");
|
|
2544
|
-
return `Exploration: ${n
|
|
2574
|
+
const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
|
|
2575
|
+
return `Exploration: ${n}`;
|
|
2545
2576
|
}
|
|
2546
2577
|
function explorationPrBody(e, ctx) {
|
|
2547
2578
|
const slug = e.slug || explorationSlug(e.name);
|
|
@@ -2551,44 +2582,78 @@ function explorationPrBody(e, ctx) {
|
|
|
2551
2582
|
const lines = [
|
|
2552
2583
|
`## Exploration: ${e.name.trim()}`,
|
|
2553
2584
|
"",
|
|
2554
|
-
`> Not mergeable as is. This draft pull request is a
|
|
2555
|
-
"",
|
|
2556
|
-
`**Purpose.** ${e.purpose.trim()}`,
|
|
2557
|
-
...uc ? ["", `**Use case.** ${uc}`] : [],
|
|
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.`,
|
|
2558
2586
|
"",
|
|
2587
|
+
...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
|
|
2588
|
+
...uc ? [`**Use case.** ${uc}`, ""] : [],
|
|
2559
2589
|
"### What is experimental",
|
|
2560
2590
|
"",
|
|
2561
|
-
...experimental.length ? experimental.map((x) => `- ${x}`) : ["- Token edits and component work committed to this branch. Nothing here reaches main or production."],
|
|
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."],
|
|
2562
2592
|
...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
|
|
2563
|
-
"",
|
|
2564
|
-
"### Try it in a product staging branch",
|
|
2565
2593
|
""
|
|
2566
2594
|
];
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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).");
|
|
2620
|
+
}
|
|
2574
2621
|
lines.push(
|
|
2575
2622
|
"",
|
|
2576
2623
|
"### When it is done",
|
|
2577
2624
|
"",
|
|
2578
|
-
`- **
|
|
2579
|
-
|
|
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."}`,
|
|
2580
2627
|
"",
|
|
2581
|
-
|
|
2628
|
+
explorationBlock({ ...e, slug })
|
|
2582
2629
|
);
|
|
2583
2630
|
return lines.join("\n");
|
|
2584
2631
|
}
|
|
2632
|
+
function explorationBlock(e) {
|
|
2633
|
+
const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
|
|
2634
|
+
const data = {
|
|
2635
|
+
name: e.name.trim(),
|
|
2636
|
+
purpose: (e.purpose ?? "").trim(),
|
|
2637
|
+
slug: e.slug || explorationSlug(e.name),
|
|
2638
|
+
...e.useCase ? { useCase: e.useCase } : {},
|
|
2639
|
+
...experimental.length ? { experimental } : {},
|
|
2640
|
+
...e.author ? { author: e.author } : {},
|
|
2641
|
+
...e.startedAt ? { startedAt: e.startedAt } : {},
|
|
2642
|
+
...e.source ? { source: e.source } : {},
|
|
2643
|
+
...e.agent ? { agent: e.agent } : {},
|
|
2644
|
+
...e.doneAt ? { doneAt: e.doneAt } : {},
|
|
2645
|
+
...e.doneSha ? { doneSha: e.doneSha } : {}
|
|
2646
|
+
};
|
|
2647
|
+
return `<!-- ${MARK$2} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
2648
|
+
}
|
|
2585
2649
|
function parseExplorationBody(body) {
|
|
2586
2650
|
if (!body) return void 0;
|
|
2587
2651
|
const m = new RegExp(`<!-- ${MARK$2} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
2588
2652
|
if (!m) return void 0;
|
|
2589
2653
|
try {
|
|
2590
2654
|
const e = JSON.parse(m[1]);
|
|
2591
|
-
|
|
2655
|
+
if (!e || typeof e.name !== "string") return void 0;
|
|
2656
|
+
return typeof e.purpose === "string" ? e : e.purpose === void 0 ? { ...e, purpose: "" } : void 0;
|
|
2592
2657
|
} catch {
|
|
2593
2658
|
return void 0;
|
|
2594
2659
|
}
|
|
@@ -2597,13 +2662,84 @@ const labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.n
|
|
|
2597
2662
|
function isExplorationPull(p) {
|
|
2598
2663
|
return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
|
|
2599
2664
|
}
|
|
2665
|
+
function isHandedOff(p) {
|
|
2666
|
+
return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
|
|
2667
|
+
}
|
|
2600
2668
|
function explorationStatus(p) {
|
|
2601
2669
|
const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
|
|
2602
2670
|
if (p.state !== "open" && !merged) return "closed";
|
|
2603
2671
|
if (p.state === "open" && labelNames(p.labels).includes(EXPLORATION_LABEL)) return "active";
|
|
2604
|
-
return "promoted";
|
|
2672
|
+
if (!isHandedOff(p)) return "promoted";
|
|
2673
|
+
if (merged) return "shipped";
|
|
2674
|
+
if (p.draft === false) return "building";
|
|
2675
|
+
const doneSha = parseExplorationBody(p.body)?.doneSha;
|
|
2676
|
+
if (doneSha && p.headSha && p.headSha !== doneSha) return "building";
|
|
2677
|
+
return "ready";
|
|
2605
2678
|
}
|
|
2606
2679
|
const isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
|
|
2680
|
+
const isArchivedExploration = (labels) => labelNames(labels ?? []).includes(EXPLORATION_ARCHIVED_LABEL);
|
|
2681
|
+
function isHeldExploration(p) {
|
|
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;
|
|
2686
|
+
}
|
|
2687
|
+
const explorationTakeCommands = (branch) => ["git fetch origin", `git checkout ${branch}`];
|
|
2688
|
+
const CONTINUE = "Continue on this branch: its pull request already exists, so Bridge follows your pushes, and the merge reads as shipped.";
|
|
2689
|
+
const EXPLORATION_STARTING_POINT = `A starting point, not ready to ship: nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
|
|
2690
|
+
function explorationStartingPoint(checks) {
|
|
2691
|
+
switch (checks) {
|
|
2692
|
+
case "success":
|
|
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;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function doneExploringPlan(input) {
|
|
2705
|
+
const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
|
|
2706
|
+
return {
|
|
2707
|
+
removeLabels: [EXPLORATION_LABEL],
|
|
2708
|
+
addLabels: [EXPLORATION_READY_LABEL],
|
|
2709
|
+
removeFiles,
|
|
2710
|
+
body: withDoneExploring(input.body, { branch: input.branch, doneAt: (input.date ?? /* @__PURE__ */ new Date()).toISOString() }),
|
|
2711
|
+
take: explorationTakeCommands(input.branch),
|
|
2712
|
+
markReady: false
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
const DEV_SECTION = "### For developers";
|
|
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();
|
|
2742
|
+
}
|
|
2607
2743
|
function promotionPlan(input) {
|
|
2608
2744
|
const exploration = parseExplorationBody(input.body);
|
|
2609
2745
|
const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
|
|
@@ -2655,8 +2791,8 @@ function explorationShareMessage(input) {
|
|
|
2655
2791
|
function explorationRules(branch) {
|
|
2656
2792
|
return [
|
|
2657
2793
|
`- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
|
|
2658
|
-
`- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It
|
|
2659
|
-
|
|
2794
|
+
`- 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.`,
|
|
2795
|
+
`- 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.`
|
|
2660
2796
|
];
|
|
2661
2797
|
}
|
|
2662
2798
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2702,7 +2838,8 @@ function requestIssueTitle(r) {
|
|
|
2702
2838
|
return subjectTitle(r.component, r.title);
|
|
2703
2839
|
}
|
|
2704
2840
|
function composeRequestIssue(r, opts = {}) {
|
|
2705
|
-
const
|
|
2841
|
+
const now = r.changes.some((c) => c.before?.trim());
|
|
2842
|
+
const rows = !r.changes.length ? [] : now ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r.changes.map((c) => `| ${cell$1(c.field)} | ${cell$1(c.before ?? "")} | ${cell$1(c.after ?? "")} |`)] : ["| What | Requested |", "| --- | --- |", ...r.changes.map((c) => `| ${cell$1(c.field)} | ${cell$1(c.after ?? "")} |`)];
|
|
2706
2843
|
const figma = r.figma?.url ? `[${r.figma.nodeName ?? "Open in Figma"}](${r.figma.url})` : r.figma?.nodeName ? r.figma.nodeName : "";
|
|
2707
2844
|
const body = [
|
|
2708
2845
|
r.kind === "new-component" ? `## New component request: ${r.component}` : `## ${kindLabel$1(r.kind)} request for ${r.component}`,
|
|
@@ -2714,10 +2851,10 @@ function composeRequestIssue(r, opts = {}) {
|
|
|
2714
2851
|
...r.images?.length ? [...r.images.map((u, i) => ``), ""] : [],
|
|
2715
2852
|
...figma ? [`**Design:** ${figma}`, ""] : [],
|
|
2716
2853
|
...opts.componentUrl ? [`**Component:** ${opts.componentUrl}`, ""] : [],
|
|
2717
|
-
...r.exploration ? [`**Exploration:** \`${r.exploration}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot and
|
|
2854
|
+
...r.exploration ? [`**Exploration:** \`${r.exploration}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot, and developers take the branch once the designer is done exploring.`, ""] : [],
|
|
2718
2855
|
"## How to implement",
|
|
2719
2856
|
"",
|
|
2720
|
-
r.exploration ? `Commit the change to \`${r.exploration}\` (its draft pull request is the place). With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool.
|
|
2857
|
+
r.exploration ? `Commit the change to \`${r.exploration}\` (its draft pull request is the place). With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool. The request closes when developers ship that branch.` : `Code is the source of truth: change the component in a pull request that says \`Closes #<this issue>\`, so this request closes when it merges. With Claude: \`bridge request implement <number>\` (CLI) or the \`bridge_get_request\` MCP tool.`,
|
|
2721
2858
|
"",
|
|
2722
2859
|
`<sub>Requested from ${sourceLabel$1(r.source)}${r.requestedBy ? ` by @${r.requestedBy}` : ""} with Bridge.</sub>`,
|
|
2723
2860
|
"",
|
|
@@ -2742,7 +2879,7 @@ function withRequestExploration(body, branch) {
|
|
|
2742
2879
|
if (!r || !body) return void 0;
|
|
2743
2880
|
const block = new RegExp(`<!-- ${MARK$1} \\{[\\s\\S]*?\\} -->`);
|
|
2744
2881
|
let out = body.replace(block, () => `<!-- ${MARK$1} ${JSON.stringify({ ...r, exploration: branch })} -->`);
|
|
2745
|
-
const line = `**Exploration:** \`${branch}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot and
|
|
2882
|
+
const line = `**Exploration:** \`${branch}\`. Implement it on that branch, not in a pull request to main: it ships to product staging as a snapshot, and developers take the branch once the designer is done exploring.`;
|
|
2746
2883
|
if (/^\*\*Exploration:\*\* .*$/m.test(out)) out = out.replace(/^\*\*Exploration:\*\* .*$/m, () => line);
|
|
2747
2884
|
else if (out.includes("\n## How to implement")) out = out.replace("\n## How to implement", () => `
|
|
2748
2885
|
${line}
|
|
@@ -2799,7 +2936,7 @@ function requestBrief(r, ctx) {
|
|
|
2799
2936
|
"Rules:",
|
|
2800
2937
|
`- Follow ${ctx.agentsFile ?? "AGENTS.md"}: tokens only in component CSS, Base UI for behavior, states, tests, story, docs.json, changeset.`,
|
|
2801
2938
|
"- A new component, prop or variant is Tier 2: open the pull request as a draft (new component: follow the new component checklist). A token add or rename is Tier 3: describe it, do not invent values.",
|
|
2802
|
-
...exploration ? [`- This request is part of the exploration ${exploration}.`, ...explorationRules(exploration), `- Reference the request in the commit message ("Refs #${ctx.issue}").
|
|
2939
|
+
...exploration ? [`- This request is part of the exploration ${exploration}.`, ...explorationRules(exploration), `- Reference the request in the commit message ("Refs #${ctx.issue}"). Shipping the exploration closes it.`] : [`- Work on a branch named agent/request-${ctx.issue}. The pull request body must contain "Closes #${ctx.issue}".`],
|
|
2803
2940
|
"- If the request is unclear or conflicts with the guidance, comment on the issue instead of guessing."
|
|
2804
2941
|
].join("\n");
|
|
2805
2942
|
}
|
|
@@ -2832,37 +2969,6 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
2832
2969
|
}
|
|
2833
2970
|
};
|
|
2834
2971
|
}
|
|
2835
|
-
async function linkRequestToExplorationOnGitHub(gh, n, branch, opts = {}) {
|
|
2836
|
-
if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
|
|
2837
|
-
const issue = await gh.getIssue(n);
|
|
2838
|
-
const body = withRequestExploration(issue.body, branch);
|
|
2839
|
-
if (!body) throw new GitHubError(422, `#${n} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
|
|
2840
|
-
await gh.updateIssue(n, { body });
|
|
2841
|
-
const warnings = [];
|
|
2842
|
-
if (!await gh.addLabels(n, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n}.`);
|
|
2843
|
-
let assignees = [];
|
|
2844
|
-
if (opts.assignee) {
|
|
2845
|
-
try {
|
|
2846
|
-
assignees = await gh.addAssignees(n, [opts.assignee.replace(/^@/, "")]);
|
|
2847
|
-
} catch {
|
|
2848
|
-
warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n}.`);
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
return { number: n, branch, assignees, warnings };
|
|
2852
|
-
}
|
|
2853
|
-
async function assignRequestOnGitHub(gh, n, login) {
|
|
2854
|
-
const who = login.trim().replace(/^@/, "");
|
|
2855
|
-
if (!who) throw new GitHubError(422, "Pick who should work on it.");
|
|
2856
|
-
const assignees = await gh.addAssignees(n, [who]);
|
|
2857
|
-
if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
|
|
2858
|
-
return assignees;
|
|
2859
|
-
}
|
|
2860
|
-
async function declineRequestOnGitHub(gh, n, reason, by) {
|
|
2861
|
-
const problems = declineReasonProblems(reason);
|
|
2862
|
-
if (problems.length) throw new GitHubError(422, problems.join(" "));
|
|
2863
|
-
await gh.addComment(n, declineComment(reason, by));
|
|
2864
|
-
await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
|
|
2865
|
-
}
|
|
2866
2972
|
const COMPONENT_SPECS_VERSION = 1;
|
|
2867
2973
|
const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
|
|
2868
2974
|
const SPEC_FIELDS = [
|
|
@@ -3020,6 +3126,23 @@ function hashSpec(spec) {
|
|
|
3020
3126
|
}
|
|
3021
3127
|
const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
3022
3128
|
const variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
3129
|
+
function matchVariant(spec, props) {
|
|
3130
|
+
if (!spec) return void 0;
|
|
3131
|
+
const key = variantKeyOf(props);
|
|
3132
|
+
const exact = spec.variants.find((v) => variantKeyOf(v.props) === key);
|
|
3133
|
+
if (exact) return exact;
|
|
3134
|
+
const asked = Object.entries(props);
|
|
3135
|
+
const fits = spec.variants.filter((v) => Object.keys(v.props).length > asked.length && asked.every(([k, val]) => v.props[k] === val));
|
|
3136
|
+
if (fits.length <= 1) return fits[0];
|
|
3137
|
+
const atDefault = (name, value) => {
|
|
3138
|
+
const axis = spec.axes?.find((a) => a.name === name);
|
|
3139
|
+
if (axis) return value === axis.default;
|
|
3140
|
+
const v = value.toLowerCase();
|
|
3141
|
+
return v === "false" || v === "default";
|
|
3142
|
+
};
|
|
3143
|
+
const best = fits.filter((v) => Object.entries(v.props).every(([k, val]) => k in props || atDefault(k, val)));
|
|
3144
|
+
return best.length === 1 ? best[0] : void 0;
|
|
3145
|
+
}
|
|
3023
3146
|
const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
|
|
3024
3147
|
function componentMotion(spec) {
|
|
3025
3148
|
const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
|
|
@@ -3082,569 +3205,163 @@ function figmaCell(entry) {
|
|
|
3082
3205
|
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
3083
3206
|
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
3084
3207
|
}
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
if (!body) return void 0;
|
|
3093
|
-
let missing = false;
|
|
3094
|
-
const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
|
|
3095
|
-
const v2 = resolve(path);
|
|
3096
|
-
const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
|
|
3097
|
-
if (n === void 0 || Number.isNaN(n)) missing = true;
|
|
3098
|
-
return String(n);
|
|
3099
|
-
});
|
|
3100
|
-
if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
|
|
3101
|
-
const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
|
|
3102
|
-
let i = 0;
|
|
3103
|
-
const factor = () => {
|
|
3104
|
-
const t = tokens[i++];
|
|
3105
|
-
if (t === "(") {
|
|
3106
|
-
const v2 = sum();
|
|
3107
|
-
i++;
|
|
3108
|
-
return v2;
|
|
3109
|
-
}
|
|
3110
|
-
if (t === "-") return -factor();
|
|
3111
|
-
return Number(t);
|
|
3112
|
-
};
|
|
3113
|
-
const product = () => {
|
|
3114
|
-
let v2 = factor();
|
|
3115
|
-
while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
|
|
3116
|
-
return v2;
|
|
3117
|
-
};
|
|
3118
|
-
const sum = () => {
|
|
3119
|
-
let v2 = product();
|
|
3120
|
-
while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
|
|
3121
|
-
return v2;
|
|
3122
|
-
};
|
|
3123
|
-
const v = sum();
|
|
3124
|
-
return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
|
|
3208
|
+
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3209
|
+
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3210
|
+
const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
3211
|
+
function rawIssue(label, r, values2) {
|
|
3212
|
+
const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
|
|
3213
|
+
const todo = !matches.length ? "No variable has that value: bind it to one so it follows the library, or create the variable and send it with the variables." : matches.length === 1 ? `${matches[0]} has that value: bind it so it follows the library.` : `${and(matches)} have that value: bind one so it follows the library.`;
|
|
3214
|
+
return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
|
|
3125
3215
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
const
|
|
3129
|
-
const
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
const
|
|
3133
|
-
|
|
3134
|
-
|
|
3216
|
+
function planComponentPush(d, ctx) {
|
|
3217
|
+
const issues = [...d.unknown];
|
|
3218
|
+
const reasons = [];
|
|
3219
|
+
const edits = /* @__PURE__ */ new Map();
|
|
3220
|
+
const also = /* @__PURE__ */ new Set();
|
|
3221
|
+
for (const r of d.rebinds) {
|
|
3222
|
+
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3223
|
+
if (r.raw?.length) {
|
|
3224
|
+
issues.push(rawIssue(label, r, r.raw));
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
if (!r.after.length) {
|
|
3228
|
+
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and send it with the variables, then scan again." });
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3232
|
+
if (missing.length) {
|
|
3233
|
+
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Send the variable first (Variables, above), then scan again." });
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
if (!r.before.length) {
|
|
3237
|
+
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
|
|
3238
|
+
continue;
|
|
3239
|
+
}
|
|
3240
|
+
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3241
|
+
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
const [from, to] = [r.before[0], r.after[0]];
|
|
3245
|
+
if (ctx.tierOf(from) !== "component") {
|
|
3246
|
+
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3250
|
+
if (elsewhere.variants.length) {
|
|
3251
|
+
const v = elsewhere.variants;
|
|
3252
|
+
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3253
|
+
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
for (const c of elsewhere.components) also.add(c);
|
|
3257
|
+
const prev = edits.get(from);
|
|
3258
|
+
if (prev && prev.value !== `{${to}}`) {
|
|
3259
|
+
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
|
|
3260
|
+
edits.delete(from);
|
|
3261
|
+
continue;
|
|
3262
|
+
}
|
|
3263
|
+
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3135
3264
|
}
|
|
3136
|
-
const
|
|
3137
|
-
if (
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3265
|
+
const list = [...edits.values()];
|
|
3266
|
+
if (d.needsCode || reasons.length) {
|
|
3267
|
+
return {
|
|
3268
|
+
route: "claude-draft",
|
|
3269
|
+
said: `A developer builds it with Claude from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3270
|
+
edits: list,
|
|
3271
|
+
reasons,
|
|
3272
|
+
issues,
|
|
3273
|
+
alsoChanges: [...also]
|
|
3274
|
+
};
|
|
3142
3275
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
const
|
|
3146
|
-
const
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
const added = /* @__PURE__ */ new Map();
|
|
3150
|
-
for (const p of parts) {
|
|
3151
|
-
if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
|
|
3152
|
-
const kids = parts.filter((c) => c.parent === p.id);
|
|
3153
|
-
if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
|
|
3276
|
+
if (list.length) {
|
|
3277
|
+
const n = list.length;
|
|
3278
|
+
const others = [...also];
|
|
3279
|
+
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3280
|
+
const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
3281
|
+
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
3154
3282
|
}
|
|
3155
|
-
|
|
3156
|
-
const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
|
|
3157
|
-
return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
|
|
3283
|
+
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
3158
3284
|
}
|
|
3159
|
-
function
|
|
3160
|
-
const
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
seen.add(p);
|
|
3173
|
-
out.push(p);
|
|
3285
|
+
function specsUsage(specs, component) {
|
|
3286
|
+
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3287
|
+
return (path, r) => {
|
|
3288
|
+
const variants = [];
|
|
3289
|
+
const components = [];
|
|
3290
|
+
for (const c of specs.components) {
|
|
3291
|
+
for (const v of c.variants) {
|
|
3292
|
+
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3293
|
+
const key = variantKeyOf(v.props);
|
|
3294
|
+
if (c === own) {
|
|
3295
|
+
if (!r.variants.includes(key)) variants.push(key);
|
|
3296
|
+
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3297
|
+
}
|
|
3174
3298
|
}
|
|
3175
|
-
return
|
|
3299
|
+
return { variants, components };
|
|
3176
3300
|
};
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3301
|
+
}
|
|
3302
|
+
function componentContrastPairs(edits, pairs2, tokens) {
|
|
3303
|
+
const out = /* @__PURE__ */ new Map();
|
|
3304
|
+
for (const e of edits) {
|
|
3305
|
+
const target = tokens.aliasOf(e.path);
|
|
3306
|
+
if (!target) continue;
|
|
3307
|
+
const group = e.path.split(".")[0];
|
|
3308
|
+
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3309
|
+
for (const p of pairs2) {
|
|
3310
|
+
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3311
|
+
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3182
3312
|
}
|
|
3183
|
-
return void 0;
|
|
3184
|
-
};
|
|
3185
|
-
const best = (p, cands) => {
|
|
3186
|
-
if (!cands.length) return void 0;
|
|
3187
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3188
|
-
if (parent) {
|
|
3189
|
-
const direct = cands.find((c) => c.parent === parent);
|
|
3190
|
-
if (direct) return direct;
|
|
3191
|
-
}
|
|
3192
|
-
const anc = pairedAncestor(p);
|
|
3193
|
-
if (anc) {
|
|
3194
|
-
const under2 = cands.find((c) => ancestors(c.id).includes(anc));
|
|
3195
|
-
if (under2) return under2;
|
|
3196
|
-
}
|
|
3197
|
-
return cands[0];
|
|
3198
|
-
};
|
|
3199
|
-
const root = staticParts.find((p) => !p.parent);
|
|
3200
|
-
if (root) take(root.id, "root");
|
|
3201
|
-
const textOf = (p) => {
|
|
3202
|
-
if (p.characters) return normText(p.characters);
|
|
3203
|
-
const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
|
|
3204
|
-
return typeof prop?.default === "string" ? normText(prop.default) : "";
|
|
3205
|
-
};
|
|
3206
|
-
const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
|
|
3207
|
-
const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
|
|
3208
|
-
const shared = (p, m) => {
|
|
3209
|
-
const mine = new Set(classesOf(p));
|
|
3210
|
-
return mClasses(m).filter((c) => mine.has(c)).length;
|
|
3211
|
-
};
|
|
3212
|
-
const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
|
|
3213
|
-
const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
|
|
3214
|
-
for (const p of staticParts) {
|
|
3215
|
-
if (p.kind !== "text" || pairs2.has(p.id)) continue;
|
|
3216
|
-
const t = textOf(p);
|
|
3217
|
-
if (!t) continue;
|
|
3218
|
-
const hit = best(p, measured.parts.filter((m) => m.kind === "text" && !taken.has(m.id) && normText(m.characters) === t && !(classesOf(p).length && mClasses(m).length && !shared(p, m))));
|
|
3219
|
-
if (hit) take(p.id, hit.id);
|
|
3220
|
-
}
|
|
3221
|
-
const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
|
|
3222
|
-
for (const p of staticParts) {
|
|
3223
|
-
if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
|
|
3224
|
-
const sp = sParts.get(p.parent);
|
|
3225
|
-
if (!sp || sp.kind !== "frame" || !sp.parent) continue;
|
|
3226
|
-
const cls = classOf(sp);
|
|
3227
|
-
const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
|
|
3228
|
-
if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
|
|
3229
|
-
}
|
|
3230
|
-
for (const p of staticParts) {
|
|
3231
|
-
if (pairs2.has(p.id) || !classesOf(p).length) continue;
|
|
3232
|
-
if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
|
|
3233
|
-
const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
|
|
3234
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3235
|
-
const under2 = parent ? all.filter((m) => m.parent === parent) : [];
|
|
3236
|
-
const cands = under2.length ? under2 : all;
|
|
3237
|
-
const most = Math.max(0, ...cands.map((m) => shared(p, m)));
|
|
3238
|
-
const hit = best(p, cands.filter((m) => shared(p, m) === most));
|
|
3239
|
-
if (hit) take(p.id, hit.id);
|
|
3240
|
-
}
|
|
3241
|
-
for (const p of staticParts) {
|
|
3242
|
-
if (pairs2.has(p.id)) continue;
|
|
3243
|
-
if (p.parent && !pairs2.has(p.parent)) continue;
|
|
3244
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3245
|
-
const known = p.kind === "text" && !!textOf(p);
|
|
3246
|
-
const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
|
|
3247
|
-
const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
|
|
3248
|
-
if (hit) take(p.id, hit.id);
|
|
3249
|
-
}
|
|
3250
|
-
for (const p of staticParts) {
|
|
3251
|
-
if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
|
|
3252
|
-
const mp = pairs2.get(p.parent);
|
|
3253
|
-
const t = textOf(p);
|
|
3254
|
-
if (mp && mParts.get(mp)?.kind === "text" && staticParts.filter((x) => x.parent === p.parent).length === 1 && (!t || normText(mParts.get(mp).characters) === t)) pairs2.set(p.id, mp);
|
|
3255
3313
|
}
|
|
3256
|
-
return
|
|
3314
|
+
return [...out.values()];
|
|
3257
3315
|
}
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
const
|
|
3270
|
-
return typeof
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
const mid = pairs2.get(sp.id);
|
|
3274
|
-
if (!mid) {
|
|
3275
|
-
out.unmatchedParts.push(sp.id);
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
const sv = staticValues[sp.id] ?? {};
|
|
3279
|
-
const mv = mid ? measured.values[mid] ?? {} : {};
|
|
3280
|
-
const mr = mid ? measured.raw[mid] ?? {} : {};
|
|
3281
|
-
const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
|
|
3282
|
-
for (const f of fields) {
|
|
3283
|
-
if ((f === "width" || f === "height") && sv[f] === void 0) continue;
|
|
3284
|
-
const s = sv[f];
|
|
3285
|
-
const m = mv[f] !== void 0 ? mv[f] : mr[f];
|
|
3286
|
-
if (s === void 0) {
|
|
3287
|
-
if (m !== void 0 && m !== 0) out.measuredOnly++;
|
|
3288
|
-
continue;
|
|
3289
|
-
}
|
|
3290
|
-
if (m === void 0 && !(f in mr)) {
|
|
3291
|
-
if (s === 0 || s === null) continue;
|
|
3292
|
-
const r = typeof s === "string" && resolve ? resolve(s) : s;
|
|
3293
|
-
if (r === 0 || r === "0px" || r === null) continue;
|
|
3294
|
-
out.staticOnly++;
|
|
3295
|
-
out.diffs.push({ part: sp.id, field: f, static: s, same: false });
|
|
3296
|
-
continue;
|
|
3297
|
-
}
|
|
3298
|
-
out.compared++;
|
|
3299
|
-
let same2 = s === m;
|
|
3300
|
-
if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
|
|
3301
|
-
if (same2 && typeof s === "string") out.sameToken++;
|
|
3302
|
-
if (!same2 && resolve) {
|
|
3303
|
-
let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
|
|
3304
|
-
const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
|
|
3305
|
-
if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
|
|
3306
|
-
same2 = near(rs, mr[f]);
|
|
3307
|
-
}
|
|
3308
|
-
if (same2) out.same++;
|
|
3309
|
-
else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
|
|
3310
|
-
}
|
|
3316
|
+
const FROM_FIGMA_LABEL = "from-figma";
|
|
3317
|
+
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3318
|
+
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3319
|
+
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3320
|
+
const ORIGIN_MARK = "bridge-from-figma";
|
|
3321
|
+
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3322
|
+
function parseFigmaOrigin(body) {
|
|
3323
|
+
if (!body) return void 0;
|
|
3324
|
+
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3325
|
+
if (!m) return void 0;
|
|
3326
|
+
try {
|
|
3327
|
+
const o = JSON.parse(m[1]);
|
|
3328
|
+
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3329
|
+
} catch {
|
|
3330
|
+
return void 0;
|
|
3311
3331
|
}
|
|
3312
|
-
const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
|
|
3313
|
-
const box = measured.boxes.root;
|
|
3314
|
-
if (box) out.rootSize = { static: { width: typeof root.width === "number" ? root.width : void 0, height: typeof root.height === "number" ? root.height : void 0 }, measured: { width: box.width, height: box.height } };
|
|
3315
|
-
return out;
|
|
3316
3332
|
}
|
|
3317
|
-
function
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3333
|
+
function figmaOriginSection(o) {
|
|
3334
|
+
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3335
|
+
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3336
|
+
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted with Claude** from the Figma spec. A person reviews it before it ships." : `**Waiting for a developer:** this branch carries the spec only, as a request. A developer builds the code here with Claude (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3337
|
+
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the Bridge Figma plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3321
3338
|
}
|
|
3322
|
-
function
|
|
3323
|
-
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3339
|
+
function withFigmaOrigin(body, o) {
|
|
3340
|
+
let b = (body ?? "").trim();
|
|
3341
|
+
const at = b.indexOf("**From Figma:**");
|
|
3342
|
+
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3343
|
+
if (at >= 0 && mark > at) {
|
|
3344
|
+
const end = b.indexOf("-->", mark);
|
|
3345
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3328
3346
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
}
|
|
3332
|
-
function markUsedTokens(tokens, stories) {
|
|
3333
|
-
const byName = new Map(tokens.properties.map((p) => [p.name, p]));
|
|
3334
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3335
|
-
const use = (name) => {
|
|
3336
|
-
if (seen.has(name)) return;
|
|
3337
|
-
seen.add(name);
|
|
3338
|
-
const p = byName.get(name);
|
|
3339
|
-
if (!p) return;
|
|
3340
|
-
p.used = true;
|
|
3341
|
-
for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
|
|
3342
|
-
if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
|
|
3343
|
-
};
|
|
3344
|
-
for (const s of stories)
|
|
3345
|
-
for (const spec of [s.spec, ...s.instances ?? []])
|
|
3346
|
-
for (const part of Object.values(spec?.tokens ?? {}))
|
|
3347
|
-
for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
|
|
3348
|
-
const used = tokens.properties.filter((p) => p.used).length;
|
|
3349
|
-
tokens.counts.used = used;
|
|
3350
|
-
return used;
|
|
3347
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
3348
|
+
|
|
3349
|
+
${b}` : ""}`;
|
|
3351
3350
|
}
|
|
3352
|
-
|
|
3351
|
+
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3352
|
+
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3353
|
+
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3354
|
+
function formatFigmaSpec(d, plan) {
|
|
3355
|
+
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3356
|
+
`;
|
|
3353
3357
|
}
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
|
|
3365
|
-
if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
|
|
3366
|
-
if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
|
|
3367
|
-
if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
|
|
3368
|
-
data.stories.forEach((s, i) => {
|
|
3369
|
-
if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
|
|
3370
|
-
const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
|
|
3371
|
-
for (const sp of specs) {
|
|
3372
|
-
if (!isObj$1(sp) || !Array.isArray(sp.parts) || !isObj$1(sp.values)) throw new MeasuredSpecsError(`measured specs: story ${s.id} has a spec without parts and values`);
|
|
3373
|
-
for (const p of sp.parts) if (!isObj$1(p) || typeof p.id !== "string" || typeof p.kind !== "string") throw new MeasuredSpecsError(`measured specs: story ${s.id} has a part without id or kind`);
|
|
3374
|
-
}
|
|
3375
|
-
if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
|
|
3376
|
-
});
|
|
3377
|
-
if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
|
|
3378
|
-
if (data.tokens !== void 0) {
|
|
3379
|
-
const t = data.tokens;
|
|
3380
|
-
if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
|
|
3381
|
-
for (const p of t.properties) if (!isObj$1(p) || typeof p.name !== "string" || typeof p.path !== "string" || p.tier !== "primitive" && p.tier !== "semantic") throw new MeasuredSpecsError("measured specs: a token has no name, path or tier");
|
|
3382
|
-
}
|
|
3383
|
-
return data;
|
|
3384
|
-
}
|
|
3385
|
-
const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
|
|
3386
|
-
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
3387
|
-
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
3388
|
-
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
3389
|
-
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
3390
|
-
];
|
|
3391
|
-
const D50_TO_D65 = [
|
|
3392
|
-
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
3393
|
-
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
3394
|
-
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
3395
|
-
];
|
|
3396
|
-
const LINEAR_P3_TO_XYZ_D65 = [
|
|
3397
|
-
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
3398
|
-
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
3399
|
-
[0, 0.04511338185890264, 1.043944368900976]
|
|
3400
|
-
];
|
|
3401
|
-
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
3402
|
-
const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
|
|
3403
|
-
const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
|
|
3404
|
-
function oklabToLinearSrgb(L, a, b) {
|
|
3405
|
-
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
3406
|
-
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
3407
|
-
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
3408
|
-
return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
3409
|
-
}
|
|
3410
|
-
function labToXyzD50(L, a, b) {
|
|
3411
|
-
const e = 216 / 24389;
|
|
3412
|
-
const k = 24389 / 27;
|
|
3413
|
-
const fy = (L + 16) / 116;
|
|
3414
|
-
const fx = a / 500 + fy;
|
|
3415
|
-
const fz = fy - b / 200;
|
|
3416
|
-
const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
3417
|
-
const y = L > k * e ? fy ** 3 : L / k;
|
|
3418
|
-
const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
3419
|
-
return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
|
|
3420
|
-
}
|
|
3421
|
-
function num(s, full = 1) {
|
|
3422
|
-
if (s === void 0 || s === "none") return 0;
|
|
3423
|
-
if (s.endsWith("%")) return parseFloat(s) / 100 * full;
|
|
3424
|
-
if (/deg$/.test(s)) return parseFloat(s);
|
|
3425
|
-
if (/turn$/.test(s)) return parseFloat(s) * 360;
|
|
3426
|
-
if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
|
|
3427
|
-
return parseFloat(s);
|
|
3428
|
-
}
|
|
3429
|
-
function linearSrgb(fn, args) {
|
|
3430
|
-
switch (fn) {
|
|
3431
|
-
case "oklab":
|
|
3432
|
-
return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
|
|
3433
|
-
case "oklch": {
|
|
3434
|
-
const c = num(args[1], 0.4);
|
|
3435
|
-
const h = num(args[2]) * Math.PI / 180;
|
|
3436
|
-
return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
|
|
3437
|
-
}
|
|
3438
|
-
case "lab":
|
|
3439
|
-
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
|
|
3440
|
-
case "lch": {
|
|
3441
|
-
const c = num(args[1], 150);
|
|
3442
|
-
const h = num(args[2]) * Math.PI / 180;
|
|
3443
|
-
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
|
|
3444
|
-
}
|
|
3445
|
-
case "color": {
|
|
3446
|
-
const [space, ...c] = args;
|
|
3447
|
-
const v = [num(c[0]), num(c[1]), num(c[2])];
|
|
3448
|
-
if (space === "srgb") return v.map(toLinear);
|
|
3449
|
-
if (space === "srgb-linear") return v;
|
|
3450
|
-
if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
|
|
3451
|
-
if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
|
|
3452
|
-
if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
|
|
3453
|
-
return void 0;
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
return void 0;
|
|
3457
|
-
}
|
|
3458
|
-
const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
|
|
3459
|
-
function cssColorToRgb(fn, inner) {
|
|
3460
|
-
const [main, alpha] = inner.split("/");
|
|
3461
|
-
const args = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
3462
|
-
const lin = linearSrgb(fn.toLowerCase(), args);
|
|
3463
|
-
if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
|
|
3464
|
-
const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
|
|
3465
|
-
const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
|
|
3466
|
-
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
|
|
3467
|
-
}
|
|
3468
|
-
function cssColorsToRgb(text) {
|
|
3469
|
-
if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
|
|
3470
|
-
return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
|
|
3471
|
-
}
|
|
3472
|
-
function withSrgbColors(value) {
|
|
3473
|
-
if (typeof value === "string") return cssColorsToRgb(value);
|
|
3474
|
-
if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
|
|
3475
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
|
|
3476
|
-
return value;
|
|
3477
|
-
}
|
|
3478
|
-
function parseBoxShadow(css) {
|
|
3479
|
-
if (!css || css.trim() === "none") return [];
|
|
3480
|
-
const out = [];
|
|
3481
|
-
for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
|
|
3482
|
-
const color = /rgba?\(([^)]*)\)/i.exec(one);
|
|
3483
|
-
const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
|
|
3484
|
-
const a = c[3] ?? 1;
|
|
3485
|
-
const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
|
|
3486
|
-
if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
|
|
3487
|
-
out.push({ inset: /\binset\b/i.test(one), x: lengths[0], y: lengths[1], blur: lengths[2] ?? 0, spread: lengths[3] ?? 0, color: { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a } });
|
|
3488
|
-
}
|
|
3489
|
-
return out;
|
|
3490
|
-
}
|
|
3491
|
-
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3492
|
-
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3493
|
-
const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
3494
|
-
function rawIssue(label, r, values2) {
|
|
3495
|
-
const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
|
|
3496
|
-
const todo = !matches.length ? "No variable has that value: bind it to one so it follows the library, or create the variable and send it with the variables." : matches.length === 1 ? `${matches[0]} has that value: bind it so it follows the library.` : `${and(matches)} have that value: bind one so it follows the library.`;
|
|
3497
|
-
return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
|
|
3498
|
-
}
|
|
3499
|
-
function planComponentPush(d, ctx) {
|
|
3500
|
-
const issues = [...d.unknown];
|
|
3501
|
-
const reasons = [];
|
|
3502
|
-
const edits = /* @__PURE__ */ new Map();
|
|
3503
|
-
const also = /* @__PURE__ */ new Set();
|
|
3504
|
-
for (const r of d.rebinds) {
|
|
3505
|
-
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3506
|
-
if (r.raw?.length) {
|
|
3507
|
-
issues.push(rawIssue(label, r, r.raw));
|
|
3508
|
-
continue;
|
|
3509
|
-
}
|
|
3510
|
-
if (!r.after.length) {
|
|
3511
|
-
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and send it with the variables, then scan again." });
|
|
3512
|
-
continue;
|
|
3513
|
-
}
|
|
3514
|
-
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3515
|
-
if (missing.length) {
|
|
3516
|
-
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Send the variable first (Variables, above), then scan again." });
|
|
3517
|
-
continue;
|
|
3518
|
-
}
|
|
3519
|
-
if (!r.before.length) {
|
|
3520
|
-
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
|
|
3521
|
-
continue;
|
|
3522
|
-
}
|
|
3523
|
-
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3524
|
-
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
|
|
3525
|
-
continue;
|
|
3526
|
-
}
|
|
3527
|
-
const [from, to] = [r.before[0], r.after[0]];
|
|
3528
|
-
if (ctx.tierOf(from) !== "component") {
|
|
3529
|
-
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
|
|
3530
|
-
continue;
|
|
3531
|
-
}
|
|
3532
|
-
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3533
|
-
if (elsewhere.variants.length) {
|
|
3534
|
-
const v = elsewhere.variants;
|
|
3535
|
-
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3536
|
-
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
|
|
3537
|
-
continue;
|
|
3538
|
-
}
|
|
3539
|
-
for (const c of elsewhere.components) also.add(c);
|
|
3540
|
-
const prev = edits.get(from);
|
|
3541
|
-
if (prev && prev.value !== `{${to}}`) {
|
|
3542
|
-
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
|
|
3543
|
-
edits.delete(from);
|
|
3544
|
-
continue;
|
|
3545
|
-
}
|
|
3546
|
-
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3547
|
-
}
|
|
3548
|
-
const list = [...edits.values()];
|
|
3549
|
-
if (d.needsCode || reasons.length) {
|
|
3550
|
-
return {
|
|
3551
|
-
route: "claude-draft",
|
|
3552
|
-
said: `A developer builds it with Claude from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3553
|
-
edits: list,
|
|
3554
|
-
reasons,
|
|
3555
|
-
issues,
|
|
3556
|
-
alsoChanges: [...also]
|
|
3557
|
-
};
|
|
3558
|
-
}
|
|
3559
|
-
if (list.length) {
|
|
3560
|
-
const n = list.length;
|
|
3561
|
-
const others = [...also];
|
|
3562
|
-
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3563
|
-
const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
3564
|
-
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
3565
|
-
}
|
|
3566
|
-
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
3567
|
-
}
|
|
3568
|
-
function specsUsage(specs, component) {
|
|
3569
|
-
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3570
|
-
return (path, r) => {
|
|
3571
|
-
const variants = [];
|
|
3572
|
-
const components = [];
|
|
3573
|
-
for (const c of specs.components) {
|
|
3574
|
-
for (const v of c.variants) {
|
|
3575
|
-
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3576
|
-
const key = variantKeyOf(v.props);
|
|
3577
|
-
if (c === own) {
|
|
3578
|
-
if (!r.variants.includes(key)) variants.push(key);
|
|
3579
|
-
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
return { variants, components };
|
|
3583
|
-
};
|
|
3584
|
-
}
|
|
3585
|
-
function componentContrastPairs(edits, pairs2, tokens) {
|
|
3586
|
-
const out = /* @__PURE__ */ new Map();
|
|
3587
|
-
for (const e of edits) {
|
|
3588
|
-
const target = tokens.aliasOf(e.path);
|
|
3589
|
-
if (!target) continue;
|
|
3590
|
-
const group = e.path.split(".")[0];
|
|
3591
|
-
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3592
|
-
for (const p of pairs2) {
|
|
3593
|
-
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3594
|
-
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3597
|
-
return [...out.values()];
|
|
3598
|
-
}
|
|
3599
|
-
const FROM_FIGMA_LABEL = "from-figma";
|
|
3600
|
-
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3601
|
-
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3602
|
-
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3603
|
-
const ORIGIN_MARK = "bridge-from-figma";
|
|
3604
|
-
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3605
|
-
function parseFigmaOrigin(body) {
|
|
3606
|
-
if (!body) return void 0;
|
|
3607
|
-
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3608
|
-
if (!m) return void 0;
|
|
3609
|
-
try {
|
|
3610
|
-
const o = JSON.parse(m[1]);
|
|
3611
|
-
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3612
|
-
} catch {
|
|
3613
|
-
return void 0;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
function figmaOriginSection(o) {
|
|
3617
|
-
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3618
|
-
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3619
|
-
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted with Claude** from the Figma spec. A person reviews it before it ships." : `**Waiting for a developer:** this branch carries the spec only, as a request. A developer builds the code here with Claude (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3620
|
-
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the Bridge Figma plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3621
|
-
}
|
|
3622
|
-
function withFigmaOrigin(body, o) {
|
|
3623
|
-
let b = (body ?? "").trim();
|
|
3624
|
-
const at = b.indexOf("**From Figma:**");
|
|
3625
|
-
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3626
|
-
if (at >= 0 && mark > at) {
|
|
3627
|
-
const end = b.indexOf("-->", mark);
|
|
3628
|
-
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3629
|
-
}
|
|
3630
|
-
return `${figmaOriginSection(o)}${b ? `
|
|
3631
|
-
|
|
3632
|
-
${b}` : ""}`;
|
|
3633
|
-
}
|
|
3634
|
-
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3635
|
-
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3636
|
-
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3637
|
-
function formatFigmaSpec(d, plan) {
|
|
3638
|
-
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3639
|
-
`;
|
|
3640
|
-
}
|
|
3641
|
-
function parseFigmaSpec(text) {
|
|
3642
|
-
if (!text) return void 0;
|
|
3643
|
-
try {
|
|
3644
|
-
const s = JSON.parse(text);
|
|
3645
|
-
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3646
|
-
} catch {
|
|
3647
|
-
return void 0;
|
|
3358
|
+
function parseFigmaSpec(text) {
|
|
3359
|
+
if (!text) return void 0;
|
|
3360
|
+
try {
|
|
3361
|
+
const s = JSON.parse(text);
|
|
3362
|
+
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3363
|
+
} catch {
|
|
3364
|
+
return void 0;
|
|
3648
3365
|
}
|
|
3649
3366
|
}
|
|
3650
3367
|
function figmaDraftBrief(d, ctx) {
|
|
@@ -3713,1163 +3430,1599 @@ function figmaPushStatus(x) {
|
|
|
3713
3430
|
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted with Claude", sentence: "The code is drafted. A person reviews it before it ships.", rebuild: false };
|
|
3714
3431
|
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3715
3432
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
return found.length ? Math.max(...found) : null;
|
|
3725
|
-
}
|
|
3726
|
-
function claudeStage(labels) {
|
|
3727
|
-
const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
|
|
3728
|
-
if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
|
|
3729
|
-
if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
|
|
3730
|
-
return null;
|
|
3731
|
-
}
|
|
3732
|
-
const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
|
|
3733
|
-
function pullState(p) {
|
|
3734
|
-
if (p.merged_at || p.merged) return "merged";
|
|
3735
|
-
if (p.state && p.state !== "open") return "closed";
|
|
3736
|
-
if (isDraft(p)) return "draft";
|
|
3737
|
-
return "open";
|
|
3738
|
-
}
|
|
3739
|
-
const PULL_STATE_PILL = {
|
|
3740
|
-
merged: { label: "Merged", cls: "" },
|
|
3741
|
-
closed: { label: "Closed", cls: "" },
|
|
3742
|
-
draft: { label: "Draft", cls: "" },
|
|
3743
|
-
open: { label: "Open", cls: "" }
|
|
3433
|
+
const SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
3434
|
+
const SUGGESTION_LABEL = "bridge-suggestion";
|
|
3435
|
+
const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
3436
|
+
const SUGGESTION_STATE_LABELS = {
|
|
3437
|
+
"needs-review": "suggestion: needs review",
|
|
3438
|
+
"changes-requested": "suggestion: changes requested",
|
|
3439
|
+
approved: "suggestion: approved",
|
|
3440
|
+
applying: "suggestion: applying"
|
|
3744
3441
|
};
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3442
|
+
const SUGGESTION_FINAL_LABELS = {
|
|
3443
|
+
applied: "suggestion: applied",
|
|
3444
|
+
declined: "suggestion: declined"
|
|
3445
|
+
};
|
|
3446
|
+
const SUGGESTION_STATUS_LABEL = {
|
|
3447
|
+
"needs-review": "Needs review",
|
|
3448
|
+
"changes-requested": "Changes requested",
|
|
3449
|
+
approved: "Approved, ready to apply",
|
|
3450
|
+
applying: "Being applied",
|
|
3451
|
+
applied: "Applied",
|
|
3452
|
+
declined: "Declined",
|
|
3453
|
+
outdated: "Outdated"
|
|
3454
|
+
};
|
|
3455
|
+
const figmaNodeUrl = (fileKey, nodeId) => `https://www.figma.com/design/${fileKey}/?node-id=${encodeURIComponent(nodeId.replace(/:/g, "-"))}`;
|
|
3456
|
+
const SUGGESTION_LIMITS = {
|
|
3457
|
+
body: 65536,
|
|
3458
|
+
/** The readable part of the body (tables are cut with "and N more"). */
|
|
3459
|
+
human: 12e3,
|
|
3460
|
+
/** The JSON block in the body. */
|
|
3461
|
+
inline: 48e3,
|
|
3462
|
+
/** The JSON block of one continuation comment. */
|
|
3463
|
+
part: 6e4,
|
|
3464
|
+
/** Continuation comments at most (about 480 KB of file contents). */
|
|
3465
|
+
maxParts: 8
|
|
3466
|
+
};
|
|
3467
|
+
class SuggestionError extends Error {
|
|
3468
|
+
constructor(code, message) {
|
|
3469
|
+
super(message);
|
|
3470
|
+
this.code = code;
|
|
3471
|
+
this.name = "SuggestionError";
|
|
3472
|
+
}
|
|
3473
|
+
code;
|
|
3750
3474
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3475
|
+
const SUGGESTION_TOO_LARGE = "Too large for a suggestion: split it, or ask a developer to open it directly.";
|
|
3476
|
+
const utf8 = (text) => new TextEncoder().encode(text);
|
|
3477
|
+
const hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
|
|
3478
|
+
function padded(bytes) {
|
|
3479
|
+
const len = bytes.length;
|
|
3480
|
+
const total = Math.ceil((len + 9) / 64) * 64;
|
|
3481
|
+
const buf = new Uint8Array(total);
|
|
3482
|
+
buf.set(bytes);
|
|
3483
|
+
buf[len] = 128;
|
|
3484
|
+
const view = new DataView(buf.buffer);
|
|
3485
|
+
view.setUint32(total - 8, Math.floor(len / 536870912), false);
|
|
3486
|
+
view.setUint32(total - 4, len * 8 >>> 0, false);
|
|
3487
|
+
return view;
|
|
3753
3488
|
}
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3489
|
+
const K256 = [
|
|
3490
|
+
1116352408,
|
|
3491
|
+
1899447441,
|
|
3492
|
+
3049323471,
|
|
3493
|
+
3921009573,
|
|
3494
|
+
961987163,
|
|
3495
|
+
1508970993,
|
|
3496
|
+
2453635748,
|
|
3497
|
+
2870763221,
|
|
3498
|
+
3624381080,
|
|
3499
|
+
310598401,
|
|
3500
|
+
607225278,
|
|
3501
|
+
1426881987,
|
|
3502
|
+
1925078388,
|
|
3503
|
+
2162078206,
|
|
3504
|
+
2614888103,
|
|
3505
|
+
3248222580,
|
|
3506
|
+
3835390401,
|
|
3507
|
+
4022224774,
|
|
3508
|
+
264347078,
|
|
3509
|
+
604807628,
|
|
3510
|
+
770255983,
|
|
3511
|
+
1249150122,
|
|
3512
|
+
1555081692,
|
|
3513
|
+
1996064986,
|
|
3514
|
+
2554220882,
|
|
3515
|
+
2821834349,
|
|
3516
|
+
2952996808,
|
|
3517
|
+
3210313671,
|
|
3518
|
+
3336571891,
|
|
3519
|
+
3584528711,
|
|
3520
|
+
113926993,
|
|
3521
|
+
338241895,
|
|
3522
|
+
666307205,
|
|
3523
|
+
773529912,
|
|
3524
|
+
1294757372,
|
|
3525
|
+
1396182291,
|
|
3526
|
+
1695183700,
|
|
3527
|
+
1986661051,
|
|
3528
|
+
2177026350,
|
|
3529
|
+
2456956037,
|
|
3530
|
+
2730485921,
|
|
3531
|
+
2820302411,
|
|
3532
|
+
3259730800,
|
|
3533
|
+
3345764771,
|
|
3534
|
+
3516065817,
|
|
3535
|
+
3600352804,
|
|
3536
|
+
4094571909,
|
|
3537
|
+
275423344,
|
|
3538
|
+
430227734,
|
|
3539
|
+
506948616,
|
|
3540
|
+
659060556,
|
|
3541
|
+
883997877,
|
|
3542
|
+
958139571,
|
|
3543
|
+
1322822218,
|
|
3544
|
+
1537002063,
|
|
3545
|
+
1747873779,
|
|
3546
|
+
1955562222,
|
|
3547
|
+
2024104815,
|
|
3548
|
+
2227730452,
|
|
3549
|
+
2361852424,
|
|
3550
|
+
2428436474,
|
|
3551
|
+
2756734187,
|
|
3552
|
+
3204031479,
|
|
3553
|
+
3329325298
|
|
3554
|
+
];
|
|
3555
|
+
function sha256Hex(text) {
|
|
3556
|
+
const view = padded(utf8(text));
|
|
3557
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
3558
|
+
const w = new Array(64);
|
|
3559
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
3560
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
3561
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
3562
|
+
for (let i = 16; i < 64; i++) {
|
|
3563
|
+
const a2 = w[i - 15];
|
|
3564
|
+
const b2 = w[i - 2];
|
|
3565
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
3566
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
3567
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
3568
|
+
}
|
|
3569
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
3570
|
+
for (let i = 0; i < 64; i++) {
|
|
3571
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
|
|
3572
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
3573
|
+
hh = g;
|
|
3574
|
+
g = f;
|
|
3575
|
+
f = e;
|
|
3576
|
+
e = d + t1 | 0;
|
|
3577
|
+
d = c;
|
|
3578
|
+
c = b;
|
|
3579
|
+
b = a;
|
|
3580
|
+
a = t1 + t2 | 0;
|
|
3581
|
+
}
|
|
3582
|
+
h[0] = h[0] + a | 0;
|
|
3583
|
+
h[1] = h[1] + b | 0;
|
|
3584
|
+
h[2] = h[2] + c | 0;
|
|
3585
|
+
h[3] = h[3] + d | 0;
|
|
3586
|
+
h[4] = h[4] + e | 0;
|
|
3587
|
+
h[5] = h[5] + f | 0;
|
|
3588
|
+
h[6] = h[6] + g | 0;
|
|
3589
|
+
h[7] = h[7] + hh | 0;
|
|
3772
3590
|
}
|
|
3773
|
-
|
|
3774
|
-
const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
|
|
3775
|
-
return { state, total, passed, failed, pending };
|
|
3591
|
+
return hex(h);
|
|
3776
3592
|
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
const
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
} else {
|
|
3801
|
-
design = a.length >= 1;
|
|
3802
|
-
tech = a.length >= 2;
|
|
3593
|
+
function sha1Bytes(bytes) {
|
|
3594
|
+
const view = padded(bytes);
|
|
3595
|
+
const h = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
|
|
3596
|
+
const w = new Array(80);
|
|
3597
|
+
const rotl = (x, n) => x << n | x >>> 32 - n;
|
|
3598
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
3599
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
3600
|
+
for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
3601
|
+
let [a, b, c, d, e] = h;
|
|
3602
|
+
for (let i = 0; i < 80; i++) {
|
|
3603
|
+
const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
|
|
3604
|
+
const t = rotl(a, 5) + f + e + k + w[i] | 0;
|
|
3605
|
+
e = d;
|
|
3606
|
+
d = c;
|
|
3607
|
+
c = rotl(b, 30);
|
|
3608
|
+
b = a;
|
|
3609
|
+
a = t;
|
|
3610
|
+
}
|
|
3611
|
+
h[0] = h[0] + a | 0;
|
|
3612
|
+
h[1] = h[1] + b | 0;
|
|
3613
|
+
h[2] = h[2] + c | 0;
|
|
3614
|
+
h[3] = h[3] + d | 0;
|
|
3615
|
+
h[4] = h[4] + e | 0;
|
|
3803
3616
|
}
|
|
3804
|
-
return
|
|
3617
|
+
return hex(h);
|
|
3805
3618
|
}
|
|
3806
|
-
function
|
|
3807
|
-
|
|
3808
|
-
const
|
|
3809
|
-
const
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3619
|
+
function gitBlobSha(text) {
|
|
3620
|
+
const body = utf8(text);
|
|
3621
|
+
const head = utf8(`blob ${body.length}\0`);
|
|
3622
|
+
const all = new Uint8Array(head.length + body.length);
|
|
3623
|
+
all.set(head);
|
|
3624
|
+
all.set(body, head.length);
|
|
3625
|
+
return sha1Bytes(all);
|
|
3626
|
+
}
|
|
3627
|
+
function canonicalJson(value) {
|
|
3628
|
+
return JSON.stringify(sortKeys(value));
|
|
3629
|
+
}
|
|
3630
|
+
function sortKeys(v) {
|
|
3631
|
+
if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
|
|
3632
|
+
if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
|
|
3633
|
+
const out = {};
|
|
3634
|
+
for (const k of Object.keys(v).sort()) {
|
|
3635
|
+
const x = v[k];
|
|
3636
|
+
if (x !== void 0) out[k] = sortKeys(x);
|
|
3637
|
+
}
|
|
3638
|
+
return out;
|
|
3814
3639
|
}
|
|
3815
|
-
|
|
3816
|
-
if (!asked.length) return [];
|
|
3817
|
-
return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
|
|
3640
|
+
return v;
|
|
3818
3641
|
}
|
|
3819
|
-
function
|
|
3820
|
-
if (!
|
|
3821
|
-
|
|
3822
|
-
if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
|
|
3823
|
-
const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
|
|
3824
|
-
if (!covered) return pr;
|
|
3825
|
-
const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
|
|
3826
|
-
return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
|
|
3642
|
+
function shaForm(p) {
|
|
3643
|
+
if (!p.files) return p;
|
|
3644
|
+
return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
|
|
3827
3645
|
}
|
|
3828
|
-
function
|
|
3829
|
-
|
|
3830
|
-
const tech = pr.tech || push.scope === "token-values";
|
|
3831
|
-
return { ...pr, design: true, tech, owner: true };
|
|
3646
|
+
function suggestionPayloadSha(p) {
|
|
3647
|
+
return sha256Hex(canonicalJson(shaForm(p)));
|
|
3832
3648
|
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3649
|
+
const KIND_SUBJECT = {
|
|
3650
|
+
"token-change": "Tokens",
|
|
3651
|
+
docs: "Docs",
|
|
3652
|
+
"component-spec": "Component",
|
|
3653
|
+
brand: "Brand",
|
|
3654
|
+
revert: "Revert",
|
|
3655
|
+
config: "Settings"
|
|
3656
|
+
};
|
|
3657
|
+
function suggestionSubject(p) {
|
|
3658
|
+
if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
|
|
3659
|
+
if (p.kind === "docs" && p.docs?.length) {
|
|
3660
|
+
const names2 = [...new Set(p.docs.map((d) => d.component))];
|
|
3661
|
+
return names2.length === 1 ? names2[0] : KIND_SUBJECT.docs;
|
|
3662
|
+
}
|
|
3663
|
+
return KIND_SUBJECT[p.kind];
|
|
3836
3664
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
3665
|
+
function suggestionIssueTitle(p) {
|
|
3666
|
+
const names2 = [...(p.docs ?? []).map((d) => d.component), ...p.componentSpec?.component ? [p.componentSpec.component] : []];
|
|
3667
|
+
return subjectTitle(suggestionSubject(p), p.title, names2);
|
|
3841
3668
|
}
|
|
3842
|
-
function
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3669
|
+
function suggestionLabels(p, opts = {}) {
|
|
3670
|
+
return [
|
|
3671
|
+
.../* @__PURE__ */ new Set([
|
|
3672
|
+
SUGGESTION_LABEL,
|
|
3673
|
+
`suggestion: ${p.kind}`,
|
|
3674
|
+
`source: ${p.source}`,
|
|
3675
|
+
`tier: ${p.tier.tier}`,
|
|
3676
|
+
SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
|
|
3677
|
+
...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
|
|
3678
|
+
...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
|
|
3679
|
+
...p.links?.exploration ? [EXPLORATION_LABEL] : [],
|
|
3680
|
+
...opts.labels ?? []
|
|
3681
|
+
])
|
|
3682
|
+
];
|
|
3848
3683
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
const approvalReasons = [];
|
|
3854
|
-
const lifecycle = pullState(input);
|
|
3855
|
-
if (lifecycle === "merged" || lifecycle === "closed") {
|
|
3856
|
-
return { allowed: false, reasons: [lifecycle === "merged" ? "This branch is already merged." : "This branch is closed."], overridable: false, missing: [], canShip: !input.viewer || canShip(input.viewer, input.ship) };
|
|
3857
|
-
}
|
|
3858
|
-
if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
|
|
3859
|
-
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
3860
|
-
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
3861
|
-
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3862
|
-
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3863
|
-
const tier = input.tier ?? 3;
|
|
3864
|
-
if (tier >= 2) {
|
|
3865
|
-
if (!input.approvals.design) {
|
|
3866
|
-
approvalReasons.push("It needs a design owner approval.");
|
|
3867
|
-
missing.push("design approval");
|
|
3868
|
-
}
|
|
3869
|
-
if (!input.approvals.tech && !input.approvals.valuesOnly) {
|
|
3870
|
-
approvalReasons.push("It needs a tech owner approval.");
|
|
3871
|
-
missing.push("tech approval");
|
|
3872
|
-
}
|
|
3873
|
-
} else if (!hasOwnerApproval(input.approvals)) {
|
|
3874
|
-
approvalReasons.push("It needs one owner approval.");
|
|
3875
|
-
missing.push("approval");
|
|
3684
|
+
function suggestionKindFromLabels(labels) {
|
|
3685
|
+
for (const l of labels) {
|
|
3686
|
+
const m = /^suggestion: (.+)$/.exec(l);
|
|
3687
|
+
if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
|
|
3876
3688
|
}
|
|
3877
|
-
|
|
3878
|
-
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
3879
|
-
const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
3880
|
-
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
3881
|
-
const allowed = reasons.length === 0;
|
|
3882
|
-
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
3883
|
-
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
3689
|
+
return void 0;
|
|
3884
3690
|
}
|
|
3885
|
-
function
|
|
3886
|
-
|
|
3887
|
-
const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
|
|
3888
|
-
const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
|
|
3889
|
-
const override = ownerOverridePolicy(input.override, input.owners);
|
|
3890
|
-
const state = input.state ?? "open";
|
|
3891
|
-
const draft = isDraft(input);
|
|
3892
|
-
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
3893
|
-
const gate = input.ci && approvals ? mergeGate({ tier: input.tier, ci: input.ci, approvals, draft, mergeable: input.mergeable, state, merged_at: input.merged_at, viewer: input.viewer, override, exploration: input.exploration, ship: input.ship }) : null;
|
|
3894
|
-
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
3895
|
-
const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
|
|
3896
|
-
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
|
|
3897
|
-
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
3898
|
-
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
3691
|
+
function suggestionStateFromLabels(labels) {
|
|
3692
|
+
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
3899
3693
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
return
|
|
3694
|
+
function suggestionIsTokenValues(p) {
|
|
3695
|
+
if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
|
|
3696
|
+
if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
|
|
3697
|
+
return (p.files ?? []).every((f) => f.path.endsWith(".md"));
|
|
3904
3698
|
}
|
|
3905
|
-
function
|
|
3906
|
-
const
|
|
3907
|
-
|
|
3908
|
-
if (
|
|
3909
|
-
if (
|
|
3910
|
-
return
|
|
3699
|
+
function suggestionStatus(i) {
|
|
3700
|
+
const prs = i.linkedPrs ?? [];
|
|
3701
|
+
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
3702
|
+
if (prs.some((p) => p.state === "open") || i.applied) return "applying";
|
|
3703
|
+
if (i.outdated) return "outdated";
|
|
3704
|
+
return i.review ?? "needs-review";
|
|
3911
3705
|
}
|
|
3912
|
-
function
|
|
3913
|
-
|
|
3914
|
-
return `${OVERRIDE_HEADING}
|
|
3915
|
-
|
|
3916
|
-
Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
|
|
3706
|
+
function withOutdated(status, outdated) {
|
|
3707
|
+
return outdated && (status === "needs-review" || status === "changes-requested" || status === "approved") ? "outdated" : status;
|
|
3917
3708
|
}
|
|
3918
|
-
function
|
|
3919
|
-
|
|
3709
|
+
function suggestionStateLabel(status) {
|
|
3710
|
+
if (status === "applied" || status === "declined") return SUGGESTION_FINAL_LABELS[status];
|
|
3711
|
+
return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
|
|
3920
3712
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
if (
|
|
3924
|
-
const
|
|
3925
|
-
|
|
3926
|
-
|
|
3713
|
+
const SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
|
|
3714
|
+
function suggestionLabelChanges(labels, status) {
|
|
3715
|
+
if (status === "outdated") return { add: [], remove: [] };
|
|
3716
|
+
const want = suggestionStateLabel(status);
|
|
3717
|
+
return {
|
|
3718
|
+
add: want && !labels.includes(want) ? [want] : [],
|
|
3719
|
+
remove: SUGGESTION_STATUS_LABELS.filter((l) => l !== want && labels.includes(l))
|
|
3720
|
+
};
|
|
3927
3721
|
}
|
|
3928
|
-
|
|
3722
|
+
const bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
|
|
3723
|
+
const MARK = "bridge-suggestion";
|
|
3724
|
+
const PART_SCHEMA = "bridge.suggestion-part/1";
|
|
3725
|
+
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
3726
|
+
const safeText = (s) => s.replace(/<!--/g, "<!--");
|
|
3727
|
+
const sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
3728
|
+
const show$1 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
3729
|
+
function table(head, rows, max) {
|
|
3730
|
+
if (!rows.length) return [];
|
|
3731
|
+
const shown = rows.slice(0, max);
|
|
3732
|
+
return [
|
|
3733
|
+
`| ${head.join(" | ")} |`,
|
|
3734
|
+
`| ${head.map(() => "---").join(" | ")} |`,
|
|
3735
|
+
...shown.map((r) => `| ${r.map((c) => cell(c)).join(" | ")} |`),
|
|
3736
|
+
...rows.length > max ? ["", `and ${rows.length - max} more`] : []
|
|
3737
|
+
];
|
|
3738
|
+
}
|
|
3739
|
+
function changesSection(p, max) {
|
|
3929
3740
|
const out = [];
|
|
3930
|
-
if (
|
|
3931
|
-
|
|
3932
|
-
|
|
3741
|
+
if (p.tokens?.changes.length) {
|
|
3742
|
+
const rows = p.tokens.changes.map((c) => [
|
|
3743
|
+
`\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
|
|
3744
|
+
c.beforeCss ?? show$1(c.before),
|
|
3745
|
+
c.afterCss ?? show$1(c.after)
|
|
3746
|
+
]);
|
|
3747
|
+
out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
|
|
3748
|
+
} else if (p.tokens?.edits.length) {
|
|
3749
|
+
const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show$1(e.value)]);
|
|
3750
|
+
out.push(...table(["Token", "Edit", "Value"], rows, max), "");
|
|
3751
|
+
}
|
|
3752
|
+
if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
|
|
3753
|
+
if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
|
|
3754
|
+
if (p.files?.length) {
|
|
3755
|
+
const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
|
|
3756
|
+
out.push(...table(["File", "Change"], rows, max), "");
|
|
3757
|
+
}
|
|
3933
3758
|
return out;
|
|
3934
3759
|
}
|
|
3935
|
-
function
|
|
3936
|
-
|
|
3760
|
+
function checksSection(p, max) {
|
|
3761
|
+
const c = p.checks;
|
|
3762
|
+
const failing = c.contrast.filter((x) => !x.pass);
|
|
3763
|
+
const rows = [...failing, ...c.contrast.filter((x) => x.pass)].map((x) => [x.pair, x.mode ?? "", x.before.toFixed(2), x.after.toFixed(2), String(x.min), x.pass ? "pass" : "**fails**"]);
|
|
3764
|
+
return [
|
|
3765
|
+
"## Checks when suggested",
|
|
3766
|
+
"",
|
|
3767
|
+
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
3768
|
+
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
3769
|
+
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
|
|
3770
|
+
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
3771
|
+
"",
|
|
3772
|
+
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
3773
|
+
];
|
|
3937
3774
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
"
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
const lifecycle = pullState(input);
|
|
3956
|
-
if (lifecycle === "merged") return out("merged");
|
|
3957
|
-
if (lifecycle === "closed") return out("closed");
|
|
3958
|
-
if (lifecycle === "draft") return out("draft");
|
|
3959
|
-
if (input.ci?.state === "fail") return out("ci-failing");
|
|
3960
|
-
if (input.approvals?.changesRequested.length) return out("changes-requested");
|
|
3961
|
-
if (input.mergeable === false) return out("conflict");
|
|
3962
|
-
if (input.ci?.state === "pending") return out("ci-running");
|
|
3963
|
-
if (!input.ci || !input.approvals) return out("unknown");
|
|
3964
|
-
const tier = input.tier ?? 3;
|
|
3965
|
-
if (tier >= 2) {
|
|
3966
|
-
if (!input.approvals.design) return out("needs-design");
|
|
3967
|
-
if (!input.approvals.tech) return out("needs-tech");
|
|
3968
|
-
} else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
|
|
3969
|
-
if (input.mergeable === null) return out("checking");
|
|
3970
|
-
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: isDraft(input), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
|
|
3971
|
-
return gate.allowed ? out("ready") : out("unknown");
|
|
3972
|
-
}
|
|
3973
|
-
const REVIEW_STATUS_LABEL = {
|
|
3974
|
-
merged: "Merged",
|
|
3975
|
-
closed: "Closed",
|
|
3976
|
-
draft: "Draft",
|
|
3977
|
-
"ci-failing": "Blocked by tests",
|
|
3978
|
-
"changes-requested": "Changes requested",
|
|
3979
|
-
"ci-running": "Tests running",
|
|
3980
|
-
"needs-design": "Needs design approval",
|
|
3981
|
-
"needs-tech": "Needs tech approval",
|
|
3982
|
-
"needs-approval": "Needs approval",
|
|
3983
|
-
conflict: "Conflict with main",
|
|
3984
|
-
checking: "Checking for conflicts",
|
|
3985
|
-
ready: "Ready to merge",
|
|
3986
|
-
unknown: "Status unknown"
|
|
3987
|
-
};
|
|
3988
|
-
const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
|
|
3989
|
-
function reviewStatusLabel(kind, lookAgain) {
|
|
3990
|
-
return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
|
|
3991
|
-
}
|
|
3992
|
-
const CI_STATE_LABEL = {
|
|
3993
|
-
pass: "Tests pass",
|
|
3994
|
-
fail: "Tests fail",
|
|
3995
|
-
pending: "Tests running",
|
|
3996
|
-
none: "No automated checks"
|
|
3997
|
-
};
|
|
3998
|
-
const BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
|
|
3999
|
-
const isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
|
|
4000
|
-
function isAutomationPull(p) {
|
|
4001
|
-
return isReleasePull(p) || BOTS.has((p.user?.login ?? "").toLowerCase());
|
|
4002
|
-
}
|
|
4003
|
-
const runningExploration = (p) => p.state === "open" && isActiveExploration(p.labels ?? []);
|
|
4004
|
-
function reviewQueue(pulls) {
|
|
4005
|
-
return openPulls(pulls).filter((p) => !isAutomationPull(p) && !runningExploration(p));
|
|
4006
|
-
}
|
|
4007
|
-
function releasePull(pulls) {
|
|
4008
|
-
return openPulls(pulls).find((p) => isReleasePull(p)) ?? null;
|
|
4009
|
-
}
|
|
4010
|
-
function effectiveTier(labels, body, files) {
|
|
4011
|
-
const labelled = parseTier(labels, body);
|
|
4012
|
-
if (!files) return labelled;
|
|
4013
|
-
const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
|
|
4014
|
-
return Math.max(labelled ?? 0, computed);
|
|
4015
|
-
}
|
|
4016
|
-
const norm$1 = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
4017
|
-
function reopenEligibility(x) {
|
|
4018
|
-
if (x.state !== "closed" || !x.viewer) return { kind: "none" };
|
|
4019
|
-
const isAuthor = norm$1(x.viewer) === norm$1(x.author);
|
|
4020
|
-
const isOwner = x.owners.some((o) => norm$1(o) === norm$1(x.viewer));
|
|
4021
|
-
if (!isAuthor && !isOwner) return { kind: "none" };
|
|
4022
|
-
if (!x.canWrite && !isAuthor) return { kind: "none" };
|
|
4023
|
-
if (x.branch === "exists") return { kind: "reopen" };
|
|
4024
|
-
if (x.branch === "gone") return { kind: "gone" };
|
|
4025
|
-
return { kind: "none" };
|
|
4026
|
-
}
|
|
4027
|
-
const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
|
|
4028
|
-
const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
|
|
4029
|
-
function requiredApprovals(tier, valuesOnly) {
|
|
4030
|
-
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false, ...valuesOnly ? { valuesOnly: true } : {} }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4031
|
-
}
|
|
4032
|
-
function pendingApprovals(tier, approvals) {
|
|
4033
|
-
if (tier == null || !approvals) return [];
|
|
4034
|
-
return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4035
|
-
}
|
|
4036
|
-
const uniqLogins = (xs) => xs.filter((x, i) => xs.findIndex((y) => normLogin(y) === normLogin(x)) === i);
|
|
4037
|
-
function eligibleReviewers(role, p) {
|
|
4038
|
-
const raw = role === "design" ? p.owners.design : role === "tech" ? p.owners.tech : uniqLogins([...p.owners.design, ...p.owners.tech]);
|
|
4039
|
-
const listed = raw.map((x) => x.replace(/^@/, ""));
|
|
4040
|
-
const approvers = (p.approvers ?? []).map(normLogin);
|
|
4041
|
-
const logins = listed.filter((l) => normLogin(l) !== normLogin(p.author) && !approvers.includes(normLogin(l)));
|
|
4042
|
-
return { open: listed.length === 0, logins, listed };
|
|
4043
|
-
}
|
|
4044
|
-
function reviewersToAsk(x) {
|
|
4045
|
-
const roles = pendingApprovals(x.tier, x.approvals);
|
|
4046
|
-
const all = roles.flatMap((r) => eligibleReviewers(r, { owners: x.owners, author: x.author, approvers: x.approvals.approvers }).logins);
|
|
4047
|
-
return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
|
|
4048
|
-
}
|
|
4049
|
-
const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6}$/;
|
|
4050
|
-
function parseJiraKey(input) {
|
|
4051
|
-
const t = (input ?? "").trim().toUpperCase();
|
|
4052
|
-
if (!t) return null;
|
|
4053
|
-
if (!JIRA_KEY.test(t)) throw new Error("Write the Jira ticket as its key, like DS-42, or leave it empty.");
|
|
4054
|
-
return t;
|
|
4055
|
-
}
|
|
4056
|
-
const isJiraKey = (input) => JIRA_KEY.test(input.trim().toUpperCase());
|
|
4057
|
-
const LEADING_KEY = /^\[?([A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6})\]?[:\s]\s*/;
|
|
4058
|
-
function jiraKeyOf(title) {
|
|
4059
|
-
return LEADING_KEY.exec(title.trim())?.[1] ?? null;
|
|
4060
|
-
}
|
|
4061
|
-
const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
|
|
4062
|
-
function titleWithJiraKey(title, key) {
|
|
4063
|
-
if (!key) return title;
|
|
4064
|
-
return `${key} ${withoutJiraKey(title)}`;
|
|
4065
|
-
}
|
|
4066
|
-
function branchWithJiraKey(branch, key) {
|
|
4067
|
-
if (!key || branch.includes(key)) return branch;
|
|
4068
|
-
const i = branch.lastIndexOf("/");
|
|
4069
|
-
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
4070
|
-
}
|
|
4071
|
-
const SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
|
|
4072
|
-
const SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
|
|
4073
|
-
const EVENTS = ["approve", "request-changes", "comment"];
|
|
4074
|
-
const ROLE_TEXT = { design: "for design", tech: "for tech", approval: "" };
|
|
4075
|
-
function composeSuggestionReviewComment(r) {
|
|
4076
|
-
const who = r.by ? ` by @${r.by.replace(/^@/, "")}` : "";
|
|
4077
|
-
const role = r.role ? ROLE_TEXT[r.role] : "";
|
|
4078
|
-
const head = r.event === "approve" ? `Approved${role ? ` ${role}` : ""}${who} in Bridge.` : r.event === "request-changes" ? `Changes requested${who} in Bridge.` : `Comment${who} in Bridge.`;
|
|
4079
|
-
const quote = r.message?.trim() ? r.message.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n") : "";
|
|
4080
|
-
const record = { event: r.event, ...r.role ? { role: r.role } : {}, payloadSha: r.payloadSha, at: r.at };
|
|
4081
|
-
return [head, ...quote ? ["", quote] : [], "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_REVIEW_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
4082
|
-
}
|
|
4083
|
-
function parseSuggestionReviewComment(body) {
|
|
4084
|
-
const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4085
|
-
if (!m) return null;
|
|
4086
|
-
try {
|
|
4087
|
-
const r = JSON.parse(m[1]);
|
|
4088
|
-
if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
|
|
4089
|
-
return {
|
|
4090
|
-
event: r.event,
|
|
4091
|
-
...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
|
|
4092
|
-
payloadSha: r.payloadSha,
|
|
4093
|
-
at: typeof r.at === "string" ? r.at : ""
|
|
4094
|
-
};
|
|
4095
|
-
} catch {
|
|
4096
|
-
return null;
|
|
3775
|
+
function linksSection(p) {
|
|
3776
|
+
const l = p.links ?? {};
|
|
3777
|
+
const out = [];
|
|
3778
|
+
if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell(l.neededBy.title)}](${l.neededBy.url})`);
|
|
3779
|
+
if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
|
|
3780
|
+
const key = l.figma?.fileKey;
|
|
3781
|
+
const nodes = (l.figma?.nodes ?? []).filter((n) => n && typeof n.nodeId === "string");
|
|
3782
|
+
for (const n of nodes) {
|
|
3783
|
+
const url = n.url ?? (key ? figmaNodeUrl(key, n.nodeId) : void 0);
|
|
3784
|
+
const what = `${n.state === "before" ? "Before" : n.state === "after" ? "After" : "Node"}${n.name ? ` (${cell(n.name)})` : ""}`;
|
|
3785
|
+
out.push(`- ${what}: ${url ? `[node ${n.nodeId}](${url})` : `node ${n.nodeId}`}`);
|
|
3786
|
+
}
|
|
3787
|
+
if (key && (nodes.length || l.figma?.nodeId)) {
|
|
3788
|
+
const ids = nodes.length ? nodes.map((n) => n.nodeId) : [l.figma.nodeId];
|
|
3789
|
+
out.push(
|
|
3790
|
+
`- Claude Code reads this design straight from Figma: the Figma MCP server (\`get_design_context\` with file key \`${key}\` and node ${ids.map((i) => `\`${i}\``).join(", ")}) or the REST API (\`GET https://api.figma.com/v1/files/${key}/nodes?ids=${ids.map(encodeURIComponent).join(",")}\`). No image hosting is needed.`
|
|
3791
|
+
);
|
|
4097
3792
|
}
|
|
3793
|
+
if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
|
|
3794
|
+
if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
|
|
3795
|
+
for (const v of p.previews ?? []) out.push(`- ${cell(v.label)}: ${v.url}`);
|
|
3796
|
+
return out.length ? ["## Previews and links", "", ...out, ""] : [];
|
|
4098
3797
|
}
|
|
4099
|
-
function
|
|
4100
|
-
const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
|
|
4101
|
-
const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
|
|
3798
|
+
function humanPart(p, details, max, repo) {
|
|
4102
3799
|
return [
|
|
4103
|
-
|
|
3800
|
+
`## Suggested change: ${safeText(p.title.trim())}`,
|
|
4104
3801
|
"",
|
|
4105
|
-
|
|
3802
|
+
safeText(p.summary.trim()),
|
|
3803
|
+
"",
|
|
3804
|
+
...changesSection(p, max),
|
|
3805
|
+
...details?.trim() ? [safeText(details.trim()), ""] : [],
|
|
3806
|
+
...checksSection(p, max),
|
|
3807
|
+
...linksSection(p),
|
|
3808
|
+
"## How to apply",
|
|
3809
|
+
"",
|
|
3810
|
+
"- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
|
|
3811
|
+
'- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
|
|
3812
|
+
` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
|
|
3813
|
+
"",
|
|
3814
|
+
"The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
|
|
3815
|
+
"",
|
|
3816
|
+
`<sub>Suggested from ${sourceLabel(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
|
|
4106
3817
|
].join("\n");
|
|
4107
3818
|
}
|
|
4108
|
-
function
|
|
4109
|
-
const
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
const r = JSON.parse(m[1]);
|
|
4113
|
-
return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
|
|
4114
|
-
} catch {
|
|
4115
|
-
return null;
|
|
3819
|
+
function fitHuman(p, details, repo) {
|
|
3820
|
+
for (const max of [50, 20, 8, 3]) {
|
|
3821
|
+
const h2 = humanPart(p, details, max, repo);
|
|
3822
|
+
if (h2.length <= SUGGESTION_LIMITS.human) return h2;
|
|
4116
3823
|
}
|
|
3824
|
+
const h = humanPart(p, void 0, 3, repo);
|
|
3825
|
+
if (h.length <= SUGGESTION_LIMITS.human) return h;
|
|
3826
|
+
const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
|
|
3827
|
+
return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
|
|
4117
3828
|
}
|
|
4118
|
-
const
|
|
4119
|
-
|
|
4120
|
-
const
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
function
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
|
|
4134
|
-
} catch {
|
|
4135
|
-
return null;
|
|
4136
|
-
}
|
|
3829
|
+
const pretty = (v) => JSON.stringify(sortKeys(v), null, 2);
|
|
3830
|
+
const fit = (v) => {
|
|
3831
|
+
const p = pretty(v);
|
|
3832
|
+
return p.length <= SUGGESTION_LIMITS.inline ? p : canonicalJson(v);
|
|
3833
|
+
};
|
|
3834
|
+
const fenced = (json) => ["```json", json, "```"].join("\n");
|
|
3835
|
+
function payloadBlock(json, sha, parts) {
|
|
3836
|
+
return [
|
|
3837
|
+
"<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
|
|
3838
|
+
"",
|
|
3839
|
+
`<!-- ${MARK}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
|
|
3840
|
+
fenced(json),
|
|
3841
|
+
"",
|
|
3842
|
+
"</details>"
|
|
3843
|
+
].join("\n");
|
|
4137
3844
|
}
|
|
4138
|
-
function
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
3845
|
+
function partComment(i, n, json) {
|
|
3846
|
+
return [
|
|
3847
|
+
`<details><summary>Machine-readable change, part ${i} of ${n} (Bridge suggestion v1)</summary>`,
|
|
3848
|
+
"",
|
|
3849
|
+
`<!-- ${MARK}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
|
|
3850
|
+
fenced(json),
|
|
3851
|
+
"",
|
|
3852
|
+
"</details>"
|
|
3853
|
+
].join("\n");
|
|
4146
3854
|
}
|
|
4147
|
-
function
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
3855
|
+
function escapedEnd(s, from, budget) {
|
|
3856
|
+
let used = 0;
|
|
3857
|
+
let i = from;
|
|
3858
|
+
while (i < s.length) {
|
|
3859
|
+
const c = s.charCodeAt(i);
|
|
3860
|
+
let w = 1;
|
|
3861
|
+
let len = 1;
|
|
3862
|
+
if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
|
|
3863
|
+
else if (c < 32) len = 6;
|
|
3864
|
+
else if (c >= 55296 && c <= 56319) {
|
|
3865
|
+
const d = s.charCodeAt(i + 1);
|
|
3866
|
+
if (d >= 56320 && d <= 57343) {
|
|
3867
|
+
w = 2;
|
|
3868
|
+
len = 2;
|
|
3869
|
+
} else len = 6;
|
|
3870
|
+
} else if (c >= 56320 && c <= 57343) len = 6;
|
|
3871
|
+
if (used + len > budget) break;
|
|
3872
|
+
used += len;
|
|
3873
|
+
i += w;
|
|
4155
3874
|
}
|
|
4156
|
-
return
|
|
3875
|
+
return i;
|
|
4157
3876
|
}
|
|
4158
|
-
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
const
|
|
4163
|
-
const
|
|
4164
|
-
const
|
|
4165
|
-
const
|
|
4166
|
-
const
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
3877
|
+
function composeSuggestionIssue(payload, opts = {}) {
|
|
3878
|
+
const problems = suggestionShapeProblems(payload);
|
|
3879
|
+
if (problems.length) throw new SuggestionError("invalid", problems.join(" "));
|
|
3880
|
+
if (payload.checks.blocked) throw new SuggestionError("invalid", `This change cannot be suggested: ${payload.checks.blocked}`);
|
|
3881
|
+
const full = shaForm(payload);
|
|
3882
|
+
const sha = suggestionPayloadSha(full);
|
|
3883
|
+
const human = fitHuman(full, opts.details, opts.repo);
|
|
3884
|
+
const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
|
|
3885
|
+
const inline = fit(full);
|
|
3886
|
+
if (inline.length <= SUGGESTION_LIMITS.inline) {
|
|
3887
|
+
return { ...base, body: `${human}
|
|
3888
|
+
|
|
3889
|
+
${payloadBlock(inline, sha, 0)}
|
|
3890
|
+
`, parts: [] };
|
|
3891
|
+
}
|
|
3892
|
+
const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
|
|
3893
|
+
const parts = [];
|
|
3894
|
+
const firstPart = /* @__PURE__ */ new Map();
|
|
3895
|
+
const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
|
|
3896
|
+
const emptyOverhead = partJson([], 9, 9).length;
|
|
3897
|
+
let cur = [];
|
|
3898
|
+
let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
3899
|
+
for (const { f, i } of moved) {
|
|
3900
|
+
const text = f.content;
|
|
3901
|
+
let at = 0;
|
|
3902
|
+
while (at < text.length) {
|
|
3903
|
+
const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
|
|
3904
|
+
if (room - entryOverhead < 256) {
|
|
3905
|
+
parts.push(cur);
|
|
3906
|
+
cur = [];
|
|
3907
|
+
room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
3908
|
+
}
|
|
3909
|
+
const end = escapedEnd(text, at, room - entryOverhead);
|
|
3910
|
+
if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
3911
|
+
if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
|
|
3912
|
+
const chunk = { i, path: f.path, text: text.slice(at, end) };
|
|
3913
|
+
cur.push(chunk);
|
|
3914
|
+
room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
|
|
3915
|
+
at = end;
|
|
4183
3916
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
3917
|
+
}
|
|
3918
|
+
if (cur.length) parts.push(cur);
|
|
3919
|
+
if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
3920
|
+
const skeleton = {
|
|
3921
|
+
...full,
|
|
3922
|
+
files: (full.files ?? []).map((f, i) => {
|
|
3923
|
+
if (!firstPart.has(i)) return f;
|
|
3924
|
+
const { content: _content, ...rest } = f;
|
|
3925
|
+
return { ...rest, part: firstPart.get(i) };
|
|
3926
|
+
})
|
|
3927
|
+
};
|
|
3928
|
+
const skeletonJson = fit(skeleton);
|
|
3929
|
+
if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
3930
|
+
const n = parts.length;
|
|
3931
|
+
const partBodies = parts.map((chunks, k) => {
|
|
3932
|
+
const json = partJson(chunks, k + 1, n);
|
|
3933
|
+
if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
3934
|
+
return partComment(k + 1, n, json);
|
|
4186
3935
|
});
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
3936
|
+
const body = `${human}
|
|
3937
|
+
|
|
3938
|
+
${payloadBlock(skeletonJson, sha, n)}
|
|
3939
|
+
`;
|
|
3940
|
+
if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
3941
|
+
return { ...base, body, parts: partBodies };
|
|
4192
3942
|
}
|
|
4193
|
-
function
|
|
4194
|
-
|
|
4195
|
-
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
3943
|
+
function hasSuggestionBlock(body) {
|
|
3944
|
+
return new RegExp(`^<!-- ${MARK}:v\\d+ sha256=`, "m").test(body ?? "");
|
|
4196
3945
|
}
|
|
4197
|
-
|
|
4198
|
-
const
|
|
4199
|
-
|
|
4200
|
-
const SUGGESTION_STATE_LABELS = {
|
|
4201
|
-
"needs-review": "suggestion: needs review",
|
|
4202
|
-
"changes-requested": "suggestion: changes requested",
|
|
4203
|
-
approved: "suggestion: approved",
|
|
4204
|
-
applying: "suggestion: applying"
|
|
4205
|
-
};
|
|
4206
|
-
const SUGGESTION_FINAL_LABELS = {
|
|
4207
|
-
applied: "suggestion: applied",
|
|
4208
|
-
declined: "suggestion: declined"
|
|
4209
|
-
};
|
|
4210
|
-
const SUGGESTION_STATUS_LABEL = {
|
|
4211
|
-
"needs-review": "Needs review",
|
|
4212
|
-
"changes-requested": "Changes requested",
|
|
4213
|
-
approved: "Approved, ready to apply",
|
|
4214
|
-
applying: "Being applied",
|
|
4215
|
-
applied: "Applied",
|
|
4216
|
-
declined: "Declined",
|
|
4217
|
-
outdated: "Outdated"
|
|
4218
|
-
};
|
|
4219
|
-
const figmaNodeUrl = (fileKey, nodeId) => `https://www.figma.com/design/${fileKey}/?node-id=${encodeURIComponent(nodeId.replace(/:/g, "-"))}`;
|
|
4220
|
-
const SUGGESTION_LIMITS = {
|
|
4221
|
-
body: 65536,
|
|
4222
|
-
/** The readable part of the body (tables are cut with "and N more"). */
|
|
4223
|
-
human: 12e3,
|
|
4224
|
-
/** The JSON block in the body. */
|
|
4225
|
-
inline: 48e3,
|
|
4226
|
-
/** The JSON block of one continuation comment. */
|
|
4227
|
-
part: 6e4,
|
|
4228
|
-
/** Continuation comments at most (about 480 KB of file contents). */
|
|
4229
|
-
maxParts: 8
|
|
4230
|
-
};
|
|
4231
|
-
class SuggestionError extends Error {
|
|
4232
|
-
constructor(code, message) {
|
|
4233
|
-
super(message);
|
|
4234
|
-
this.code = code;
|
|
4235
|
-
this.name = "SuggestionError";
|
|
4236
|
-
}
|
|
4237
|
-
code;
|
|
3946
|
+
function fencedAfter(text, from) {
|
|
3947
|
+
const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
|
|
3948
|
+
return m?.[1];
|
|
4238
3949
|
}
|
|
4239
|
-
const
|
|
4240
|
-
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4243
|
-
const
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
3950
|
+
const fail = (reason, message) => ({ ok: false, reason, message });
|
|
3951
|
+
function parseSuggestionIssue(body, opts = {}) {
|
|
3952
|
+
const text = body ?? "";
|
|
3953
|
+
const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
|
|
3954
|
+
const marker = markers[markers.length - 1];
|
|
3955
|
+
if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
|
|
3956
|
+
if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
|
|
3957
|
+
const json = fencedAfter(text, marker.index + marker[0].length);
|
|
3958
|
+
if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
|
|
3959
|
+
let raw;
|
|
3960
|
+
try {
|
|
3961
|
+
raw = JSON.parse(json);
|
|
3962
|
+
} catch {
|
|
3963
|
+
return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
|
|
3964
|
+
}
|
|
3965
|
+
const schema = raw?.schema;
|
|
3966
|
+
if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
|
|
3967
|
+
const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
|
|
3968
|
+
return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
|
|
3969
|
+
}
|
|
3970
|
+
const problems = suggestionShapeProblems(raw);
|
|
3971
|
+
if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
|
|
3972
|
+
const payload = raw;
|
|
3973
|
+
const markedSha = marker[2];
|
|
3974
|
+
const expected = marker[3] ? Number(marker[3]) : 0;
|
|
3975
|
+
let edited = false;
|
|
3976
|
+
if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
|
|
3977
|
+
const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
|
|
3978
|
+
const found = /* @__PURE__ */ new Map();
|
|
3979
|
+
const author = opts.author?.toLowerCase();
|
|
3980
|
+
for (const c of opts.comments ?? []) {
|
|
3981
|
+
if (author && c.author && c.author.toLowerCase() !== author) continue;
|
|
3982
|
+
const m = new RegExp(`^<!-- ${MARK}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
|
|
3983
|
+
if (!m) continue;
|
|
3984
|
+
const i = Number(m[1]);
|
|
3985
|
+
if (found.has(i)) continue;
|
|
3986
|
+
const pj = fencedAfter(c.body, m.index + m[0].length);
|
|
3987
|
+
if (pj === void 0) continue;
|
|
3988
|
+
if (sha256Hex(pj) !== m[3]) edited = true;
|
|
3989
|
+
try {
|
|
3990
|
+
const part = JSON.parse(pj);
|
|
3991
|
+
if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
|
|
3992
|
+
found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
|
|
3993
|
+
} catch {
|
|
3994
|
+
continue;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
for (let i = 1; i <= want; i++) {
|
|
3998
|
+
if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
|
|
3999
|
+
}
|
|
4000
|
+
const files = (payload.files ?? []).map((f, idx) => {
|
|
4001
|
+
if (f.part === void 0) return f;
|
|
4002
|
+
let content = "";
|
|
4003
|
+
for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
|
|
4004
|
+
const { part: _part, ...rest } = f;
|
|
4005
|
+
return { ...rest, content };
|
|
4006
|
+
});
|
|
4007
|
+
payload.files = files;
|
|
4008
|
+
}
|
|
4009
|
+
const sha = suggestionPayloadSha(payload);
|
|
4010
|
+
return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
|
|
4252
4011
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4012
|
+
function suggestionShapeProblems(raw) {
|
|
4013
|
+
const out = [];
|
|
4014
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
|
|
4015
|
+
const p = raw;
|
|
4016
|
+
if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
|
|
4017
|
+
if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
|
|
4018
|
+
if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
|
|
4019
|
+
if (typeof p.summary !== "string") out.push('"summary" must be text.');
|
|
4020
|
+
if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
|
|
4021
|
+
if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
|
|
4022
|
+
const base = p.base;
|
|
4023
|
+
if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
|
|
4024
|
+
const tier = p.tier;
|
|
4025
|
+
if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
|
|
4026
|
+
const checks = p.checks;
|
|
4027
|
+
if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
|
|
4028
|
+
out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
|
|
4029
|
+
const tokens = p.tokens;
|
|
4030
|
+
if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
4031
|
+
if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
|
|
4032
|
+
const spec = p.componentSpec;
|
|
4033
|
+
if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
|
|
4034
|
+
const nodes = p.links?.figma?.nodes;
|
|
4035
|
+
if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
|
|
4036
|
+
if (p.files !== void 0) {
|
|
4037
|
+
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
4038
|
+
else {
|
|
4039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4040
|
+
for (const f of p.files) {
|
|
4041
|
+
const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
|
|
4042
|
+
if (!ok) {
|
|
4043
|
+
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
4044
|
+
break;
|
|
4045
|
+
}
|
|
4046
|
+
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
4047
|
+
seen.add(f.path);
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
return out;
|
|
4052
|
+
}
|
|
4053
|
+
function unsafePathProblem(path) {
|
|
4054
|
+
if (typeof path !== "string" || !path.length) return "A path is empty.";
|
|
4055
|
+
if (/[\0-\x1f\x7f]/.test(path)) return `"${path.replace(/[\0-\x1f\x7f]/g, "?")}" has control characters.`;
|
|
4056
|
+
if (path.includes("\\")) return `"${path}" uses backslashes: paths use forward slashes.`;
|
|
4057
|
+
if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.startsWith("~")) return `"${path}" is absolute: paths are relative to the repository root.`;
|
|
4058
|
+
const segs = path.split("/");
|
|
4059
|
+
if (segs.some((s) => s === "" || s === "." || s === "..")) return `"${path}" has an empty, "." or ".." part: paths stay inside the repository.`;
|
|
4060
|
+
if (segs.some((s) => s.toLowerCase() === ".git")) return `"${path}" is inside .git.`;
|
|
4061
|
+
return null;
|
|
4062
|
+
}
|
|
4063
|
+
const under = (path, root) => {
|
|
4064
|
+
const r = root.replace(/\/+$/, "");
|
|
4065
|
+
return !r || r === "." || path === r || path.startsWith(`${r}/`);
|
|
4066
|
+
};
|
|
4067
|
+
function isGuardrailPath(path, guardrails) {
|
|
4068
|
+
return guardrails.some((g) => g.endsWith("/") ? path.startsWith(g) || path === g.slice(0, -1) : path === g);
|
|
4069
|
+
}
|
|
4070
|
+
function suggestionAllowedRoots(kind, config) {
|
|
4071
|
+
const tokenRoot2 = tokensPath(config) || ".";
|
|
4072
|
+
const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
|
|
4073
|
+
switch (kind) {
|
|
4074
|
+
case "token-change":
|
|
4075
|
+
return [tokenRoot2, ...changesets];
|
|
4076
|
+
case "docs":
|
|
4077
|
+
return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
|
|
4078
|
+
case "component-spec":
|
|
4079
|
+
return [".bridge/figma"];
|
|
4080
|
+
case "brand":
|
|
4081
|
+
return [tokenRoot2, ...changesets, CONFIG_FILE];
|
|
4082
|
+
case "config":
|
|
4083
|
+
return [CONFIG_FILE];
|
|
4084
|
+
case "revert":
|
|
4085
|
+
return void 0;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
function suggestionPathProblems(p, config) {
|
|
4089
|
+
const out = [];
|
|
4090
|
+
const roots = suggestionAllowedRoots(p.kind, config);
|
|
4091
|
+
const guardrails = config.governance.guardrails;
|
|
4092
|
+
const check = (path) => {
|
|
4093
|
+
const bad = unsafePathProblem(path);
|
|
4094
|
+
if (bad) return out.push(bad);
|
|
4095
|
+
if (roots && !roots.some((r) => under(path, r))) return out.push(`"${path}" is outside what a ${p.kind} suggestion may change (${roots.join(", ")}).`);
|
|
4096
|
+
const configAllowed = path === CONFIG_FILE && (p.kind === "config" || p.kind === "brand");
|
|
4097
|
+
if (!configAllowed && isGuardrailPath(path, guardrails)) return out.push(`"${path}" is a guardrail file: change it in a pull request, not a suggestion.`);
|
|
4098
|
+
return 0;
|
|
4099
|
+
};
|
|
4100
|
+
for (const f of p.files ?? []) check(f.path);
|
|
4101
|
+
for (const d of p.docs ?? []) check(d.path);
|
|
4102
|
+
if (p.componentSpec) check(p.componentSpec.specPath);
|
|
4103
|
+
for (const e of p.tokens?.edits ?? []) {
|
|
4104
|
+
if (e.file === void 0) continue;
|
|
4105
|
+
const bad = unsafePathProblem(e.file);
|
|
4106
|
+
if (bad) out.push(bad);
|
|
4107
|
+
}
|
|
4108
|
+
return [...new Set(out)];
|
|
4109
|
+
}
|
|
4110
|
+
async function linkRequestToExplorationOnGitHub(gh, n, branch, opts = {}) {
|
|
4111
|
+
if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
|
|
4112
|
+
const issue = await gh.getIssue(n);
|
|
4113
|
+
const body = withRequestExploration(issue.body, branch);
|
|
4114
|
+
if (!body) throw new GitHubError(422, `#${n} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
|
|
4115
|
+
await gh.updateIssue(n, { body });
|
|
4116
|
+
const warnings = [];
|
|
4117
|
+
if (!await gh.addLabels(n, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n}.`);
|
|
4118
|
+
let assignees = [];
|
|
4119
|
+
if (opts.assignee) {
|
|
4120
|
+
try {
|
|
4121
|
+
assignees = await gh.addAssignees(n, [opts.assignee.replace(/^@/, "")]);
|
|
4122
|
+
} catch {
|
|
4123
|
+
warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n}.`);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
return { number: n, branch, assignees, warnings };
|
|
4127
|
+
}
|
|
4128
|
+
async function assignRequestOnGitHub(gh, n, login) {
|
|
4129
|
+
const who = login.trim().replace(/^@/, "");
|
|
4130
|
+
if (!who) throw new GitHubError(422, "Pick who should work on it.");
|
|
4131
|
+
const assignees = await gh.addAssignees(n, [who]);
|
|
4132
|
+
if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
|
|
4133
|
+
return assignees;
|
|
4134
|
+
}
|
|
4135
|
+
async function declineRequestOnGitHub(gh, n, reason, by) {
|
|
4136
|
+
const problems = declineReasonProblems(reason);
|
|
4137
|
+
if (problems.length) throw new GitHubError(422, problems.join(" "));
|
|
4138
|
+
await gh.addComment(n, declineComment(reason, by));
|
|
4139
|
+
await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
|
|
4140
|
+
}
|
|
4141
|
+
const px = (v) => {
|
|
4142
|
+
if (!v) return void 0;
|
|
4143
|
+
const m = /^(-?[\d.]+)px$/.exec(v.trim());
|
|
4144
|
+
return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
|
|
4145
|
+
};
|
|
4146
|
+
function evalCalc(expr, resolve) {
|
|
4147
|
+
const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
|
|
4148
|
+
if (!body) return void 0;
|
|
4149
|
+
let missing = false;
|
|
4150
|
+
const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
|
|
4151
|
+
const v2 = resolve(path);
|
|
4152
|
+
const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
|
|
4153
|
+
if (n === void 0 || Number.isNaN(n)) missing = true;
|
|
4154
|
+
return String(n);
|
|
4155
|
+
});
|
|
4156
|
+
if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
|
|
4157
|
+
const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
|
|
4158
|
+
let i = 0;
|
|
4159
|
+
const factor = () => {
|
|
4160
|
+
const t = tokens[i++];
|
|
4161
|
+
if (t === "(") {
|
|
4162
|
+
const v2 = sum();
|
|
4163
|
+
i++;
|
|
4164
|
+
return v2;
|
|
4165
|
+
}
|
|
4166
|
+
if (t === "-") return -factor();
|
|
4167
|
+
return Number(t);
|
|
4168
|
+
};
|
|
4169
|
+
const product = () => {
|
|
4170
|
+
let v2 = factor();
|
|
4171
|
+
while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
|
|
4172
|
+
return v2;
|
|
4173
|
+
};
|
|
4174
|
+
const sum = () => {
|
|
4175
|
+
let v2 = product();
|
|
4176
|
+
while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
|
|
4177
|
+
return v2;
|
|
4178
|
+
};
|
|
4179
|
+
const v = sum();
|
|
4180
|
+
return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
|
|
4181
|
+
}
|
|
4182
|
+
const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
4183
|
+
function normColor(v) {
|
|
4184
|
+
const t = v.trim().toLowerCase();
|
|
4185
|
+
const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
|
|
4186
|
+
if (hex2) {
|
|
4187
|
+
const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
|
|
4188
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
4189
|
+
const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
|
|
4190
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
|
|
4191
|
+
}
|
|
4192
|
+
const m = /^rgba?\(([^)]*)\)$/.exec(t);
|
|
4193
|
+
if (m) {
|
|
4194
|
+
const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
|
|
4195
|
+
const [r, g, b] = parts;
|
|
4196
|
+
const a = parts[3] ?? 1;
|
|
4197
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
|
|
4198
|
+
}
|
|
4199
|
+
return t.replace(/\s+/g, "");
|
|
4200
|
+
}
|
|
4201
|
+
const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
4202
|
+
const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
|
|
4203
|
+
function withoutAddedWrappers(parts, measured) {
|
|
4204
|
+
const real = new Set(measured.parts.map((m) => m.id));
|
|
4205
|
+
const added = /* @__PURE__ */ new Map();
|
|
4206
|
+
for (const p of parts) {
|
|
4207
|
+
if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
|
|
4208
|
+
const kids = parts.filter((c) => c.parent === p.id);
|
|
4209
|
+
if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
|
|
4210
|
+
}
|
|
4211
|
+
if (!added.size) return parts;
|
|
4212
|
+
const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
|
|
4213
|
+
return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
|
|
4214
|
+
}
|
|
4215
|
+
function pairParts(staticParts, measured, o = {}) {
|
|
4216
|
+
const pairs2 = /* @__PURE__ */ new Map();
|
|
4217
|
+
const taken = /* @__PURE__ */ new Set();
|
|
4218
|
+
const mParts = new Map(measured.parts.map((p) => [p.id, p]));
|
|
4219
|
+
const sParts = new Map(staticParts.map((p) => [p.id, p]));
|
|
4220
|
+
const take = (s, m) => {
|
|
4221
|
+
pairs2.set(s, m);
|
|
4222
|
+
taken.add(m);
|
|
4223
|
+
};
|
|
4224
|
+
const ancestors = (id) => {
|
|
4225
|
+
const out = [];
|
|
4226
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
4227
|
+
for (let p = mParts.get(id)?.parent; p && !seen.has(p); p = mParts.get(p)?.parent) {
|
|
4228
|
+
seen.add(p);
|
|
4229
|
+
out.push(p);
|
|
4230
|
+
}
|
|
4231
|
+
return out;
|
|
4232
|
+
};
|
|
4233
|
+
const pairedAncestor = (p) => {
|
|
4234
|
+
const seen = /* @__PURE__ */ new Set([p.id]);
|
|
4235
|
+
for (let a = p.parent; a && !seen.has(a); a = sParts.get(a)?.parent) {
|
|
4236
|
+
if (pairs2.has(a)) return pairs2.get(a);
|
|
4237
|
+
seen.add(a);
|
|
4238
|
+
}
|
|
4239
|
+
return void 0;
|
|
4240
|
+
};
|
|
4241
|
+
const best = (p, cands) => {
|
|
4242
|
+
if (!cands.length) return void 0;
|
|
4243
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
4244
|
+
if (parent) {
|
|
4245
|
+
const direct = cands.find((c) => c.parent === parent);
|
|
4246
|
+
if (direct) return direct;
|
|
4247
|
+
}
|
|
4248
|
+
const anc = pairedAncestor(p);
|
|
4249
|
+
if (anc) {
|
|
4250
|
+
const under2 = cands.find((c) => ancestors(c.id).includes(anc));
|
|
4251
|
+
if (under2) return under2;
|
|
4252
|
+
}
|
|
4253
|
+
return cands[0];
|
|
4254
|
+
};
|
|
4255
|
+
const root = staticParts.find((p) => !p.parent);
|
|
4256
|
+
if (root) take(root.id, "root");
|
|
4257
|
+
const textOf = (p) => {
|
|
4258
|
+
if (p.characters) return normText(p.characters);
|
|
4259
|
+
const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
|
|
4260
|
+
return typeof prop?.default === "string" ? normText(prop.default) : "";
|
|
4261
|
+
};
|
|
4262
|
+
const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
|
|
4263
|
+
const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
|
|
4264
|
+
const shared = (p, m) => {
|
|
4265
|
+
const mine = new Set(classesOf(p));
|
|
4266
|
+
return mClasses(m).filter((c) => mine.has(c)).length;
|
|
4267
|
+
};
|
|
4268
|
+
const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
|
|
4269
|
+
const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
|
|
4270
|
+
for (const p of staticParts) {
|
|
4271
|
+
if (p.kind !== "text" || pairs2.has(p.id)) continue;
|
|
4272
|
+
const t = textOf(p);
|
|
4273
|
+
if (!t) continue;
|
|
4274
|
+
const hit = best(p, measured.parts.filter((m) => m.kind === "text" && !taken.has(m.id) && normText(m.characters) === t && !(classesOf(p).length && mClasses(m).length && !shared(p, m))));
|
|
4275
|
+
if (hit) take(p.id, hit.id);
|
|
4276
|
+
}
|
|
4277
|
+
const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
|
|
4278
|
+
for (const p of staticParts) {
|
|
4279
|
+
if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
|
|
4280
|
+
const sp = sParts.get(p.parent);
|
|
4281
|
+
if (!sp || sp.kind !== "frame" || !sp.parent) continue;
|
|
4282
|
+
const cls = classOf(sp);
|
|
4283
|
+
const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
|
|
4284
|
+
if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
|
|
4285
|
+
}
|
|
4286
|
+
for (const p of staticParts) {
|
|
4287
|
+
if (pairs2.has(p.id) || !classesOf(p).length) continue;
|
|
4288
|
+
if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
|
|
4289
|
+
const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
|
|
4290
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
4291
|
+
const under2 = parent ? all.filter((m) => m.parent === parent) : [];
|
|
4292
|
+
const cands = under2.length ? under2 : all;
|
|
4293
|
+
const most = Math.max(0, ...cands.map((m) => shared(p, m)));
|
|
4294
|
+
const hit = best(p, cands.filter((m) => shared(p, m) === most));
|
|
4295
|
+
if (hit) take(p.id, hit.id);
|
|
4296
|
+
}
|
|
4297
|
+
for (const p of staticParts) {
|
|
4298
|
+
if (pairs2.has(p.id)) continue;
|
|
4299
|
+
if (p.parent && !pairs2.has(p.parent)) continue;
|
|
4300
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
4301
|
+
const known = p.kind === "text" && !!textOf(p);
|
|
4302
|
+
const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
|
|
4303
|
+
const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
|
|
4304
|
+
if (hit) take(p.id, hit.id);
|
|
4305
|
+
}
|
|
4306
|
+
for (const p of staticParts) {
|
|
4307
|
+
if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
|
|
4308
|
+
const mp = pairs2.get(p.parent);
|
|
4309
|
+
const t = textOf(p);
|
|
4310
|
+
if (mp && mParts.get(mp)?.kind === "text" && staticParts.filter((x) => x.parent === p.parent).length === 1 && (!t || normText(mParts.get(mp).characters) === t)) pairs2.set(p.id, mp);
|
|
4311
|
+
}
|
|
4312
|
+
return pairs2;
|
|
4313
|
+
}
|
|
4314
|
+
function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
|
|
4315
|
+
const pairs2 = pairParts(staticParts, measured, o);
|
|
4316
|
+
const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
|
|
4317
|
+
const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
|
|
4318
|
+
const near = (a, b) => {
|
|
4319
|
+
if (a === void 0 || b === void 0) return false;
|
|
4320
|
+
const na0 = num2(a);
|
|
4321
|
+
const nb0 = num2(b);
|
|
4322
|
+
if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
|
|
4323
|
+
if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
|
|
4324
|
+
const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
|
|
4325
|
+
const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
|
|
4326
|
+
return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
|
|
4327
|
+
};
|
|
4328
|
+
for (const sp of staticParts) {
|
|
4329
|
+
const mid = pairs2.get(sp.id);
|
|
4330
|
+
if (!mid) {
|
|
4331
|
+
out.unmatchedParts.push(sp.id);
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
const sv = staticValues[sp.id] ?? {};
|
|
4335
|
+
const mv = mid ? measured.values[mid] ?? {} : {};
|
|
4336
|
+
const mr = mid ? measured.raw[mid] ?? {} : {};
|
|
4337
|
+
const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
|
|
4338
|
+
for (const f of fields) {
|
|
4339
|
+
if ((f === "width" || f === "height") && sv[f] === void 0) continue;
|
|
4340
|
+
const s = sv[f];
|
|
4341
|
+
const m = mv[f] !== void 0 ? mv[f] : mr[f];
|
|
4342
|
+
if (s === void 0) {
|
|
4343
|
+
if (m !== void 0 && m !== 0) out.measuredOnly++;
|
|
4344
|
+
continue;
|
|
4345
|
+
}
|
|
4346
|
+
if (m === void 0 && !(f in mr)) {
|
|
4347
|
+
if (s === 0 || s === null) continue;
|
|
4348
|
+
const r = typeof s === "string" && resolve ? resolve(s) : s;
|
|
4349
|
+
if (r === 0 || r === "0px" || r === null) continue;
|
|
4350
|
+
out.staticOnly++;
|
|
4351
|
+
out.diffs.push({ part: sp.id, field: f, static: s, same: false });
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4354
|
+
out.compared++;
|
|
4355
|
+
let same2 = s === m;
|
|
4356
|
+
if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
|
|
4357
|
+
if (same2 && typeof s === "string") out.sameToken++;
|
|
4358
|
+
if (!same2 && resolve) {
|
|
4359
|
+
let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
|
|
4360
|
+
const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
|
|
4361
|
+
if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
|
|
4362
|
+
same2 = near(rs, mr[f]);
|
|
4363
|
+
}
|
|
4364
|
+
if (same2) out.same++;
|
|
4365
|
+
else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
|
|
4369
|
+
const box = measured.boxes.root;
|
|
4370
|
+
if (box) out.rootSize = { static: { width: typeof root.width === "number" ? root.width : void 0, height: typeof root.height === "number" ? root.height : void 0 }, measured: { width: box.width, height: box.height } };
|
|
4371
|
+
return out;
|
|
4372
|
+
}
|
|
4373
|
+
function tokenVariablePath(name, prefix) {
|
|
4374
|
+
let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
|
|
4375
|
+
if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
|
|
4376
|
+
return parts.join("/");
|
|
4377
|
+
}
|
|
4378
|
+
function sharedTokenPrefix(names2) {
|
|
4379
|
+
if (names2.length < 20) return void 0;
|
|
4380
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4381
|
+
for (const n2 of names2) {
|
|
4382
|
+
const first = n2.replace(/^--/, "").split("-")[0];
|
|
4383
|
+
counts.set(first, (counts.get(first) ?? 0) + 1);
|
|
4384
|
+
}
|
|
4385
|
+
const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
4386
|
+
return top && n / names2.length >= 0.6 ? top : void 0;
|
|
4387
|
+
}
|
|
4388
|
+
function markUsedTokens(tokens, stories) {
|
|
4389
|
+
const byName = new Map(tokens.properties.map((p) => [p.name, p]));
|
|
4390
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4391
|
+
const use = (name) => {
|
|
4392
|
+
if (seen.has(name)) return;
|
|
4393
|
+
seen.add(name);
|
|
4394
|
+
const p = byName.get(name);
|
|
4395
|
+
if (!p) return;
|
|
4396
|
+
p.used = true;
|
|
4397
|
+
for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
|
|
4398
|
+
if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
|
|
4399
|
+
};
|
|
4400
|
+
for (const s of stories)
|
|
4401
|
+
for (const spec of [s.spec, ...s.instances ?? []])
|
|
4402
|
+
for (const part of Object.values(spec?.tokens ?? {}))
|
|
4403
|
+
for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
|
|
4404
|
+
const used = tokens.properties.filter((p) => p.used).length;
|
|
4405
|
+
tokens.counts.used = used;
|
|
4406
|
+
return used;
|
|
4407
|
+
}
|
|
4408
|
+
class MeasuredSpecsError extends Error {
|
|
4409
|
+
}
|
|
4410
|
+
const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4411
|
+
function parseMeasuredSpecsFile(input) {
|
|
4412
|
+
let data = input;
|
|
4413
|
+
if (typeof input === "string") {
|
|
4414
|
+
try {
|
|
4415
|
+
data = JSON.parse(input);
|
|
4416
|
+
} catch (e) {
|
|
4417
|
+
throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
|
|
4421
|
+
if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
|
|
4422
|
+
if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
|
|
4423
|
+
if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
|
|
4424
|
+
data.stories.forEach((s, i) => {
|
|
4425
|
+
if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
|
|
4426
|
+
const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
|
|
4427
|
+
for (const sp of specs) {
|
|
4428
|
+
if (!isObj$1(sp) || !Array.isArray(sp.parts) || !isObj$1(sp.values)) throw new MeasuredSpecsError(`measured specs: story ${s.id} has a spec without parts and values`);
|
|
4429
|
+
for (const p of sp.parts) if (!isObj$1(p) || typeof p.id !== "string" || typeof p.kind !== "string") throw new MeasuredSpecsError(`measured specs: story ${s.id} has a part without id or kind`);
|
|
4430
|
+
}
|
|
4431
|
+
if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
|
|
4432
|
+
});
|
|
4433
|
+
if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
|
|
4434
|
+
if (data.tokens !== void 0) {
|
|
4435
|
+
const t = data.tokens;
|
|
4436
|
+
if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
|
|
4437
|
+
for (const p of t.properties) if (!isObj$1(p) || typeof p.name !== "string" || typeof p.path !== "string" || p.tier !== "primitive" && p.tier !== "semantic") throw new MeasuredSpecsError("measured specs: a token has no name, path or tier");
|
|
4438
|
+
}
|
|
4439
|
+
return data;
|
|
4440
|
+
}
|
|
4441
|
+
const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
|
|
4442
|
+
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
4443
|
+
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
4444
|
+
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
4445
|
+
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
4318
4446
|
];
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4447
|
+
const D50_TO_D65 = [
|
|
4448
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
4449
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
4450
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
4451
|
+
];
|
|
4452
|
+
const LINEAR_P3_TO_XYZ_D65 = [
|
|
4453
|
+
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
4454
|
+
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
4455
|
+
[0, 0.04511338185890264, 1.043944368900976]
|
|
4456
|
+
];
|
|
4457
|
+
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
4458
|
+
const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
|
|
4459
|
+
const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
|
|
4460
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
4461
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
4462
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
4463
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
4464
|
+
return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
4465
|
+
}
|
|
4466
|
+
function labToXyzD50(L, a, b) {
|
|
4467
|
+
const e = 216 / 24389;
|
|
4468
|
+
const k = 24389 / 27;
|
|
4469
|
+
const fy = (L + 16) / 116;
|
|
4470
|
+
const fx = a / 500 + fy;
|
|
4471
|
+
const fz = fy - b / 200;
|
|
4472
|
+
const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
4473
|
+
const y = L > k * e ? fy ** 3 : L / k;
|
|
4474
|
+
const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
4475
|
+
return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
|
|
4476
|
+
}
|
|
4477
|
+
function num(s, full = 1) {
|
|
4478
|
+
if (s === void 0 || s === "none") return 0;
|
|
4479
|
+
if (s.endsWith("%")) return parseFloat(s) / 100 * full;
|
|
4480
|
+
if (/deg$/.test(s)) return parseFloat(s);
|
|
4481
|
+
if (/turn$/.test(s)) return parseFloat(s) * 360;
|
|
4482
|
+
if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
|
|
4483
|
+
return parseFloat(s);
|
|
4484
|
+
}
|
|
4485
|
+
function linearSrgb(fn, args) {
|
|
4486
|
+
switch (fn) {
|
|
4487
|
+
case "oklab":
|
|
4488
|
+
return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
|
|
4489
|
+
case "oklch": {
|
|
4490
|
+
const c = num(args[1], 0.4);
|
|
4491
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
4492
|
+
return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
|
|
4332
4493
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
const
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4494
|
+
case "lab":
|
|
4495
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
|
|
4496
|
+
case "lch": {
|
|
4497
|
+
const c = num(args[1], 150);
|
|
4498
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
4499
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
|
|
4500
|
+
}
|
|
4501
|
+
case "color": {
|
|
4502
|
+
const [space, ...c] = args;
|
|
4503
|
+
const v = [num(c[0]), num(c[1]), num(c[2])];
|
|
4504
|
+
if (space === "srgb") return v.map(toLinear);
|
|
4505
|
+
if (space === "srgb-linear") return v;
|
|
4506
|
+
if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
|
|
4507
|
+
if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
|
|
4508
|
+
if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
|
|
4509
|
+
return void 0;
|
|
4345
4510
|
}
|
|
4346
|
-
h[0] = h[0] + a | 0;
|
|
4347
|
-
h[1] = h[1] + b | 0;
|
|
4348
|
-
h[2] = h[2] + c | 0;
|
|
4349
|
-
h[3] = h[3] + d | 0;
|
|
4350
|
-
h[4] = h[4] + e | 0;
|
|
4351
|
-
h[5] = h[5] + f | 0;
|
|
4352
|
-
h[6] = h[6] + g | 0;
|
|
4353
|
-
h[7] = h[7] + hh | 0;
|
|
4354
4511
|
}
|
|
4355
|
-
return
|
|
4512
|
+
return void 0;
|
|
4356
4513
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
const
|
|
4360
|
-
const
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4514
|
+
const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
|
|
4515
|
+
function cssColorToRgb(fn, inner) {
|
|
4516
|
+
const [main, alpha] = inner.split("/");
|
|
4517
|
+
const args = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
4518
|
+
const lin = linearSrgb(fn.toLowerCase(), args);
|
|
4519
|
+
if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
|
|
4520
|
+
const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
|
|
4521
|
+
const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
|
|
4522
|
+
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
|
|
4523
|
+
}
|
|
4524
|
+
function cssColorsToRgb(text) {
|
|
4525
|
+
if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
|
|
4526
|
+
return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
|
|
4527
|
+
}
|
|
4528
|
+
function withSrgbColors(value) {
|
|
4529
|
+
if (typeof value === "string") return cssColorsToRgb(value);
|
|
4530
|
+
if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
|
|
4531
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
|
|
4532
|
+
return value;
|
|
4533
|
+
}
|
|
4534
|
+
function parseBoxShadow(css) {
|
|
4535
|
+
if (!css || css.trim() === "none") return [];
|
|
4536
|
+
const out = [];
|
|
4537
|
+
for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
|
|
4538
|
+
const color = /rgba?\(([^)]*)\)/i.exec(one);
|
|
4539
|
+
const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
|
|
4540
|
+
const a = c[3] ?? 1;
|
|
4541
|
+
const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
|
|
4542
|
+
if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
|
|
4543
|
+
out.push({ inset: /\binset\b/i.test(one), x: lengths[0], y: lengths[1], blur: lengths[2] ?? 0, spread: lengths[3] ?? 0, color: { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a } });
|
|
4380
4544
|
}
|
|
4381
|
-
return
|
|
4545
|
+
return out;
|
|
4546
|
+
}
|
|
4547
|
+
function parseTier(labels, body) {
|
|
4548
|
+
const found = [];
|
|
4549
|
+
for (const l of labels) {
|
|
4550
|
+
const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
|
|
4551
|
+
if (m) found.push(Number(m[1]));
|
|
4552
|
+
}
|
|
4553
|
+
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
4554
|
+
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
4555
|
+
return found.length ? Math.max(...found) : null;
|
|
4556
|
+
}
|
|
4557
|
+
function claudeStage(labels) {
|
|
4558
|
+
const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
|
|
4559
|
+
if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
|
|
4560
|
+
if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
|
|
4561
|
+
return null;
|
|
4562
|
+
}
|
|
4563
|
+
const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
|
|
4564
|
+
function pullState(p) {
|
|
4565
|
+
if (p.merged_at || p.merged) return "merged";
|
|
4566
|
+
if (p.state && p.state !== "open") return "closed";
|
|
4567
|
+
if (isDraft(p)) return "draft";
|
|
4568
|
+
return "open";
|
|
4569
|
+
}
|
|
4570
|
+
const PULL_STATE_PILL = {
|
|
4571
|
+
merged: { label: "Merged", cls: "" },
|
|
4572
|
+
closed: { label: "Closed", cls: "" },
|
|
4573
|
+
draft: { label: "Draft", cls: "" },
|
|
4574
|
+
open: { label: "Open", cls: "" }
|
|
4575
|
+
};
|
|
4576
|
+
function openPulls(pulls) {
|
|
4577
|
+
return pulls.filter((p) => {
|
|
4578
|
+
const s = pullState(p);
|
|
4579
|
+
return s === "open" || s === "draft";
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
function isPlatformRefusal(s) {
|
|
4583
|
+
return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
|
|
4584
|
+
}
|
|
4585
|
+
const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
4586
|
+
function summarizeCi(runs, statuses = []) {
|
|
4587
|
+
const failed = [];
|
|
4588
|
+
const pending = [];
|
|
4589
|
+
let passed = 0;
|
|
4590
|
+
for (const r of runs) {
|
|
4591
|
+
if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
|
|
4592
|
+
else if (r.conclusion && OK.has(r.conclusion)) passed++;
|
|
4593
|
+
else failed.push({ name: r.name, url: r.html_url });
|
|
4594
|
+
}
|
|
4595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4596
|
+
for (const s of statuses) {
|
|
4597
|
+
if (seen.has(s.context)) continue;
|
|
4598
|
+
seen.add(s.context);
|
|
4599
|
+
if (isPlatformRefusal(s)) continue;
|
|
4600
|
+
if (s.state === "success") passed++;
|
|
4601
|
+
else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
4602
|
+
else failed.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
4603
|
+
}
|
|
4604
|
+
const total = passed + failed.length + pending.length;
|
|
4605
|
+
const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
|
|
4606
|
+
return { state, total, passed, failed, pending };
|
|
4607
|
+
}
|
|
4608
|
+
const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
|
|
4609
|
+
function summarizeApprovals(reviews, owners, author, rules = {}) {
|
|
4610
|
+
const latest = /* @__PURE__ */ new Map();
|
|
4611
|
+
const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
4612
|
+
for (const r of sorted) {
|
|
4613
|
+
const login = r.user?.login;
|
|
4614
|
+
if (!login || login === author) continue;
|
|
4615
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
|
|
4616
|
+
}
|
|
4617
|
+
const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
|
|
4618
|
+
const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
|
|
4619
|
+
const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
|
|
4620
|
+
const d = lower2(owners.design);
|
|
4621
|
+
const t = lower2(owners.tech);
|
|
4622
|
+
const a = lower2(approvers);
|
|
4623
|
+
let design;
|
|
4624
|
+
let tech;
|
|
4625
|
+
const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
|
|
4626
|
+
if (d.length || t.length) {
|
|
4627
|
+
design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
|
|
4628
|
+
const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
|
|
4629
|
+
const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
|
|
4630
|
+
tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
|
|
4631
|
+
} else {
|
|
4632
|
+
design = a.length >= 1;
|
|
4633
|
+
tech = a.length >= 2;
|
|
4634
|
+
}
|
|
4635
|
+
return { approvers, changesRequested, design, tech, owner };
|
|
4636
|
+
}
|
|
4637
|
+
function lookAgainReviewers(reviews, headSha, author) {
|
|
4638
|
+
if (!headSha) return [];
|
|
4639
|
+
const latest = /* @__PURE__ */ new Map();
|
|
4640
|
+
const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
4641
|
+
for (const r of sorted) {
|
|
4642
|
+
const login = r.user?.login;
|
|
4643
|
+
if (!login || login === author) continue;
|
|
4644
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
|
|
4645
|
+
}
|
|
4646
|
+
const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
|
|
4647
|
+
if (!asked.length) return [];
|
|
4648
|
+
return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
|
|
4649
|
+
}
|
|
4650
|
+
function withSuggestionApprovals(pr, suggestion, prTier) {
|
|
4651
|
+
if (!suggestion) return pr;
|
|
4652
|
+
const s = suggestion.approvals;
|
|
4653
|
+
if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
|
|
4654
|
+
const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
|
|
4655
|
+
if (!covered) return pr;
|
|
4656
|
+
const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
|
|
4657
|
+
return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
|
|
4658
|
+
}
|
|
4659
|
+
function withFigmaPushApprovals(pr, push) {
|
|
4660
|
+
if (!push || push.scope === "outside") return pr;
|
|
4661
|
+
const tech = pr.tech || push.scope === "token-values";
|
|
4662
|
+
return { ...pr, design: true, tech, owner: true };
|
|
4663
|
+
}
|
|
4664
|
+
function withTokenValueApprovals(pr, valuesOnly) {
|
|
4665
|
+
if (!valuesOnly) return pr;
|
|
4666
|
+
return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
|
|
4382
4667
|
}
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
all.set(head);
|
|
4388
|
-
all.set(body, head.length);
|
|
4389
|
-
return sha1Bytes(all);
|
|
4668
|
+
const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
4669
|
+
function canOverride(login, policy) {
|
|
4670
|
+
if (!login || !policy?.allowed.length) return false;
|
|
4671
|
+
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
4390
4672
|
}
|
|
4391
|
-
function
|
|
4392
|
-
|
|
4673
|
+
function overrideHolders(policy, owners) {
|
|
4674
|
+
const allowed = policy?.allowed ?? [];
|
|
4675
|
+
const all = [...owners.design, ...owners.tech];
|
|
4676
|
+
if (!all.length || all.some((o) => o.includes("/"))) return allowed;
|
|
4677
|
+
const named = all.map(normLogin);
|
|
4678
|
+
return allowed.filter((l) => named.includes(normLogin(l)));
|
|
4393
4679
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4680
|
+
const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
|
|
4681
|
+
function mergeGate(input) {
|
|
4682
|
+
const blocking = [];
|
|
4683
|
+
const missing = [];
|
|
4684
|
+
const approvalReasons = [];
|
|
4685
|
+
const lifecycle = pullState(input);
|
|
4686
|
+
if (lifecycle === "merged" || lifecycle === "closed") {
|
|
4687
|
+
return { allowed: false, reasons: [lifecycle === "merged" ? "This branch is already merged." : "This branch is closed."], overridable: false, missing: [], canShip: !input.viewer || canShip(input.viewer, input.ship) };
|
|
4688
|
+
}
|
|
4689
|
+
if (input.exploration) blocking.push("It is an exploration: say it is done exploring first, then developers take it.");
|
|
4690
|
+
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
4691
|
+
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
4692
|
+
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
4693
|
+
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
4694
|
+
const tier = input.tier ?? 3;
|
|
4695
|
+
if (tier >= 2) {
|
|
4696
|
+
if (!input.approvals.design) {
|
|
4697
|
+
approvalReasons.push("It needs a design owner approval.");
|
|
4698
|
+
missing.push("design approval");
|
|
4401
4699
|
}
|
|
4402
|
-
|
|
4700
|
+
if (!input.approvals.tech && !input.approvals.valuesOnly) {
|
|
4701
|
+
approvalReasons.push("It needs a tech owner approval.");
|
|
4702
|
+
missing.push("tech approval");
|
|
4703
|
+
}
|
|
4704
|
+
} else if (!hasOwnerApproval(input.approvals)) {
|
|
4705
|
+
approvalReasons.push("It needs one owner approval.");
|
|
4706
|
+
missing.push("approval");
|
|
4403
4707
|
}
|
|
4404
|
-
|
|
4708
|
+
const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
|
|
4709
|
+
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
4710
|
+
const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
4711
|
+
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
4712
|
+
const allowed = reasons.length === 0;
|
|
4713
|
+
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
4714
|
+
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
4405
4715
|
}
|
|
4406
|
-
function
|
|
4407
|
-
|
|
4408
|
-
|
|
4716
|
+
function branchVerdict(input) {
|
|
4717
|
+
const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
|
|
4718
|
+
const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
|
|
4719
|
+
const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
|
|
4720
|
+
const override = ownerOverridePolicy(input.override, input.owners);
|
|
4721
|
+
const state = input.state ?? "open";
|
|
4722
|
+
const draft = isDraft(input);
|
|
4723
|
+
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
4724
|
+
const gate = input.ci && approvals ? mergeGate({ tier: input.tier, ci: input.ci, approvals, draft, mergeable: input.mergeable, state, merged_at: input.merged_at, viewer: input.viewer, override, exploration: input.exploration, ship: input.ship }) : null;
|
|
4725
|
+
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
4726
|
+
const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
|
|
4727
|
+
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
|
|
4728
|
+
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
4729
|
+
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
4409
4730
|
}
|
|
4410
|
-
|
|
4411
|
-
|
|
4731
|
+
const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
|
|
4732
|
+
const OVERRIDE_HEADING = "**Bridge owner override**";
|
|
4733
|
+
function overrideReasonMin(policy) {
|
|
4734
|
+
return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
|
|
4412
4735
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
config: "Settings"
|
|
4420
|
-
};
|
|
4421
|
-
function suggestionSubject(p) {
|
|
4422
|
-
if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
|
|
4423
|
-
if (p.kind === "docs" && p.docs?.length) {
|
|
4424
|
-
const names2 = [...new Set(p.docs.map((d) => d.component))];
|
|
4425
|
-
return names2.length === 1 ? names2[0] : KIND_SUBJECT.docs;
|
|
4426
|
-
}
|
|
4427
|
-
return KIND_SUBJECT[p.kind];
|
|
4736
|
+
function overrideReasonProblem(reason, policy) {
|
|
4737
|
+
const min = overrideReasonMin(policy);
|
|
4738
|
+
const n = reason.trim().length;
|
|
4739
|
+
if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
|
|
4740
|
+
if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
|
|
4741
|
+
return null;
|
|
4428
4742
|
}
|
|
4429
|
-
function
|
|
4430
|
-
const
|
|
4431
|
-
return
|
|
4743
|
+
function overrideComment(login, reason, missing) {
|
|
4744
|
+
const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
|
|
4745
|
+
return `${OVERRIDE_HEADING}
|
|
4746
|
+
|
|
4747
|
+
Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
|
|
4432
4748
|
}
|
|
4433
|
-
function
|
|
4434
|
-
return [
|
|
4435
|
-
.../* @__PURE__ */ new Set([
|
|
4436
|
-
SUGGESTION_LABEL,
|
|
4437
|
-
`suggestion: ${p.kind}`,
|
|
4438
|
-
`source: ${p.source}`,
|
|
4439
|
-
`tier: ${p.tier.tier}`,
|
|
4440
|
-
SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
|
|
4441
|
-
...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
|
|
4442
|
-
...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
|
|
4443
|
-
...p.links?.exploration ? [EXPLORATION_LABEL] : [],
|
|
4444
|
-
...opts.labels ?? []
|
|
4445
|
-
])
|
|
4446
|
-
];
|
|
4749
|
+
function overrideRefusedComment(body, message) {
|
|
4750
|
+
return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
|
|
4447
4751
|
}
|
|
4448
|
-
function
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
return
|
|
4752
|
+
function parseOverrideComment(body) {
|
|
4753
|
+
const text = (body ?? "").trim();
|
|
4754
|
+
if (!text.startsWith(OVERRIDE_HEADING)) return null;
|
|
4755
|
+
const m = text.match(/Owner override by @([\w-]+): ([\s\S]*?)\. Missing: ([^.]*)\. (Merged from Bridge\.|Merge refused by GitHub)/);
|
|
4756
|
+
if (!m) return null;
|
|
4757
|
+
return { login: m[1], reason: m[2], missing: m[3].split(/,\s*/).filter(Boolean), merged: m[4].startsWith("Merged") };
|
|
4454
4758
|
}
|
|
4455
|
-
function
|
|
4456
|
-
|
|
4759
|
+
function allowedMergeMethods(repo) {
|
|
4760
|
+
const out = [];
|
|
4761
|
+
if (repo?.allow_squash_merge !== false) out.push("squash");
|
|
4762
|
+
if (repo?.allow_merge_commit !== false) out.push("merge");
|
|
4763
|
+
if (repo?.allow_rebase_merge !== false) out.push("rebase");
|
|
4764
|
+
return out;
|
|
4457
4765
|
}
|
|
4458
|
-
function
|
|
4459
|
-
|
|
4460
|
-
if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
|
|
4461
|
-
return (p.files ?? []).every((f) => f.path.endsWith(".md"));
|
|
4766
|
+
function hasWriteAccess(perms) {
|
|
4767
|
+
return Boolean(perms && (perms.admin || perms.maintain || perms.push));
|
|
4462
4768
|
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4769
|
+
const REVIEW_STATUS_ORDER = {
|
|
4770
|
+
"ci-failing": 0,
|
|
4771
|
+
conflict: 1,
|
|
4772
|
+
"changes-requested": 2,
|
|
4773
|
+
"needs-design": 3,
|
|
4774
|
+
"needs-tech": 4,
|
|
4775
|
+
"needs-approval": 5,
|
|
4776
|
+
ready: 6,
|
|
4777
|
+
"ci-running": 7,
|
|
4778
|
+
checking: 8,
|
|
4779
|
+
unknown: 9,
|
|
4780
|
+
draft: 10,
|
|
4781
|
+
merged: 11,
|
|
4782
|
+
closed: 12
|
|
4783
|
+
};
|
|
4784
|
+
function reviewStatusKind(input) {
|
|
4785
|
+
const out = (kind) => kind;
|
|
4786
|
+
const lifecycle = pullState(input);
|
|
4787
|
+
if (lifecycle === "merged") return out("merged");
|
|
4788
|
+
if (lifecycle === "closed") return out("closed");
|
|
4789
|
+
if (lifecycle === "draft") return out("draft");
|
|
4790
|
+
if (input.ci?.state === "fail") return out("ci-failing");
|
|
4791
|
+
if (input.approvals?.changesRequested.length) return out("changes-requested");
|
|
4792
|
+
if (input.mergeable === false) return out("conflict");
|
|
4793
|
+
if (input.ci?.state === "pending") return out("ci-running");
|
|
4794
|
+
if (!input.ci || !input.approvals) return out("unknown");
|
|
4795
|
+
const tier = input.tier ?? 3;
|
|
4796
|
+
if (tier >= 2) {
|
|
4797
|
+
if (!input.approvals.design) return out("needs-design");
|
|
4798
|
+
if (!input.approvals.tech) return out("needs-tech");
|
|
4799
|
+
} else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
|
|
4800
|
+
if (input.mergeable === null) return out("checking");
|
|
4801
|
+
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: isDraft(input), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
|
|
4802
|
+
return gate.allowed ? out("ready") : out("unknown");
|
|
4469
4803
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4804
|
+
const REVIEW_STATUS_LABEL = {
|
|
4805
|
+
merged: "Merged",
|
|
4806
|
+
closed: "Closed",
|
|
4807
|
+
draft: "Draft",
|
|
4808
|
+
"ci-failing": "Blocked by tests",
|
|
4809
|
+
"changes-requested": "Changes requested",
|
|
4810
|
+
"ci-running": "Tests running",
|
|
4811
|
+
"needs-design": "Needs design approval",
|
|
4812
|
+
"needs-tech": "Needs tech approval",
|
|
4813
|
+
"needs-approval": "Needs approval",
|
|
4814
|
+
conflict: "Conflict with main",
|
|
4815
|
+
checking: "Checking for conflicts",
|
|
4816
|
+
ready: "Ready to merge",
|
|
4817
|
+
unknown: "Status unknown"
|
|
4818
|
+
};
|
|
4819
|
+
const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
|
|
4820
|
+
function reviewStatusLabel(kind, lookAgain) {
|
|
4821
|
+
return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
|
|
4472
4822
|
}
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4823
|
+
const CI_STATE_LABEL = {
|
|
4824
|
+
pass: "Tests pass",
|
|
4825
|
+
fail: "Tests fail",
|
|
4826
|
+
pending: "Tests running",
|
|
4827
|
+
none: "No automated checks"
|
|
4828
|
+
};
|
|
4829
|
+
const BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
|
|
4830
|
+
const isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
|
|
4831
|
+
function isAutomationPull(p) {
|
|
4832
|
+
return isReleasePull(p) || BOTS.has((p.user?.login ?? "").toLowerCase());
|
|
4476
4833
|
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
if (status === "outdated") return { add: [], remove: [] };
|
|
4480
|
-
const want = suggestionStateLabel(status);
|
|
4481
|
-
return {
|
|
4482
|
-
add: want && !labels.includes(want) ? [want] : [],
|
|
4483
|
-
remove: SUGGESTION_STATUS_LABELS.filter((l) => l !== want && labels.includes(l))
|
|
4484
|
-
};
|
|
4834
|
+
function reviewQueue(pulls) {
|
|
4835
|
+
return openPulls(pulls).filter((p) => !isAutomationPull(p) && !isHeldExploration(p));
|
|
4485
4836
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
const
|
|
4491
|
-
|
|
4492
|
-
const
|
|
4493
|
-
|
|
4494
|
-
if (!rows.length) return [];
|
|
4495
|
-
const shown = rows.slice(0, max);
|
|
4496
|
-
return [
|
|
4497
|
-
`| ${head.join(" | ")} |`,
|
|
4498
|
-
`| ${head.map(() => "---").join(" | ")} |`,
|
|
4499
|
-
...shown.map((r) => `| ${r.map((c) => cell(c)).join(" | ")} |`),
|
|
4500
|
-
...rows.length > max ? ["", `and ${rows.length - max} more`] : []
|
|
4501
|
-
];
|
|
4837
|
+
function releasePull(pulls) {
|
|
4838
|
+
return openPulls(pulls).find((p) => isReleasePull(p)) ?? null;
|
|
4839
|
+
}
|
|
4840
|
+
function effectiveTier(labels, body, files) {
|
|
4841
|
+
const labelled = parseTier(labels, body);
|
|
4842
|
+
if (!files) return labelled;
|
|
4843
|
+
const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
|
|
4844
|
+
return Math.max(labelled ?? 0, computed);
|
|
4502
4845
|
}
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
if (
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show$1(e.value)]);
|
|
4514
|
-
out.push(...table(["Token", "Edit", "Value"], rows, max), "");
|
|
4515
|
-
}
|
|
4516
|
-
if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
|
|
4517
|
-
if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
|
|
4518
|
-
if (p.files?.length) {
|
|
4519
|
-
const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
|
|
4520
|
-
out.push(...table(["File", "Change"], rows, max), "");
|
|
4521
|
-
}
|
|
4522
|
-
return out;
|
|
4846
|
+
const norm$1 = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
4847
|
+
function reopenEligibility(x) {
|
|
4848
|
+
if (x.state !== "closed" || !x.viewer) return { kind: "none" };
|
|
4849
|
+
const isAuthor = norm$1(x.viewer) === norm$1(x.author);
|
|
4850
|
+
const isOwner = x.owners.some((o) => norm$1(o) === norm$1(x.viewer));
|
|
4851
|
+
if (!isAuthor && !isOwner) return { kind: "none" };
|
|
4852
|
+
if (!x.canWrite && !isAuthor) return { kind: "none" };
|
|
4853
|
+
if (x.branch === "exists") return { kind: "reopen" };
|
|
4854
|
+
if (x.branch === "gone") return { kind: "gone" };
|
|
4855
|
+
return { kind: "none" };
|
|
4523
4856
|
}
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
return [
|
|
4529
|
-
"## Checks when suggested",
|
|
4530
|
-
"",
|
|
4531
|
-
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
4532
|
-
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
4533
|
-
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
|
|
4534
|
-
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
4535
|
-
"",
|
|
4536
|
-
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
4537
|
-
];
|
|
4857
|
+
const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
|
|
4858
|
+
const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
|
|
4859
|
+
function requiredApprovals(tier, valuesOnly) {
|
|
4860
|
+
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false, ...valuesOnly ? { valuesOnly: true } : {} }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4538
4861
|
}
|
|
4539
|
-
function
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell(l.neededBy.title)}](${l.neededBy.url})`);
|
|
4543
|
-
if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
|
|
4544
|
-
const key = l.figma?.fileKey;
|
|
4545
|
-
const nodes = (l.figma?.nodes ?? []).filter((n) => n && typeof n.nodeId === "string");
|
|
4546
|
-
for (const n of nodes) {
|
|
4547
|
-
const url = n.url ?? (key ? figmaNodeUrl(key, n.nodeId) : void 0);
|
|
4548
|
-
const what = `${n.state === "before" ? "Before" : n.state === "after" ? "After" : "Node"}${n.name ? ` (${cell(n.name)})` : ""}`;
|
|
4549
|
-
out.push(`- ${what}: ${url ? `[node ${n.nodeId}](${url})` : `node ${n.nodeId}`}`);
|
|
4550
|
-
}
|
|
4551
|
-
if (key && (nodes.length || l.figma?.nodeId)) {
|
|
4552
|
-
const ids = nodes.length ? nodes.map((n) => n.nodeId) : [l.figma.nodeId];
|
|
4553
|
-
out.push(
|
|
4554
|
-
`- Claude Code reads this design straight from Figma: the Figma MCP server (\`get_design_context\` with file key \`${key}\` and node ${ids.map((i) => `\`${i}\``).join(", ")}) or the REST API (\`GET https://api.figma.com/v1/files/${key}/nodes?ids=${ids.map(encodeURIComponent).join(",")}\`). No image hosting is needed.`
|
|
4555
|
-
);
|
|
4556
|
-
}
|
|
4557
|
-
if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
|
|
4558
|
-
if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
|
|
4559
|
-
for (const v of p.previews ?? []) out.push(`- ${cell(v.label)}: ${v.url}`);
|
|
4560
|
-
return out.length ? ["## Previews and links", "", ...out, ""] : [];
|
|
4862
|
+
function pendingApprovals(tier, approvals) {
|
|
4863
|
+
if (tier == null || !approvals) return [];
|
|
4864
|
+
return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4561
4865
|
}
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
...details?.trim() ? [safeText(details.trim()), ""] : [],
|
|
4570
|
-
...checksSection(p, max),
|
|
4571
|
-
...linksSection(p),
|
|
4572
|
-
"## How to apply",
|
|
4573
|
-
"",
|
|
4574
|
-
"- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
|
|
4575
|
-
'- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
|
|
4576
|
-
` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
|
|
4577
|
-
"",
|
|
4578
|
-
"The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
|
|
4579
|
-
"",
|
|
4580
|
-
`<sub>Suggested from ${sourceLabel(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
|
|
4581
|
-
].join("\n");
|
|
4866
|
+
const uniqLogins = (xs) => xs.filter((x, i) => xs.findIndex((y) => normLogin(y) === normLogin(x)) === i);
|
|
4867
|
+
function eligibleReviewers(role, p) {
|
|
4868
|
+
const raw = role === "design" ? p.owners.design : role === "tech" ? p.owners.tech : uniqLogins([...p.owners.design, ...p.owners.tech]);
|
|
4869
|
+
const listed = raw.map((x) => x.replace(/^@/, ""));
|
|
4870
|
+
const approvers = (p.approvers ?? []).map(normLogin);
|
|
4871
|
+
const logins = listed.filter((l) => normLogin(l) !== normLogin(p.author) && !approvers.includes(normLogin(l)));
|
|
4872
|
+
return { open: listed.length === 0, logins, listed };
|
|
4582
4873
|
}
|
|
4583
|
-
function
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
}
|
|
4588
|
-
const h = humanPart(p, void 0, 3, repo);
|
|
4589
|
-
if (h.length <= SUGGESTION_LIMITS.human) return h;
|
|
4590
|
-
const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
|
|
4591
|
-
return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
|
|
4874
|
+
function reviewersToAsk(x) {
|
|
4875
|
+
const roles = pendingApprovals(x.tier, x.approvals);
|
|
4876
|
+
const all = roles.flatMap((r) => eligibleReviewers(r, { owners: x.owners, author: x.author, approvers: x.approvals.approvers }).logins);
|
|
4877
|
+
return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
|
|
4592
4878
|
}
|
|
4593
|
-
const
|
|
4594
|
-
|
|
4595
|
-
const
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
function payloadBlock(json, sha, parts) {
|
|
4600
|
-
return [
|
|
4601
|
-
"<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
|
|
4602
|
-
"",
|
|
4603
|
-
`<!-- ${MARK}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
|
|
4604
|
-
fenced(json),
|
|
4605
|
-
"",
|
|
4606
|
-
"</details>"
|
|
4607
|
-
].join("\n");
|
|
4879
|
+
const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6}$/;
|
|
4880
|
+
function parseJiraKey(input) {
|
|
4881
|
+
const t = (input ?? "").trim().toUpperCase();
|
|
4882
|
+
if (!t) return null;
|
|
4883
|
+
if (!JIRA_KEY.test(t)) throw new Error("Write the Jira ticket as its key, like DS-42, or leave it empty.");
|
|
4884
|
+
return t;
|
|
4608
4885
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
`<!-- ${MARK}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
|
|
4614
|
-
fenced(json),
|
|
4615
|
-
"",
|
|
4616
|
-
"</details>"
|
|
4617
|
-
].join("\n");
|
|
4886
|
+
const isJiraKey = (input) => JIRA_KEY.test(input.trim().toUpperCase());
|
|
4887
|
+
const LEADING_KEY = /^\[?([A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6})\]?[:\s]\s*/;
|
|
4888
|
+
function jiraKeyOf(title) {
|
|
4889
|
+
return LEADING_KEY.exec(title.trim())?.[1] ?? null;
|
|
4618
4890
|
}
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
const c = s.charCodeAt(i);
|
|
4624
|
-
let w = 1;
|
|
4625
|
-
let len = 1;
|
|
4626
|
-
if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
|
|
4627
|
-
else if (c < 32) len = 6;
|
|
4628
|
-
else if (c >= 55296 && c <= 56319) {
|
|
4629
|
-
const d = s.charCodeAt(i + 1);
|
|
4630
|
-
if (d >= 56320 && d <= 57343) {
|
|
4631
|
-
w = 2;
|
|
4632
|
-
len = 2;
|
|
4633
|
-
} else len = 6;
|
|
4634
|
-
} else if (c >= 56320 && c <= 57343) len = 6;
|
|
4635
|
-
if (used + len > budget) break;
|
|
4636
|
-
used += len;
|
|
4637
|
-
i += w;
|
|
4638
|
-
}
|
|
4639
|
-
return i;
|
|
4891
|
+
const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
|
|
4892
|
+
function titleWithJiraKey(title, key) {
|
|
4893
|
+
if (!key) return title;
|
|
4894
|
+
return `${key} ${withoutJiraKey(title)}`;
|
|
4640
4895
|
}
|
|
4641
|
-
function
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
${
|
|
4654
|
-
|
|
4655
|
-
}
|
|
4656
|
-
|
|
4657
|
-
const parts = [];
|
|
4658
|
-
const firstPart = /* @__PURE__ */ new Map();
|
|
4659
|
-
const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
|
|
4660
|
-
const emptyOverhead = partJson([], 9, 9).length;
|
|
4661
|
-
let cur = [];
|
|
4662
|
-
let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
4663
|
-
for (const { f, i } of moved) {
|
|
4664
|
-
const text = f.content;
|
|
4665
|
-
let at = 0;
|
|
4666
|
-
while (at < text.length) {
|
|
4667
|
-
const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
|
|
4668
|
-
if (room - entryOverhead < 256) {
|
|
4669
|
-
parts.push(cur);
|
|
4670
|
-
cur = [];
|
|
4671
|
-
room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
4672
|
-
}
|
|
4673
|
-
const end = escapedEnd(text, at, room - entryOverhead);
|
|
4674
|
-
if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4675
|
-
if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
|
|
4676
|
-
const chunk = { i, path: f.path, text: text.slice(at, end) };
|
|
4677
|
-
cur.push(chunk);
|
|
4678
|
-
room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
|
|
4679
|
-
at = end;
|
|
4680
|
-
}
|
|
4681
|
-
}
|
|
4682
|
-
if (cur.length) parts.push(cur);
|
|
4683
|
-
if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4684
|
-
const skeleton = {
|
|
4685
|
-
...full,
|
|
4686
|
-
files: (full.files ?? []).map((f, i) => {
|
|
4687
|
-
if (!firstPart.has(i)) return f;
|
|
4688
|
-
const { content: _content, ...rest } = f;
|
|
4689
|
-
return { ...rest, part: firstPart.get(i) };
|
|
4690
|
-
})
|
|
4691
|
-
};
|
|
4692
|
-
const skeletonJson = fit(skeleton);
|
|
4693
|
-
if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4694
|
-
const n = parts.length;
|
|
4695
|
-
const partBodies = parts.map((chunks, k) => {
|
|
4696
|
-
const json = partJson(chunks, k + 1, n);
|
|
4697
|
-
if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4698
|
-
return partComment(k + 1, n, json);
|
|
4699
|
-
});
|
|
4700
|
-
const body = `${human}
|
|
4701
|
-
|
|
4702
|
-
${payloadBlock(skeletonJson, sha, n)}
|
|
4703
|
-
`;
|
|
4704
|
-
if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4705
|
-
return { ...base, body, parts: partBodies };
|
|
4896
|
+
function branchWithJiraKey(branch, key) {
|
|
4897
|
+
if (!key || branch.includes(key)) return branch;
|
|
4898
|
+
const i = branch.lastIndexOf("/");
|
|
4899
|
+
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
4900
|
+
}
|
|
4901
|
+
const SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
|
|
4902
|
+
const SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
|
|
4903
|
+
const EVENTS = ["approve", "request-changes", "comment"];
|
|
4904
|
+
const ROLE_TEXT = { design: "for design", tech: "for tech", approval: "" };
|
|
4905
|
+
function composeSuggestionReviewComment(r) {
|
|
4906
|
+
const who = r.by ? ` by @${r.by.replace(/^@/, "")}` : "";
|
|
4907
|
+
const role = r.role ? ROLE_TEXT[r.role] : "";
|
|
4908
|
+
const head = r.event === "approve" ? `Approved${role ? ` ${role}` : ""}${who} in Bridge.` : r.event === "request-changes" ? `Changes requested${who} in Bridge.` : `Comment${who} in Bridge.`;
|
|
4909
|
+
const quote = r.message?.trim() ? r.message.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n") : "";
|
|
4910
|
+
const record = { event: r.event, ...r.role ? { role: r.role } : {}, payloadSha: r.payloadSha, at: r.at };
|
|
4911
|
+
return [head, ...quote ? ["", quote] : [], "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_REVIEW_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
4706
4912
|
}
|
|
4707
|
-
function
|
|
4708
|
-
|
|
4913
|
+
function parseSuggestionReviewComment(body) {
|
|
4914
|
+
const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4915
|
+
if (!m) return null;
|
|
4916
|
+
try {
|
|
4917
|
+
const r = JSON.parse(m[1]);
|
|
4918
|
+
if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
|
|
4919
|
+
return {
|
|
4920
|
+
event: r.event,
|
|
4921
|
+
...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
|
|
4922
|
+
payloadSha: r.payloadSha,
|
|
4923
|
+
at: typeof r.at === "string" ? r.at : ""
|
|
4924
|
+
};
|
|
4925
|
+
} catch {
|
|
4926
|
+
return null;
|
|
4927
|
+
}
|
|
4709
4928
|
}
|
|
4710
|
-
function
|
|
4711
|
-
const
|
|
4712
|
-
|
|
4929
|
+
function composeSuggestionAppliedComment(r) {
|
|
4930
|
+
const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
|
|
4931
|
+
const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
|
|
4932
|
+
return [
|
|
4933
|
+
`Being applied ${where2}${r.by ? ` by @${r.by.replace(/^@/, "")}` : ""}.${r.forced ? " Applied before every approval was in." : ""}`,
|
|
4934
|
+
"",
|
|
4935
|
+
`<!-- ${SUGGESTION_APPLIED_MARK} ${JSON.stringify(record)} -->`
|
|
4936
|
+
].join("\n");
|
|
4713
4937
|
}
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
|
|
4718
|
-
const marker = markers[markers.length - 1];
|
|
4719
|
-
if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
|
|
4720
|
-
if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
|
|
4721
|
-
const json = fencedAfter(text, marker.index + marker[0].length);
|
|
4722
|
-
if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
|
|
4723
|
-
let raw;
|
|
4938
|
+
function parseSuggestionAppliedComment(body) {
|
|
4939
|
+
const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4940
|
+
if (!m) return null;
|
|
4724
4941
|
try {
|
|
4725
|
-
|
|
4942
|
+
const r = JSON.parse(m[1]);
|
|
4943
|
+
return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
|
|
4726
4944
|
} catch {
|
|
4727
|
-
return
|
|
4728
|
-
}
|
|
4729
|
-
const schema = raw?.schema;
|
|
4730
|
-
if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
|
|
4731
|
-
const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
|
|
4732
|
-
return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
|
|
4733
|
-
}
|
|
4734
|
-
const problems = suggestionShapeProblems(raw);
|
|
4735
|
-
if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
|
|
4736
|
-
const payload = raw;
|
|
4737
|
-
const markedSha = marker[2];
|
|
4738
|
-
const expected = marker[3] ? Number(marker[3]) : 0;
|
|
4739
|
-
let edited = false;
|
|
4740
|
-
if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
|
|
4741
|
-
const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
|
|
4742
|
-
const found = /* @__PURE__ */ new Map();
|
|
4743
|
-
const author = opts.author?.toLowerCase();
|
|
4744
|
-
for (const c of opts.comments ?? []) {
|
|
4745
|
-
if (author && c.author && c.author.toLowerCase() !== author) continue;
|
|
4746
|
-
const m = new RegExp(`^<!-- ${MARK}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
|
|
4747
|
-
if (!m) continue;
|
|
4748
|
-
const i = Number(m[1]);
|
|
4749
|
-
if (found.has(i)) continue;
|
|
4750
|
-
const pj = fencedAfter(c.body, m.index + m[0].length);
|
|
4751
|
-
if (pj === void 0) continue;
|
|
4752
|
-
if (sha256Hex(pj) !== m[3]) edited = true;
|
|
4753
|
-
try {
|
|
4754
|
-
const part = JSON.parse(pj);
|
|
4755
|
-
if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
|
|
4756
|
-
found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
|
|
4757
|
-
} catch {
|
|
4758
|
-
continue;
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
for (let i = 1; i <= want; i++) {
|
|
4762
|
-
if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
|
|
4763
|
-
}
|
|
4764
|
-
const files = (payload.files ?? []).map((f, idx) => {
|
|
4765
|
-
if (f.part === void 0) return f;
|
|
4766
|
-
let content = "";
|
|
4767
|
-
for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
|
|
4768
|
-
const { part: _part, ...rest } = f;
|
|
4769
|
-
return { ...rest, content };
|
|
4770
|
-
});
|
|
4771
|
-
payload.files = files;
|
|
4945
|
+
return null;
|
|
4772
4946
|
}
|
|
4773
|
-
const sha = suggestionPayloadSha(payload);
|
|
4774
|
-
return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
|
|
4775
4947
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
const
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
if (!
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
4795
|
-
if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
|
|
4796
|
-
const spec = p.componentSpec;
|
|
4797
|
-
if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
|
|
4798
|
-
const nodes = p.links?.figma?.nodes;
|
|
4799
|
-
if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
|
|
4800
|
-
if (p.files !== void 0) {
|
|
4801
|
-
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
4802
|
-
else {
|
|
4803
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4804
|
-
for (const f of p.files) {
|
|
4805
|
-
const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
|
|
4806
|
-
if (!ok) {
|
|
4807
|
-
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
4808
|
-
break;
|
|
4809
|
-
}
|
|
4810
|
-
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
4811
|
-
seen.add(f.path);
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4948
|
+
const SUGGESTION_OVERRIDE_MARK = "bridge-suggestion-override";
|
|
4949
|
+
function composeSuggestionOverrideComment(r) {
|
|
4950
|
+
const audit = overrideComment(r.login, r.reason, r.missing).replace(/ Merged from Bridge\.$/, " Approved as owner from Bridge.");
|
|
4951
|
+
const record = { payloadSha: r.payloadSha, login: r.login.replace(/^@/, "").toLowerCase(), missing: r.missing };
|
|
4952
|
+
return [audit, "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_OVERRIDE_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
4953
|
+
}
|
|
4954
|
+
function parseSuggestionOverrideComment(body) {
|
|
4955
|
+
const text = (body ?? "").trim();
|
|
4956
|
+
if (!text.startsWith(OVERRIDE_HEADING)) return null;
|
|
4957
|
+
const m = new RegExp(`<!-- ${SUGGESTION_OVERRIDE_MARK} (\\{[^\\n]*?\\}) -->`).exec(text);
|
|
4958
|
+
if (!m) return null;
|
|
4959
|
+
try {
|
|
4960
|
+
const r = JSON.parse(m[1]);
|
|
4961
|
+
if (typeof r.payloadSha !== "string" || typeof r.login !== "string") return null;
|
|
4962
|
+
const why = /Owner override by @[\w-]+: ([\s\S]*?)\. Missing:/.exec(text);
|
|
4963
|
+
return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
|
|
4964
|
+
} catch {
|
|
4965
|
+
return null;
|
|
4814
4966
|
}
|
|
4815
|
-
return out;
|
|
4816
4967
|
}
|
|
4817
|
-
function
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
if (
|
|
4821
|
-
if (
|
|
4822
|
-
const
|
|
4823
|
-
if (
|
|
4824
|
-
if (segs.some((s) => s.toLowerCase() === ".git")) return `"${path}" is inside .git.`;
|
|
4968
|
+
function suggestionOverrideProblem(x) {
|
|
4969
|
+
const lists = approverLists(x.gov.owners, x.gov.rights ?? null, x.gov.teams);
|
|
4970
|
+
const holders = overrideHolders(x.gov.override, lists).map(norm);
|
|
4971
|
+
if (!x.viewer || !holders.includes(norm(x.viewer))) return "Only an owner the review rules allow to use the owner override can approve as owner.";
|
|
4972
|
+
if (norm(x.viewer) !== norm(x.author)) return "The owner override is for your own suggestion: approve this one the usual way.";
|
|
4973
|
+
const others = eligibleReviewers("approval", { owners: lists, author: x.author });
|
|
4974
|
+
if (others.open || others.logins.length) return "Someone else can review it: ask them instead.";
|
|
4825
4975
|
return null;
|
|
4826
4976
|
}
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
const tokenRoot2 = tokensPath(config) || ".";
|
|
4836
|
-
const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
|
|
4837
|
-
switch (kind) {
|
|
4838
|
-
case "token-change":
|
|
4839
|
-
return [tokenRoot2, ...changesets];
|
|
4840
|
-
case "docs":
|
|
4841
|
-
return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
|
|
4842
|
-
case "component-spec":
|
|
4843
|
-
return [".bridge/figma"];
|
|
4844
|
-
case "brand":
|
|
4845
|
-
return [tokenRoot2, ...changesets, CONFIG_FILE];
|
|
4846
|
-
case "config":
|
|
4847
|
-
return [CONFIG_FILE];
|
|
4848
|
-
case "revert":
|
|
4849
|
-
return void 0;
|
|
4977
|
+
function suggestionOverrideOf(comments, gov, opts) {
|
|
4978
|
+
if (!gov.override || !opts.author) return null;
|
|
4979
|
+
for (const c of [...comments].reverse()) {
|
|
4980
|
+
const r = parseSuggestionOverrideComment(c.body);
|
|
4981
|
+
if (!r || r.payloadSha !== opts.payloadSha || !c.author || norm(c.author) !== norm(r.login)) continue;
|
|
4982
|
+
if (suggestionOverrideProblem({ viewer: c.author, author: opts.author, gov })) continue;
|
|
4983
|
+
if (overrideReasonProblem(r.reason, gov.override)) continue;
|
|
4984
|
+
return r;
|
|
4850
4985
|
}
|
|
4986
|
+
return null;
|
|
4851
4987
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
const
|
|
4856
|
-
const
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
if (
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4988
|
+
const norm = (l) => l.trim().replace(/^@/, "").toLowerCase();
|
|
4989
|
+
const COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
4990
|
+
function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
4991
|
+
const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
|
|
4992
|
+
const owners = new Set([...lists.design, ...lists.tech].map(norm));
|
|
4993
|
+
const anyOwner = owners.size > 0;
|
|
4994
|
+
const reviews = [];
|
|
4995
|
+
const latestOld = /* @__PURE__ */ new Map();
|
|
4996
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
4997
|
+
const ordered = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
|
|
4998
|
+
ordered.forEach(({ c }, i) => {
|
|
4999
|
+
const r = parseSuggestionReviewComment(c.body);
|
|
5000
|
+
if (!r || r.event === "comment" || !c.author) return;
|
|
5001
|
+
const login = norm(c.author);
|
|
5002
|
+
if (opts.author && login === norm(opts.author)) return;
|
|
5003
|
+
const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
|
|
5004
|
+
if (!counts) {
|
|
5005
|
+
ignored.add(c.author);
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
5008
|
+
const at = String(i).padStart(6, "0");
|
|
5009
|
+
if (r.payloadSha !== opts.payloadSha) {
|
|
5010
|
+
latestOld.set(c.author, r.event);
|
|
5011
|
+
reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
latestOld.delete(c.author);
|
|
5015
|
+
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
5016
|
+
});
|
|
5017
|
+
const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
|
|
5018
|
+
const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
|
|
5019
|
+
const out = { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
|
|
5020
|
+
const override = summary.changesRequested.length ? null : suggestionOverrideOf(comments, gov, { payloadSha: opts.payloadSha, author: opts.author });
|
|
5021
|
+
return override ? { ...out, design: true, tech: true, owner: true, override } : out;
|
|
5022
|
+
}
|
|
5023
|
+
function suggestionReviewState(tier, approvals) {
|
|
5024
|
+
if (approvals.changesRequested.length) return "changes-requested";
|
|
5025
|
+
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
4873
5026
|
}
|
|
4874
5027
|
const SUGGESTION_APPLY_EXIT = {
|
|
4875
5028
|
ready: 0,
|
|
@@ -5751,323 +5904,337 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
|
|
|
5751
5904
|
return out;
|
|
5752
5905
|
}
|
|
5753
5906
|
export {
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5907
|
+
overrideComment as $,
|
|
5908
|
+
explorationNote as A,
|
|
5909
|
+
explorationNotePath as B,
|
|
5910
|
+
explorationPrTitle as C,
|
|
5758
5911
|
DRAFT_LINE_MAX as D,
|
|
5759
|
-
|
|
5760
|
-
|
|
5912
|
+
EXPLORATION_ARCHIVED_LABEL as E,
|
|
5913
|
+
explorationPrBody as F,
|
|
5761
5914
|
GitHubError as G,
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5915
|
+
suggestionStateFromLabels as H,
|
|
5916
|
+
suggestionStatus as I,
|
|
5917
|
+
composeRequestIssue as J,
|
|
5918
|
+
EXPLORATION_NOTES_DIR as K,
|
|
5919
|
+
isExplorationBranch as L,
|
|
5920
|
+
doneExploringPlan as M,
|
|
5921
|
+
withDoneExploring as N,
|
|
5922
|
+
explorationStartingPoint as O,
|
|
5770
5923
|
PROSE_KEYS as P,
|
|
5771
|
-
|
|
5924
|
+
explorationRefLabel as Q,
|
|
5772
5925
|
REQUESTS_BRANCH as R,
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5926
|
+
compareSpecToMeasured as S,
|
|
5927
|
+
variantKeyOf as T,
|
|
5928
|
+
matchVariant as U,
|
|
5929
|
+
withoutAddedWrappers as V,
|
|
5930
|
+
pairParts as W,
|
|
5931
|
+
hashSpec as X,
|
|
5932
|
+
SPEC_FIELDS as Y,
|
|
5933
|
+
COLOR_FIELDS as Z,
|
|
5934
|
+
DRAFTED_BY_CLAUDE_LABEL as _,
|
|
5782
5935
|
parseChangelog as a,
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5936
|
+
EXPLORATION_TIER_LABEL as a$,
|
|
5937
|
+
OVERRIDE_HEADING as a0,
|
|
5938
|
+
overrideRefusedComment as a1,
|
|
5939
|
+
diffLayered as a2,
|
|
5940
|
+
contrastDelta as a3,
|
|
5941
|
+
runLayeredContrast as a4,
|
|
5942
|
+
layeredImpact as a5,
|
|
5943
|
+
classifyLayered as a6,
|
|
5944
|
+
composeProposalBody as a7,
|
|
5945
|
+
createSuggestionOnGitHub as a8,
|
|
5946
|
+
gitBlobSha as a9,
|
|
5947
|
+
GitHubClient as aA,
|
|
5948
|
+
effectiveTier as aB,
|
|
5949
|
+
parseRequestIssue as aC,
|
|
5950
|
+
requestStatus as aD,
|
|
5951
|
+
listSuggestionsOnGitHub as aE,
|
|
5952
|
+
getSuggestionOnGitHub as aF,
|
|
5953
|
+
closingIssueNumbers as aG,
|
|
5954
|
+
requestIssueTitle as aH,
|
|
5955
|
+
pullState as aI,
|
|
5956
|
+
summarizeCi as aJ,
|
|
5957
|
+
summarizeApprovals as aK,
|
|
5958
|
+
BULK_LIMIT as aL,
|
|
5959
|
+
CI_STATE_LABEL as aM,
|
|
5960
|
+
COMPONENT_SPECS_VERSION as aN,
|
|
5961
|
+
CONFLICT_REASON as aO,
|
|
5962
|
+
DEFAULT_COMPONENT_SPECS_FILE as aP,
|
|
5963
|
+
DEFAULT_PROTECTED_BRANCHES as aQ,
|
|
5964
|
+
DOCS_CHANGE_BUMP as aR,
|
|
5965
|
+
DOCS_CLI_ORIGIN as aS,
|
|
5966
|
+
DOCS_EDITOR_ORIGIN as aT,
|
|
5967
|
+
DOCS_KEYS as aU,
|
|
5968
|
+
DOCS_MCP_ORIGIN as aV,
|
|
5969
|
+
EXPLORATION_CHECK as aW,
|
|
5970
|
+
EXPLORATION_READY_LABEL as aX,
|
|
5971
|
+
EXPLORATION_SLUG_MAX as aY,
|
|
5972
|
+
EXPLORATION_STARTING_POINT as aZ,
|
|
5973
|
+
EXPLORATION_STATUS_LABEL as a_,
|
|
5974
|
+
SUGGESTION_SCHEMA as aa,
|
|
5975
|
+
branchWithJiraKey as ab,
|
|
5976
|
+
titleWithJiraKey as ac,
|
|
5977
|
+
figmaSpecPath as ad,
|
|
5978
|
+
formatFigmaSpec as ae,
|
|
5979
|
+
figmaDraftBrief as af,
|
|
5980
|
+
parseFigmaOrigin as ag,
|
|
5981
|
+
figmaPushStatus as ah,
|
|
5982
|
+
parseFigmaSpec as ai,
|
|
5983
|
+
FIGMA_SPEC_DIR as aj,
|
|
5984
|
+
FROM_FIGMA_LABEL as ak,
|
|
5985
|
+
withFigmaOrigin as al,
|
|
5986
|
+
WAITING_FOR_CLAUDE_LABEL as am,
|
|
5987
|
+
planComponentPush as an,
|
|
5988
|
+
figmaOriginSection as ao,
|
|
5989
|
+
figmaBranchTitle as ap,
|
|
5990
|
+
figmaSpecSlug as aq,
|
|
5991
|
+
figmaDraftBody as ar,
|
|
5992
|
+
REQUEST_LABEL as as,
|
|
5993
|
+
figmaNodeUrl as at,
|
|
5994
|
+
parentDir as au,
|
|
5995
|
+
parseComponentSpecs as av,
|
|
5996
|
+
readRepoTexts as aw,
|
|
5997
|
+
normalizeRepoPath as ax,
|
|
5998
|
+
isUnder as ay,
|
|
5999
|
+
componentMotion as az,
|
|
5847
6000
|
buildReport as b,
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
6001
|
+
cssModuleClasses as b$,
|
|
6002
|
+
EXPLORATION_USE_CASES as b0,
|
|
6003
|
+
FORMER_MEMBER as b1,
|
|
6004
|
+
LOOK_AGAIN_STATUS_LABEL as b2,
|
|
6005
|
+
MeasuredSpecsError as b3,
|
|
6006
|
+
PULL_STATE_PILL as b4,
|
|
6007
|
+
ProtectedBranchError as b5,
|
|
6008
|
+
REDUCED_MOTION_NOTE as b6,
|
|
6009
|
+
REQUEST_STATUS_LABEL as b7,
|
|
6010
|
+
REVIEW_STATUS_LABEL as b8,
|
|
6011
|
+
REVIEW_STATUS_ORDER as b9,
|
|
6012
|
+
assignRequestOnGitHub as bA,
|
|
6013
|
+
assignVersions as bB,
|
|
6014
|
+
base64ToUtf8 as bC,
|
|
6015
|
+
branchVerdict as bD,
|
|
6016
|
+
bridgeMcpInstallCommand as bE,
|
|
6017
|
+
canOverride as bF,
|
|
6018
|
+
canonicalJson as bG,
|
|
6019
|
+
changeTable as bH,
|
|
6020
|
+
changesetBump as bI,
|
|
6021
|
+
classifyAuth as bJ,
|
|
6022
|
+
claudeStage as bK,
|
|
6023
|
+
cleanDocs as bL,
|
|
6024
|
+
clearSharedEtags as bM,
|
|
6025
|
+
commitFileOnGitHub as bN,
|
|
6026
|
+
componentContrastPairs as bO,
|
|
6027
|
+
componentsUsingMotionToken as bP,
|
|
6028
|
+
composeChangeset as bQ,
|
|
6029
|
+
composeSuggestionAppliedComment as bR,
|
|
6030
|
+
composeSuggestionIssue as bS,
|
|
6031
|
+
composeSuggestionOverrideComment as bT,
|
|
6032
|
+
composeSuggestionReviewComment as bU,
|
|
6033
|
+
continueDocsProposalOnGitHub as bV,
|
|
6034
|
+
contrastDeltaTable as bW,
|
|
6035
|
+
countChanges as bX,
|
|
6036
|
+
coverageSummary as bY,
|
|
6037
|
+
cssColorToRgb as bZ,
|
|
6038
|
+
cssColorsToRgb as b_,
|
|
6039
|
+
SUGGESTION_APPLIED_MARK as ba,
|
|
6040
|
+
SUGGESTION_APPLY_EXIT as bb,
|
|
6041
|
+
SUGGESTION_FINAL_LABELS as bc,
|
|
6042
|
+
SUGGESTION_KINDS as bd,
|
|
6043
|
+
SUGGESTION_LABEL as be,
|
|
6044
|
+
SUGGESTION_LIMITS as bf,
|
|
6045
|
+
SUGGESTION_OVERRIDE_MARK as bg,
|
|
6046
|
+
SUGGESTION_REVIEW_MARK as bh,
|
|
6047
|
+
SUGGESTION_STATE_LABELS as bi,
|
|
6048
|
+
SUGGESTION_STATUS_LABEL as bj,
|
|
6049
|
+
SUGGESTION_STATUS_LABELS as bk,
|
|
6050
|
+
SUGGESTION_TOO_LARGE as bl,
|
|
6051
|
+
StorybookIndexError as bm,
|
|
6052
|
+
SuggestionError as bn,
|
|
6053
|
+
TOKEN_REJECTED_MESSAGE as bo,
|
|
6054
|
+
TOKEN_SETTINGS_URL as bp,
|
|
6055
|
+
TRIGGER_LABELS as bq,
|
|
6056
|
+
UNRELEASED as br,
|
|
6057
|
+
acceptDocsDraft as bs,
|
|
6058
|
+
acceptDocsText as bt,
|
|
6059
|
+
allowedMergeMethods as bu,
|
|
6060
|
+
anatomyLabel as bv,
|
|
6061
|
+
appliedStatus as bw,
|
|
6062
|
+
approvedSuggestionNumber as bx,
|
|
6063
|
+
approvedSuggestionOnGitHub as by,
|
|
6064
|
+
assertSafeBranch as bz,
|
|
5912
6065
|
collectHistory as c,
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
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
|
-
|
|
6066
|
+
mergeGate as c$,
|
|
6067
|
+
declineComment as c0,
|
|
6068
|
+
declineReasonProblems as c1,
|
|
6069
|
+
declineRequestOnGitHub as c2,
|
|
6070
|
+
declineSuggestionOnGitHub as c3,
|
|
6071
|
+
defaultDocsFooter as c4,
|
|
6072
|
+
docsAuthorLine as c5,
|
|
6073
|
+
docsFileText as c6,
|
|
6074
|
+
docsProposal as c7,
|
|
6075
|
+
docsSectionShapes as c8,
|
|
6076
|
+
docsSlug as c9,
|
|
6077
|
+
iconComponentName as cA,
|
|
6078
|
+
installationFor as cB,
|
|
6079
|
+
isActiveExploration as cC,
|
|
6080
|
+
isAutomationPull as cD,
|
|
6081
|
+
isDocsProposalBranch as cE,
|
|
6082
|
+
isDraft as cF,
|
|
6083
|
+
isGuardrailPath as cG,
|
|
6084
|
+
isHandedOff as cH,
|
|
6085
|
+
isHeldExploration as cI,
|
|
6086
|
+
isJiraKey as cJ,
|
|
6087
|
+
isMissingPermission as cK,
|
|
6088
|
+
isPlatformRefusal as cL,
|
|
6089
|
+
isReleasePull as cM,
|
|
6090
|
+
isTokenRejected as cN,
|
|
6091
|
+
jiraIssueFields as cO,
|
|
6092
|
+
jiraKeyOf as cP,
|
|
6093
|
+
joinRepoPath as cQ,
|
|
6094
|
+
linkRequestToExplorationOnGitHub as cR,
|
|
6095
|
+
linkSuggestionPrOnGitHub as cS,
|
|
6096
|
+
linkedPullsOf as cT,
|
|
6097
|
+
loadRepoFiles as cU,
|
|
6098
|
+
lookAgainReviewers as cV,
|
|
6099
|
+
lowerFirstWord as cW,
|
|
6100
|
+
markSuggestionsAppliedOnGitHub as cX,
|
|
6101
|
+
markUsedTokens as cY,
|
|
6102
|
+
memoryEtagStore as cZ,
|
|
6103
|
+
memorySource as c_,
|
|
6104
|
+
docsSuggestionPayload as ca,
|
|
6105
|
+
docsText as cb,
|
|
6106
|
+
draftChangeLine as cc,
|
|
6107
|
+
draftPushDescription as cd,
|
|
6108
|
+
draftRequestText as ce,
|
|
6109
|
+
eligibleReviewers as cf,
|
|
6110
|
+
evalCalc as cg,
|
|
6111
|
+
explorationBranch as ch,
|
|
6112
|
+
explorationRules as ci,
|
|
6113
|
+
explorationShareMessage as cj,
|
|
6114
|
+
explorationSlugFromBranch as ck,
|
|
6115
|
+
explorationTakeCommands as cl,
|
|
6116
|
+
fetchStorybookIndex as cm,
|
|
6117
|
+
figmaCell as cn,
|
|
6118
|
+
fileModes as co,
|
|
6119
|
+
filterHistory as cp,
|
|
6120
|
+
findOpenDocsProposal as cq,
|
|
6121
|
+
findOpenDocsProposalOnGitHub as cr,
|
|
6122
|
+
firstParagraph as cs,
|
|
6123
|
+
formatDocsJson as ct,
|
|
6124
|
+
friendlyMessage as cu,
|
|
6125
|
+
githubSuggestionWorkspace as cv,
|
|
6126
|
+
groupStorybookComponents as cw,
|
|
6127
|
+
hasOwnerApproval as cx,
|
|
6128
|
+
hasSuggestionBlock as cy,
|
|
6129
|
+
hasWriteAccess as cz,
|
|
5977
6130
|
applyTokenEdits as d,
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
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
|
-
|
|
6131
|
+
storybookManagerUrl as d$,
|
|
6132
|
+
missingPermissionMessage as d0,
|
|
6133
|
+
missingProse as d1,
|
|
6134
|
+
modeLabel as d2,
|
|
6135
|
+
needsDocsReview as d3,
|
|
6136
|
+
normalizeStorybookUrl as d4,
|
|
6137
|
+
openPulls as d5,
|
|
6138
|
+
overrideHolders as d6,
|
|
6139
|
+
overrideReasonMin as d7,
|
|
6140
|
+
overrideReasonProblem as d8,
|
|
6141
|
+
overrideSuggestionOnGitHub as d9,
|
|
6142
|
+
propsTable as dA,
|
|
6143
|
+
pushChangeRows as dB,
|
|
6144
|
+
rateLimitMessage as dC,
|
|
6145
|
+
releasePull as dD,
|
|
6146
|
+
reopenEligibility as dE,
|
|
6147
|
+
requestBrief as dF,
|
|
6148
|
+
requestImagePath as dG,
|
|
6149
|
+
requesterFirstName as dH,
|
|
6150
|
+
requesterLine as dI,
|
|
6151
|
+
requesterOf as dJ,
|
|
6152
|
+
requiredApprovals as dK,
|
|
6153
|
+
requiredPermission as dL,
|
|
6154
|
+
reviewQueue as dM,
|
|
6155
|
+
reviewStatusKind as dN,
|
|
6156
|
+
reviewStatusLabel as dO,
|
|
6157
|
+
reviewSuggestionOnGitHub as dP,
|
|
6158
|
+
reviewersToAsk as dQ,
|
|
6159
|
+
sectionsNeedingReview as dR,
|
|
6160
|
+
sha256Hex as dS,
|
|
6161
|
+
sharedTokenPrefix as dT,
|
|
6162
|
+
shortVariant as dU,
|
|
6163
|
+
shortVariants as dV,
|
|
6164
|
+
specHash as dW,
|
|
6165
|
+
specsUsage as dX,
|
|
6166
|
+
stalenessStatus as dY,
|
|
6167
|
+
storybookIframeUrl as dZ,
|
|
6168
|
+
storybookIndexUrls as d_,
|
|
6169
|
+
ownerOverridePolicy as da,
|
|
6170
|
+
parseBoxShadow as db,
|
|
6171
|
+
parseChangesetText as dc,
|
|
6172
|
+
parseExplorationRef as dd,
|
|
6173
|
+
parseJiraKey as de,
|
|
6174
|
+
parseMeasuredSpecsFile as df,
|
|
6175
|
+
parseOverrideComment as dg,
|
|
6176
|
+
parseRepo as dh,
|
|
6177
|
+
parseStorybookIndex as di,
|
|
6178
|
+
parseStructureChange as dj,
|
|
6179
|
+
parseSuggestionAppliedComment as dk,
|
|
6180
|
+
parseSuggestionIssue as dl,
|
|
6181
|
+
parseSuggestionOverrideComment as dm,
|
|
6182
|
+
parseSuggestionReviewComment as dn,
|
|
6183
|
+
parseTier as dp,
|
|
6184
|
+
patchDocsJson as dq,
|
|
6185
|
+
pendingApprovals as dr,
|
|
6186
|
+
permissionLabel as ds,
|
|
6187
|
+
planRestore as dt,
|
|
6188
|
+
planRevert as du,
|
|
6189
|
+
planSuggestionApply as dv,
|
|
6190
|
+
planToJson as dw,
|
|
6191
|
+
prNumberFromSubject as dx,
|
|
6192
|
+
prefillDocs as dy,
|
|
6193
|
+
proposeDocsOnGitHub as dz,
|
|
6041
6194
|
buildHistoryEntry as e,
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6195
|
+
subjectTitle as e0,
|
|
6196
|
+
suggestionAllowedRoots as e1,
|
|
6197
|
+
suggestionAppliedComment as e2,
|
|
6198
|
+
suggestionApplyTable as e3,
|
|
6199
|
+
suggestionIsTokenValues as e4,
|
|
6200
|
+
suggestionIssueTitle as e5,
|
|
6201
|
+
suggestionKindFromLabels as e6,
|
|
6202
|
+
suggestionLabelChanges as e7,
|
|
6203
|
+
suggestionLabels as e8,
|
|
6204
|
+
suggestionOverrideOf as e9,
|
|
6205
|
+
withFigmaPushApprovals as eA,
|
|
6206
|
+
withOutdated as eB,
|
|
6207
|
+
withRequestExploration as eC,
|
|
6208
|
+
withSrgbColors as eD,
|
|
6209
|
+
withSuggestionApprovals as eE,
|
|
6210
|
+
withTokenValueApprovals as eF,
|
|
6211
|
+
withoutJiraKey as eG,
|
|
6212
|
+
suggestionOverrideProblem as ea,
|
|
6213
|
+
suggestionPathProblems as eb,
|
|
6214
|
+
suggestionPayloadSha as ec,
|
|
6215
|
+
suggestionReviewState as ed,
|
|
6216
|
+
suggestionShapeProblems as ee,
|
|
6217
|
+
suggestionStalenessOnGitHub as ef,
|
|
6218
|
+
suggestionStateLabel as eg,
|
|
6219
|
+
suggestionSubject as eh,
|
|
6220
|
+
suggestionTouches as ei,
|
|
6221
|
+
summarizeSuggestionApprovals as ej,
|
|
6222
|
+
syncSuggestionStateLabel as ek,
|
|
6223
|
+
titleOf as el,
|
|
6224
|
+
toChangeSummary as em,
|
|
6225
|
+
tokenCssVar as en,
|
|
6226
|
+
tokenFingerprint as eo,
|
|
6227
|
+
tokenHistory as ep,
|
|
6228
|
+
tokenVariablePath as eq,
|
|
6229
|
+
unsafePathProblem as er,
|
|
6230
|
+
utf8ToBase64 as es,
|
|
6231
|
+
validateComponentSpec as et,
|
|
6232
|
+
validateDocs as eu,
|
|
6233
|
+
validateIntegrations as ev,
|
|
6234
|
+
variantName as ew,
|
|
6235
|
+
versionTimeline as ex,
|
|
6236
|
+
webStorageEtagStore as ey,
|
|
6237
|
+
withAuthorLine as ez,
|
|
6071
6238
|
coerceValue as f,
|
|
6072
6239
|
checkLayeredPair as g,
|
|
6073
6240
|
formatChangeset as h,
|
|
@@ -6077,17 +6244,17 @@ export {
|
|
|
6077
6244
|
REQUEST_KINDS as l,
|
|
6078
6245
|
mergeReleases as m,
|
|
6079
6246
|
parseExplorationBody as n,
|
|
6080
|
-
|
|
6247
|
+
EXPLORATION_PREFIX as o,
|
|
6081
6248
|
packageDirCandidates as p,
|
|
6082
|
-
|
|
6249
|
+
explorationSlug as q,
|
|
6083
6250
|
releasesFromTags as r,
|
|
6084
6251
|
sanitizeDraftLine as s,
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6252
|
+
isArchivedExploration as t,
|
|
6253
|
+
explorationStatus as u,
|
|
6254
|
+
explorationTryIt as v,
|
|
6255
|
+
isExplorationPull as w,
|
|
6256
|
+
promotionPlan as x,
|
|
6257
|
+
EXPLORATION_LABEL as y,
|
|
6258
|
+
explorationProblems as z
|
|
6092
6259
|
};
|
|
6093
|
-
//# sourceMappingURL=repo-source-
|
|
6260
|
+
//# sourceMappingURL=repo-source-CEBBBtm1.js.map
|