@bridge-ds/core 0.19.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/LICENSE +17 -0
- package/bridge.config.schema.json +5 -0
- 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/config.d.ts +5 -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/fidelity.d.ts +12 -1
- package/dist/figma-fix-loop.d.ts +1 -1
- package/dist/figma-fix-run.d.ts +103 -0
- package/dist/figma-fixes.d.ts +14 -2
- package/dist/gap-case.d.ts +1 -2
- package/dist/github-client.d.ts +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2139 -1290
- package/dist/index.js.map +1 -1
- package/dist/measure-location.d.ts +42 -0
- package/dist/measure-workflow.d.ts +48 -0
- package/dist/measured-only.d.ts +15 -1
- package/dist/node/index.js +15 -12
- package/dist/node/index.js.map +1 -1
- package/dist/previews-github.d.ts +13 -0
- package/dist/previews.d.ts +8 -1
- package/dist/{prod-branch-Bdx_2bRQ.js → prod-branch-C7tphFAA.js} +257 -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-model.d.ts +2 -0
- package/dist/{repo-source-m0Tc603o.js → repo-source-CEBBBtm1.js} +2183 -1937
- package/dist/repo-source-CEBBBtm1.js.map +1 -0
- package/dist/{report-DFPKygfk.js → report-B7wNAnSu.js} +38 -38
- package/dist/{report-DFPKygfk.js.map → report-B7wNAnSu.js.map} +1 -1
- package/dist/review-gate.d.ts +7 -1
- package/dist/suggestion-review.d.ts +43 -2
- package/dist/suggestions-github.d.ts +12 -1
- package/dist/tier1-publish.d.ts +48 -0
- package/dist/try-in-product.d.ts +4 -1
- package/package.json +1 -1
- package/dist/prod-branch-Bdx_2bRQ.js.map +0 -1
- package/dist/repo-source-m0Tc603o.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
|
}
|
|
@@ -1026,6 +1026,7 @@ class GitHubClient {
|
|
|
1026
1026
|
const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
1027
1027
|
const r = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`);
|
|
1028
1028
|
if (r.type !== "file") return void 0;
|
|
1029
|
+
if (r.encoding === "none" || !r.content && (r.size ?? 0) > 0) return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`, void 0, true, { accept: ACCEPT_RAW, text: true });
|
|
1029
1030
|
if (r.encoding === "base64" && r.content !== void 0) return base64ToUtf8(r.content);
|
|
1030
1031
|
return r.content;
|
|
1031
1032
|
} catch (err) {
|
|
@@ -1251,6 +1252,10 @@ class GitHubClient {
|
|
|
1251
1252
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
|
|
1252
1253
|
return r.map((f) => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions }));
|
|
1253
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
|
+
}
|
|
1254
1259
|
async updatePullRequest(n, patch) {
|
|
1255
1260
|
await this.request("PATCH", `${this.repoPath}/pulls/${n}`, patch);
|
|
1256
1261
|
}
|
|
@@ -2485,6 +2490,8 @@ const EXPLORATION_PREFIX = "explore/";
|
|
|
2485
2490
|
const EXPLORATION_NOTES_DIR = ".bridge/explorations";
|
|
2486
2491
|
const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
|
|
2487
2492
|
const EXPLORATION_CHECK = "Exploration not merged as is";
|
|
2493
|
+
const EXPLORATION_READY_LABEL = "ready-for-devs";
|
|
2494
|
+
const EXPLORATION_ARCHIVED_LABEL = "archived";
|
|
2488
2495
|
const EXPLORATION_USE_CASES = [
|
|
2489
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." },
|
|
2490
2497
|
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
|
|
@@ -2492,13 +2499,21 @@ const EXPLORATION_USE_CASES = [
|
|
|
2492
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." },
|
|
2493
2500
|
{ value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
|
|
2494
2501
|
];
|
|
2495
|
-
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
|
+
};
|
|
2496
2510
|
const MARK$2 = "bridge-exploration";
|
|
2497
2511
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2512
|
+
const EXPLORATION_SLUG_MAX = 60;
|
|
2498
2513
|
function explorationSlug(name) {
|
|
2499
2514
|
const s = name.trim().replace(/^explore\//i, "");
|
|
2500
2515
|
if (!/[a-z0-9]/i.test(s)) return "";
|
|
2501
|
-
return slugify(s,
|
|
2516
|
+
return slugify(s, EXPLORATION_SLUG_MAX);
|
|
2502
2517
|
}
|
|
2503
2518
|
function explorationBranch(nameOrBranch) {
|
|
2504
2519
|
return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
|
|
@@ -2510,6 +2525,25 @@ function isExplorationBranch(ref) {
|
|
|
2510
2525
|
function explorationSlugFromBranch(ref) {
|
|
2511
2526
|
return isExplorationBranch(ref) ? ref.replace(/^refs\/heads\//, "").slice(EXPLORATION_PREFIX.length) : null;
|
|
2512
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}`;
|
|
2513
2547
|
const explorationNotePath = (slug) => `${EXPLORATION_NOTES_DIR}/${slug}.md`;
|
|
2514
2548
|
function explorationProblems(e) {
|
|
2515
2549
|
const out = [];
|
|
@@ -2518,7 +2552,6 @@ function explorationProblems(e) {
|
|
|
2518
2552
|
if (!name) out.push('Name the exploration, for example "Denser data tables".');
|
|
2519
2553
|
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
|
|
2520
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.");
|
|
2521
|
-
if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
|
|
2522
2555
|
return out;
|
|
2523
2556
|
}
|
|
2524
2557
|
const useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
|
|
@@ -2528,19 +2561,18 @@ function explorationNote(e) {
|
|
|
2528
2561
|
return [
|
|
2529
2562
|
`# Exploration: ${e.name.trim()}`,
|
|
2530
2563
|
"",
|
|
2531
|
-
e.purpose.trim(),
|
|
2532
|
-
"",
|
|
2564
|
+
...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
|
|
2533
2565
|
`- Branch: \`${EXPLORATION_PREFIX}${slug}\``,
|
|
2534
2566
|
...uc ? [`- Use case: ${uc}`] : [],
|
|
2535
2567
|
...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
|
|
2536
2568
|
"",
|
|
2537
|
-
|
|
2569
|
+
'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
|
|
2538
2570
|
""
|
|
2539
2571
|
].join("\n");
|
|
2540
2572
|
}
|
|
2541
2573
|
function explorationPrTitle(e) {
|
|
2542
|
-
const n = e.name.trim().replace(/\.$/, "");
|
|
2543
|
-
return `Exploration: ${n
|
|
2574
|
+
const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
|
|
2575
|
+
return `Exploration: ${n}`;
|
|
2544
2576
|
}
|
|
2545
2577
|
function explorationPrBody(e, ctx) {
|
|
2546
2578
|
const slug = e.slug || explorationSlug(e.name);
|
|
@@ -2550,44 +2582,78 @@ function explorationPrBody(e, ctx) {
|
|
|
2550
2582
|
const lines = [
|
|
2551
2583
|
`## Exploration: ${e.name.trim()}`,
|
|
2552
2584
|
"",
|
|
2553
|
-
`> Not mergeable as is. This draft pull request is a
|
|
2554
|
-
"",
|
|
2555
|
-
`**Purpose.** ${e.purpose.trim()}`,
|
|
2556
|
-
...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.`,
|
|
2557
2586
|
"",
|
|
2587
|
+
...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
|
|
2588
|
+
...uc ? [`**Use case.** ${uc}`, ""] : [],
|
|
2558
2589
|
"### What is experimental",
|
|
2559
2590
|
"",
|
|
2560
|
-
...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."],
|
|
2561
2592
|
...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
|
|
2562
|
-
"",
|
|
2563
|
-
"### Try it in a product staging branch",
|
|
2564
2593
|
""
|
|
2565
2594
|
];
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
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
|
+
}
|
|
2573
2621
|
lines.push(
|
|
2574
2622
|
"",
|
|
2575
2623
|
"### When it is done",
|
|
2576
2624
|
"",
|
|
2577
|
-
`- **
|
|
2578
|
-
|
|
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."}`,
|
|
2579
2627
|
"",
|
|
2580
|
-
|
|
2628
|
+
explorationBlock({ ...e, slug })
|
|
2581
2629
|
);
|
|
2582
2630
|
return lines.join("\n");
|
|
2583
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
|
+
}
|
|
2584
2649
|
function parseExplorationBody(body) {
|
|
2585
2650
|
if (!body) return void 0;
|
|
2586
2651
|
const m = new RegExp(`<!-- ${MARK$2} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
2587
2652
|
if (!m) return void 0;
|
|
2588
2653
|
try {
|
|
2589
2654
|
const e = JSON.parse(m[1]);
|
|
2590
|
-
|
|
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;
|
|
2591
2657
|
} catch {
|
|
2592
2658
|
return void 0;
|
|
2593
2659
|
}
|
|
@@ -2596,13 +2662,84 @@ const labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.n
|
|
|
2596
2662
|
function isExplorationPull(p) {
|
|
2597
2663
|
return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
|
|
2598
2664
|
}
|
|
2665
|
+
function isHandedOff(p) {
|
|
2666
|
+
return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
|
|
2667
|
+
}
|
|
2599
2668
|
function explorationStatus(p) {
|
|
2600
2669
|
const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
|
|
2601
2670
|
if (p.state !== "open" && !merged) return "closed";
|
|
2602
2671
|
if (p.state === "open" && labelNames(p.labels).includes(EXPLORATION_LABEL)) return "active";
|
|
2603
|
-
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";
|
|
2604
2678
|
}
|
|
2605
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
|
+
}
|
|
2606
2743
|
function promotionPlan(input) {
|
|
2607
2744
|
const exploration = parseExplorationBody(input.body);
|
|
2608
2745
|
const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
|
|
@@ -2654,8 +2791,8 @@ function explorationShareMessage(input) {
|
|
|
2654
2791
|
function explorationRules(branch) {
|
|
2655
2792
|
return [
|
|
2656
2793
|
`- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
|
|
2657
|
-
`- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It
|
|
2658
|
-
|
|
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.`
|
|
2659
2796
|
];
|
|
2660
2797
|
}
|
|
2661
2798
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2701,7 +2838,8 @@ function requestIssueTitle(r) {
|
|
|
2701
2838
|
return subjectTitle(r.component, r.title);
|
|
2702
2839
|
}
|
|
2703
2840
|
function composeRequestIssue(r, opts = {}) {
|
|
2704
|
-
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 ?? "")} |`)];
|
|
2705
2843
|
const figma = r.figma?.url ? `[${r.figma.nodeName ?? "Open in Figma"}](${r.figma.url})` : r.figma?.nodeName ? r.figma.nodeName : "";
|
|
2706
2844
|
const body = [
|
|
2707
2845
|
r.kind === "new-component" ? `## New component request: ${r.component}` : `## ${kindLabel$1(r.kind)} request for ${r.component}`,
|
|
@@ -2713,10 +2851,10 @@ function composeRequestIssue(r, opts = {}) {
|
|
|
2713
2851
|
...r.images?.length ? [...r.images.map((u, i) => ``), ""] : [],
|
|
2714
2852
|
...figma ? [`**Design:** ${figma}`, ""] : [],
|
|
2715
2853
|
...opts.componentUrl ? [`**Component:** ${opts.componentUrl}`, ""] : [],
|
|
2716
|
-
...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.`, ""] : [],
|
|
2717
2855
|
"## How to implement",
|
|
2718
2856
|
"",
|
|
2719
|
-
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.`,
|
|
2720
2858
|
"",
|
|
2721
2859
|
`<sub>Requested from ${sourceLabel$1(r.source)}${r.requestedBy ? ` by @${r.requestedBy}` : ""} with Bridge.</sub>`,
|
|
2722
2860
|
"",
|
|
@@ -2741,7 +2879,7 @@ function withRequestExploration(body, branch) {
|
|
|
2741
2879
|
if (!r || !body) return void 0;
|
|
2742
2880
|
const block = new RegExp(`<!-- ${MARK$1} \\{[\\s\\S]*?\\} -->`);
|
|
2743
2881
|
let out = body.replace(block, () => `<!-- ${MARK$1} ${JSON.stringify({ ...r, exploration: branch })} -->`);
|
|
2744
|
-
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.`;
|
|
2745
2883
|
if (/^\*\*Exploration:\*\* .*$/m.test(out)) out = out.replace(/^\*\*Exploration:\*\* .*$/m, () => line);
|
|
2746
2884
|
else if (out.includes("\n## How to implement")) out = out.replace("\n## How to implement", () => `
|
|
2747
2885
|
${line}
|
|
@@ -2798,7 +2936,7 @@ function requestBrief(r, ctx) {
|
|
|
2798
2936
|
"Rules:",
|
|
2799
2937
|
`- Follow ${ctx.agentsFile ?? "AGENTS.md"}: tokens only in component CSS, Base UI for behavior, states, tests, story, docs.json, changeset.`,
|
|
2800
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.",
|
|
2801
|
-
...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}".`],
|
|
2802
2940
|
"- If the request is unclear or conflicts with the guidance, comment on the issue instead of guessing."
|
|
2803
2941
|
].join("\n");
|
|
2804
2942
|
}
|
|
@@ -2831,37 +2969,6 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
2831
2969
|
}
|
|
2832
2970
|
};
|
|
2833
2971
|
}
|
|
2834
|
-
async function linkRequestToExplorationOnGitHub(gh, n, branch, opts = {}) {
|
|
2835
|
-
if (!isExplorationBranch(branch)) throw new GitHubError(422, `"${branch}" is not an exploration branch. Exploration branches are named explore/<name>.`);
|
|
2836
|
-
const issue = await gh.getIssue(n);
|
|
2837
|
-
const body = withRequestExploration(issue.body, branch);
|
|
2838
|
-
if (!body) throw new GitHubError(422, `#${n} has no Bridge request block, so it cannot be linked. Add "exploration: ${branch}" to it on GitHub.`);
|
|
2839
|
-
await gh.updateIssue(n, { body });
|
|
2840
|
-
const warnings = [];
|
|
2841
|
-
if (!await gh.addLabels(n, [EXPLORATION_LABEL])) warnings.push(`The ${EXPLORATION_LABEL} label could not be added to #${n}.`);
|
|
2842
|
-
let assignees = [];
|
|
2843
|
-
if (opts.assignee) {
|
|
2844
|
-
try {
|
|
2845
|
-
assignees = await gh.addAssignees(n, [opts.assignee.replace(/^@/, "")]);
|
|
2846
|
-
} catch {
|
|
2847
|
-
warnings.push(`@${opts.assignee.replace(/^@/, "")} could not be assigned to #${n}.`);
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
return { number: n, branch, assignees, warnings };
|
|
2851
|
-
}
|
|
2852
|
-
async function assignRequestOnGitHub(gh, n, login) {
|
|
2853
|
-
const who = login.trim().replace(/^@/, "");
|
|
2854
|
-
if (!who) throw new GitHubError(422, "Pick who should work on it.");
|
|
2855
|
-
const assignees = await gh.addAssignees(n, [who]);
|
|
2856
|
-
if (!assignees.some((a) => a.toLowerCase() === who.toLowerCase())) throw new GitHubError(422, `GitHub did not assign @${who}: they need access to this repository.`);
|
|
2857
|
-
return assignees;
|
|
2858
|
-
}
|
|
2859
|
-
async function declineRequestOnGitHub(gh, n, reason, by) {
|
|
2860
|
-
const problems = declineReasonProblems(reason);
|
|
2861
|
-
if (problems.length) throw new GitHubError(422, problems.join(" "));
|
|
2862
|
-
await gh.addComment(n, declineComment(reason, by));
|
|
2863
|
-
await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
|
|
2864
|
-
}
|
|
2865
2972
|
const COMPONENT_SPECS_VERSION = 1;
|
|
2866
2973
|
const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
|
|
2867
2974
|
const SPEC_FIELDS = [
|
|
@@ -3019,6 +3126,23 @@ function hashSpec(spec) {
|
|
|
3019
3126
|
}
|
|
3020
3127
|
const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
3021
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
|
+
}
|
|
3022
3146
|
const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
|
|
3023
3147
|
function componentMotion(spec) {
|
|
3024
3148
|
const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
|
|
@@ -3081,553 +3205,155 @@ function figmaCell(entry) {
|
|
|
3081
3205
|
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
3082
3206
|
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
3083
3207
|
}
|
|
3084
|
-
const
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
if (!body) return void 0;
|
|
3092
|
-
let missing = false;
|
|
3093
|
-
const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
|
|
3094
|
-
const v2 = resolve(path);
|
|
3095
|
-
const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
|
|
3096
|
-
if (n === void 0 || Number.isNaN(n)) missing = true;
|
|
3097
|
-
return String(n);
|
|
3098
|
-
});
|
|
3099
|
-
if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
|
|
3100
|
-
const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
|
|
3101
|
-
let i = 0;
|
|
3102
|
-
const factor = () => {
|
|
3103
|
-
const t = tokens[i++];
|
|
3104
|
-
if (t === "(") {
|
|
3105
|
-
const v2 = sum();
|
|
3106
|
-
i++;
|
|
3107
|
-
return v2;
|
|
3108
|
-
}
|
|
3109
|
-
if (t === "-") return -factor();
|
|
3110
|
-
return Number(t);
|
|
3111
|
-
};
|
|
3112
|
-
const product = () => {
|
|
3113
|
-
let v2 = factor();
|
|
3114
|
-
while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
|
|
3115
|
-
return v2;
|
|
3116
|
-
};
|
|
3117
|
-
const sum = () => {
|
|
3118
|
-
let v2 = product();
|
|
3119
|
-
while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
|
|
3120
|
-
return v2;
|
|
3121
|
-
};
|
|
3122
|
-
const v = sum();
|
|
3123
|
-
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 } };
|
|
3124
3215
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
const
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
const
|
|
3132
|
-
|
|
3133
|
-
|
|
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}}` });
|
|
3134
3264
|
}
|
|
3135
|
-
const
|
|
3136
|
-
if (
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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
|
+
};
|
|
3141
3275
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
const
|
|
3145
|
-
const
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
const added = /* @__PURE__ */ new Map();
|
|
3149
|
-
for (const p of parts) {
|
|
3150
|
-
if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
|
|
3151
|
-
const kids = parts.filter((c) => c.parent === p.id);
|
|
3152
|
-
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 };
|
|
3153
3282
|
}
|
|
3154
|
-
|
|
3155
|
-
const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
|
|
3156
|
-
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: [] };
|
|
3157
3284
|
}
|
|
3158
|
-
function
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
};
|
|
3172
|
-
const pairedAncestor = (p) => {
|
|
3173
|
-
for (let a = p.parent; a; a = sParts.get(a)?.parent) if (pairs2.has(a)) return pairs2.get(a);
|
|
3174
|
-
return void 0;
|
|
3175
|
-
};
|
|
3176
|
-
const best = (p, cands) => {
|
|
3177
|
-
if (!cands.length) return void 0;
|
|
3178
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3179
|
-
if (parent) {
|
|
3180
|
-
const direct = cands.find((c) => c.parent === parent);
|
|
3181
|
-
if (direct) return direct;
|
|
3182
|
-
}
|
|
3183
|
-
const anc = pairedAncestor(p);
|
|
3184
|
-
if (anc) {
|
|
3185
|
-
const under2 = cands.find((c) => ancestors(c.id).includes(anc));
|
|
3186
|
-
if (under2) return under2;
|
|
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
|
+
}
|
|
3187
3298
|
}
|
|
3188
|
-
return
|
|
3299
|
+
return { variants, components };
|
|
3189
3300
|
};
|
|
3190
|
-
const root = staticParts.find((p) => !p.parent);
|
|
3191
|
-
if (root) take(root.id, "root");
|
|
3192
|
-
const textOf = (p) => {
|
|
3193
|
-
if (p.characters) return normText(p.characters);
|
|
3194
|
-
const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
|
|
3195
|
-
return typeof prop?.default === "string" ? normText(prop.default) : "";
|
|
3196
|
-
};
|
|
3197
|
-
const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
|
|
3198
|
-
const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
|
|
3199
|
-
const shared = (p, m) => {
|
|
3200
|
-
const mine = new Set(classesOf(p));
|
|
3201
|
-
return mClasses(m).filter((c) => mine.has(c)).length;
|
|
3202
|
-
};
|
|
3203
|
-
const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
|
|
3204
|
-
const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
|
|
3205
|
-
for (const p of staticParts) {
|
|
3206
|
-
if (p.kind !== "text" || pairs2.has(p.id)) continue;
|
|
3207
|
-
const t = textOf(p);
|
|
3208
|
-
if (!t) continue;
|
|
3209
|
-
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))));
|
|
3210
|
-
if (hit) take(p.id, hit.id);
|
|
3211
|
-
}
|
|
3212
|
-
const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
|
|
3213
|
-
for (const p of staticParts) {
|
|
3214
|
-
if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
|
|
3215
|
-
const sp = sParts.get(p.parent);
|
|
3216
|
-
if (!sp || sp.kind !== "frame" || !sp.parent) continue;
|
|
3217
|
-
const cls = classOf(sp);
|
|
3218
|
-
const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
|
|
3219
|
-
if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
|
|
3220
|
-
}
|
|
3221
|
-
for (const p of staticParts) {
|
|
3222
|
-
if (pairs2.has(p.id) || !classesOf(p).length) continue;
|
|
3223
|
-
if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
|
|
3224
|
-
const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
|
|
3225
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3226
|
-
const under2 = parent ? all.filter((m) => m.parent === parent) : [];
|
|
3227
|
-
const cands = under2.length ? under2 : all;
|
|
3228
|
-
const most = Math.max(0, ...cands.map((m) => shared(p, m)));
|
|
3229
|
-
const hit = best(p, cands.filter((m) => shared(p, m) === most));
|
|
3230
|
-
if (hit) take(p.id, hit.id);
|
|
3231
|
-
}
|
|
3232
|
-
for (const p of staticParts) {
|
|
3233
|
-
if (pairs2.has(p.id)) continue;
|
|
3234
|
-
if (p.parent && !pairs2.has(p.parent)) continue;
|
|
3235
|
-
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3236
|
-
const known = p.kind === "text" && !!textOf(p);
|
|
3237
|
-
const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
|
|
3238
|
-
const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
|
|
3239
|
-
if (hit) take(p.id, hit.id);
|
|
3240
|
-
}
|
|
3241
|
-
for (const p of staticParts) {
|
|
3242
|
-
if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
|
|
3243
|
-
const mp = pairs2.get(p.parent);
|
|
3244
|
-
const t = textOf(p);
|
|
3245
|
-
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);
|
|
3246
|
-
}
|
|
3247
|
-
return pairs2;
|
|
3248
3301
|
}
|
|
3249
|
-
function
|
|
3250
|
-
const
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
|
|
3260
|
-
const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
|
|
3261
|
-
return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
|
|
3262
|
-
};
|
|
3263
|
-
for (const sp of staticParts) {
|
|
3264
|
-
const mid = pairs2.get(sp.id);
|
|
3265
|
-
if (!mid) {
|
|
3266
|
-
out.unmatchedParts.push(sp.id);
|
|
3267
|
-
continue;
|
|
3268
|
-
}
|
|
3269
|
-
const sv = staticValues[sp.id] ?? {};
|
|
3270
|
-
const mv = mid ? measured.values[mid] ?? {} : {};
|
|
3271
|
-
const mr = mid ? measured.raw[mid] ?? {} : {};
|
|
3272
|
-
const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
|
|
3273
|
-
for (const f of fields) {
|
|
3274
|
-
if ((f === "width" || f === "height") && sv[f] === void 0) continue;
|
|
3275
|
-
const s = sv[f];
|
|
3276
|
-
const m = mv[f] !== void 0 ? mv[f] : mr[f];
|
|
3277
|
-
if (s === void 0) {
|
|
3278
|
-
if (m !== void 0 && m !== 0) out.measuredOnly++;
|
|
3279
|
-
continue;
|
|
3280
|
-
}
|
|
3281
|
-
if (m === void 0 && !(f in mr)) {
|
|
3282
|
-
if (s === 0 || s === null) continue;
|
|
3283
|
-
const r = typeof s === "string" && resolve ? resolve(s) : s;
|
|
3284
|
-
if (r === 0 || r === "0px" || r === null) continue;
|
|
3285
|
-
out.staticOnly++;
|
|
3286
|
-
out.diffs.push({ part: sp.id, field: f, static: s, same: false });
|
|
3287
|
-
continue;
|
|
3288
|
-
}
|
|
3289
|
-
out.compared++;
|
|
3290
|
-
let same2 = s === m;
|
|
3291
|
-
if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
|
|
3292
|
-
if (same2 && typeof s === "string") out.sameToken++;
|
|
3293
|
-
if (!same2 && resolve) {
|
|
3294
|
-
let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
|
|
3295
|
-
const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
|
|
3296
|
-
if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
|
|
3297
|
-
same2 = near(rs, mr[f]);
|
|
3298
|
-
}
|
|
3299
|
-
if (same2) out.same++;
|
|
3300
|
-
else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
|
|
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 });
|
|
3301
3312
|
}
|
|
3302
3313
|
}
|
|
3303
|
-
|
|
3304
|
-
const box = measured.boxes.root;
|
|
3305
|
-
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 } };
|
|
3306
|
-
return out;
|
|
3307
|
-
}
|
|
3308
|
-
function tokenVariablePath(name, prefix) {
|
|
3309
|
-
let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
|
|
3310
|
-
if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
|
|
3311
|
-
return parts.join("/");
|
|
3314
|
+
return [...out.values()];
|
|
3312
3315
|
}
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
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;
|
|
3319
3331
|
}
|
|
3320
|
-
const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
3321
|
-
return top && n / names2.length >= 0.6 ? top : void 0;
|
|
3322
|
-
}
|
|
3323
|
-
function markUsedTokens(tokens, stories) {
|
|
3324
|
-
const byName = new Map(tokens.properties.map((p) => [p.name, p]));
|
|
3325
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3326
|
-
const use = (name) => {
|
|
3327
|
-
if (seen.has(name)) return;
|
|
3328
|
-
seen.add(name);
|
|
3329
|
-
const p = byName.get(name);
|
|
3330
|
-
if (!p) return;
|
|
3331
|
-
p.used = true;
|
|
3332
|
-
for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
|
|
3333
|
-
if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
|
|
3334
|
-
};
|
|
3335
|
-
for (const s of stories)
|
|
3336
|
-
for (const spec of [s.spec, ...s.instances ?? []])
|
|
3337
|
-
for (const part of Object.values(spec?.tokens ?? {}))
|
|
3338
|
-
for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
|
|
3339
|
-
const used = tokens.properties.filter((p) => p.used).length;
|
|
3340
|
-
tokens.counts.used = used;
|
|
3341
|
-
return used;
|
|
3342
3332
|
}
|
|
3343
|
-
|
|
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");
|
|
3344
3338
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
}
|
|
3352
|
-
throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
|
|
3356
|
-
if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
|
|
3357
|
-
if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
|
|
3358
|
-
if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
|
|
3359
|
-
data.stories.forEach((s, i) => {
|
|
3360
|
-
if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
|
|
3361
|
-
const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
|
|
3362
|
-
for (const sp of specs) {
|
|
3363
|
-
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`);
|
|
3364
|
-
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`);
|
|
3365
|
-
}
|
|
3366
|
-
if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
|
|
3367
|
-
});
|
|
3368
|
-
if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
|
|
3369
|
-
if (data.tokens !== void 0) {
|
|
3370
|
-
const t = data.tokens;
|
|
3371
|
-
if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
|
|
3372
|
-
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");
|
|
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();
|
|
3373
3346
|
}
|
|
3374
|
-
return
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
3378
|
-
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
3379
|
-
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
3380
|
-
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
3381
|
-
];
|
|
3382
|
-
const D50_TO_D65 = [
|
|
3383
|
-
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
3384
|
-
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
3385
|
-
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
3386
|
-
];
|
|
3387
|
-
const LINEAR_P3_TO_XYZ_D65 = [
|
|
3388
|
-
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
3389
|
-
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
3390
|
-
[0, 0.04511338185890264, 1.043944368900976]
|
|
3391
|
-
];
|
|
3392
|
-
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
3393
|
-
const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
|
|
3394
|
-
const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
|
|
3395
|
-
function oklabToLinearSrgb(L, a, b) {
|
|
3396
|
-
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
3397
|
-
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
3398
|
-
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
3399
|
-
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];
|
|
3347
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
3348
|
+
|
|
3349
|
+
${b}` : ""}`;
|
|
3400
3350
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
3408
|
-
const y = L > k * e ? fy ** 3 : L / k;
|
|
3409
|
-
const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
3410
|
-
return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
|
|
3411
|
-
}
|
|
3412
|
-
function num(s, full = 1) {
|
|
3413
|
-
if (s === void 0 || s === "none") return 0;
|
|
3414
|
-
if (s.endsWith("%")) return parseFloat(s) / 100 * full;
|
|
3415
|
-
if (/deg$/.test(s)) return parseFloat(s);
|
|
3416
|
-
if (/turn$/.test(s)) return parseFloat(s) * 360;
|
|
3417
|
-
if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
|
|
3418
|
-
return parseFloat(s);
|
|
3419
|
-
}
|
|
3420
|
-
function linearSrgb(fn, args) {
|
|
3421
|
-
switch (fn) {
|
|
3422
|
-
case "oklab":
|
|
3423
|
-
return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
|
|
3424
|
-
case "oklch": {
|
|
3425
|
-
const c = num(args[1], 0.4);
|
|
3426
|
-
const h = num(args[2]) * Math.PI / 180;
|
|
3427
|
-
return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
|
|
3428
|
-
}
|
|
3429
|
-
case "lab":
|
|
3430
|
-
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
|
|
3431
|
-
case "lch": {
|
|
3432
|
-
const c = num(args[1], 150);
|
|
3433
|
-
const h = num(args[2]) * Math.PI / 180;
|
|
3434
|
-
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
|
|
3435
|
-
}
|
|
3436
|
-
case "color": {
|
|
3437
|
-
const [space, ...c] = args;
|
|
3438
|
-
const v = [num(c[0]), num(c[1]), num(c[2])];
|
|
3439
|
-
if (space === "srgb") return v.map(toLinear);
|
|
3440
|
-
if (space === "srgb-linear") return v;
|
|
3441
|
-
if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
|
|
3442
|
-
if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
|
|
3443
|
-
if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
|
|
3444
|
-
return void 0;
|
|
3445
|
-
}
|
|
3446
|
-
}
|
|
3447
|
-
return void 0;
|
|
3448
|
-
}
|
|
3449
|
-
const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
|
|
3450
|
-
function cssColorToRgb(fn, inner) {
|
|
3451
|
-
const [main, alpha] = inner.split("/");
|
|
3452
|
-
const args = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
3453
|
-
const lin = linearSrgb(fn.toLowerCase(), args);
|
|
3454
|
-
if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
|
|
3455
|
-
const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
|
|
3456
|
-
const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
|
|
3457
|
-
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
|
|
3458
|
-
}
|
|
3459
|
-
function cssColorsToRgb(text) {
|
|
3460
|
-
if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
|
|
3461
|
-
return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
|
|
3462
|
-
}
|
|
3463
|
-
function withSrgbColors(value) {
|
|
3464
|
-
if (typeof value === "string") return cssColorsToRgb(value);
|
|
3465
|
-
if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
|
|
3466
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
|
|
3467
|
-
return value;
|
|
3468
|
-
}
|
|
3469
|
-
function parseBoxShadow(css) {
|
|
3470
|
-
if (!css || css.trim() === "none") return [];
|
|
3471
|
-
const out = [];
|
|
3472
|
-
for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
|
|
3473
|
-
const color = /rgba?\(([^)]*)\)/i.exec(one);
|
|
3474
|
-
const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
|
|
3475
|
-
const a = c[3] ?? 1;
|
|
3476
|
-
const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
|
|
3477
|
-
if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
|
|
3478
|
-
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 } });
|
|
3479
|
-
}
|
|
3480
|
-
return out;
|
|
3481
|
-
}
|
|
3482
|
-
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3483
|
-
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3484
|
-
const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
3485
|
-
function rawIssue(label, r, values2) {
|
|
3486
|
-
const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
|
|
3487
|
-
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.`;
|
|
3488
|
-
return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
|
|
3489
|
-
}
|
|
3490
|
-
function planComponentPush(d, ctx) {
|
|
3491
|
-
const issues = [...d.unknown];
|
|
3492
|
-
const reasons = [];
|
|
3493
|
-
const edits = /* @__PURE__ */ new Map();
|
|
3494
|
-
const also = /* @__PURE__ */ new Set();
|
|
3495
|
-
for (const r of d.rebinds) {
|
|
3496
|
-
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3497
|
-
if (r.raw?.length) {
|
|
3498
|
-
issues.push(rawIssue(label, r, r.raw));
|
|
3499
|
-
continue;
|
|
3500
|
-
}
|
|
3501
|
-
if (!r.after.length) {
|
|
3502
|
-
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." });
|
|
3503
|
-
continue;
|
|
3504
|
-
}
|
|
3505
|
-
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3506
|
-
if (missing.length) {
|
|
3507
|
-
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." });
|
|
3508
|
-
continue;
|
|
3509
|
-
}
|
|
3510
|
-
if (!r.before.length) {
|
|
3511
|
-
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
|
|
3512
|
-
continue;
|
|
3513
|
-
}
|
|
3514
|
-
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3515
|
-
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
|
|
3516
|
-
continue;
|
|
3517
|
-
}
|
|
3518
|
-
const [from, to] = [r.before[0], r.after[0]];
|
|
3519
|
-
if (ctx.tierOf(from) !== "component") {
|
|
3520
|
-
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
|
|
3521
|
-
continue;
|
|
3522
|
-
}
|
|
3523
|
-
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3524
|
-
if (elsewhere.variants.length) {
|
|
3525
|
-
const v = elsewhere.variants;
|
|
3526
|
-
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3527
|
-
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
|
|
3528
|
-
continue;
|
|
3529
|
-
}
|
|
3530
|
-
for (const c of elsewhere.components) also.add(c);
|
|
3531
|
-
const prev = edits.get(from);
|
|
3532
|
-
if (prev && prev.value !== `{${to}}`) {
|
|
3533
|
-
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
|
|
3534
|
-
edits.delete(from);
|
|
3535
|
-
continue;
|
|
3536
|
-
}
|
|
3537
|
-
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3538
|
-
}
|
|
3539
|
-
const list = [...edits.values()];
|
|
3540
|
-
if (d.needsCode || reasons.length) {
|
|
3541
|
-
return {
|
|
3542
|
-
route: "claude-draft",
|
|
3543
|
-
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.` : ""}`,
|
|
3544
|
-
edits: list,
|
|
3545
|
-
reasons,
|
|
3546
|
-
issues,
|
|
3547
|
-
alsoChanges: [...also]
|
|
3548
|
-
};
|
|
3549
|
-
}
|
|
3550
|
-
if (list.length) {
|
|
3551
|
-
const n = list.length;
|
|
3552
|
-
const others = [...also];
|
|
3553
|
-
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3554
|
-
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"}.` : "";
|
|
3555
|
-
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 };
|
|
3556
|
-
}
|
|
3557
|
-
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: [] };
|
|
3558
|
-
}
|
|
3559
|
-
function specsUsage(specs, component) {
|
|
3560
|
-
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3561
|
-
return (path, r) => {
|
|
3562
|
-
const variants = [];
|
|
3563
|
-
const components = [];
|
|
3564
|
-
for (const c of specs.components) {
|
|
3565
|
-
for (const v of c.variants) {
|
|
3566
|
-
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3567
|
-
const key = variantKeyOf(v.props);
|
|
3568
|
-
if (c === own) {
|
|
3569
|
-
if (!r.variants.includes(key)) variants.push(key);
|
|
3570
|
-
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
return { variants, components };
|
|
3574
|
-
};
|
|
3575
|
-
}
|
|
3576
|
-
function componentContrastPairs(edits, pairs2, tokens) {
|
|
3577
|
-
const out = /* @__PURE__ */ new Map();
|
|
3578
|
-
for (const e of edits) {
|
|
3579
|
-
const target = tokens.aliasOf(e.path);
|
|
3580
|
-
if (!target) continue;
|
|
3581
|
-
const group = e.path.split(".")[0];
|
|
3582
|
-
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3583
|
-
for (const p of pairs2) {
|
|
3584
|
-
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3585
|
-
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
return [...out.values()];
|
|
3589
|
-
}
|
|
3590
|
-
const FROM_FIGMA_LABEL = "from-figma";
|
|
3591
|
-
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3592
|
-
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3593
|
-
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3594
|
-
const ORIGIN_MARK = "bridge-from-figma";
|
|
3595
|
-
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3596
|
-
function parseFigmaOrigin(body) {
|
|
3597
|
-
if (!body) return void 0;
|
|
3598
|
-
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3599
|
-
if (!m) return void 0;
|
|
3600
|
-
try {
|
|
3601
|
-
const o = JSON.parse(m[1]);
|
|
3602
|
-
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3603
|
-
} catch {
|
|
3604
|
-
return void 0;
|
|
3605
|
-
}
|
|
3606
|
-
}
|
|
3607
|
-
function figmaOriginSection(o) {
|
|
3608
|
-
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3609
|
-
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3610
|
-
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}.`;
|
|
3611
|
-
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");
|
|
3612
|
-
}
|
|
3613
|
-
function withFigmaOrigin(body, o) {
|
|
3614
|
-
let b = (body ?? "").trim();
|
|
3615
|
-
const at = b.indexOf("**From Figma:**");
|
|
3616
|
-
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3617
|
-
if (at >= 0 && mark > at) {
|
|
3618
|
-
const end = b.indexOf("-->", mark);
|
|
3619
|
-
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3620
|
-
}
|
|
3621
|
-
return `${figmaOriginSection(o)}${b ? `
|
|
3622
|
-
|
|
3623
|
-
${b}` : ""}`;
|
|
3624
|
-
}
|
|
3625
|
-
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3626
|
-
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3627
|
-
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3628
|
-
function formatFigmaSpec(d, plan) {
|
|
3629
|
-
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3630
|
-
`;
|
|
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
|
+
`;
|
|
3631
3357
|
}
|
|
3632
3358
|
function parseFigmaSpec(text) {
|
|
3633
3359
|
if (!text) return void 0;
|
|
@@ -3704,1120 +3430,1599 @@ function figmaPushStatus(x) {
|
|
|
3704
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 };
|
|
3705
3431
|
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3706
3432
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
return found.length ? Math.max(...found) : null;
|
|
3716
|
-
}
|
|
3717
|
-
function claudeStage(labels) {
|
|
3718
|
-
const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
|
|
3719
|
-
if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
|
|
3720
|
-
if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
|
|
3721
|
-
return null;
|
|
3722
|
-
}
|
|
3723
|
-
const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
|
|
3724
|
-
function pullState(p) {
|
|
3725
|
-
if (p.merged_at || p.merged) return "merged";
|
|
3726
|
-
if (p.state && p.state !== "open") return "closed";
|
|
3727
|
-
if (isDraft(p)) return "draft";
|
|
3728
|
-
return "open";
|
|
3729
|
-
}
|
|
3730
|
-
const PULL_STATE_PILL = {
|
|
3731
|
-
merged: { label: "Merged", cls: "" },
|
|
3732
|
-
closed: { label: "Closed", cls: "" },
|
|
3733
|
-
draft: { label: "Draft", cls: "" },
|
|
3734
|
-
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"
|
|
3735
3441
|
};
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
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;
|
|
3741
3474
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
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;
|
|
3744
3488
|
}
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
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;
|
|
3763
3590
|
}
|
|
3764
|
-
|
|
3765
|
-
const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
|
|
3766
|
-
return { state, total, passed, failed, pending };
|
|
3591
|
+
return hex(h);
|
|
3767
3592
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
const
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
} else {
|
|
3792
|
-
design = a.length >= 1;
|
|
3793
|
-
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;
|
|
3794
3616
|
}
|
|
3795
|
-
return
|
|
3617
|
+
return hex(h);
|
|
3796
3618
|
}
|
|
3797
|
-
function
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
const
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
|
|
3805
|
-
}
|
|
3806
|
-
const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
|
|
3807
|
-
if (!asked.length) return [];
|
|
3808
|
-
return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
|
|
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);
|
|
3809
3626
|
}
|
|
3810
|
-
function
|
|
3811
|
-
|
|
3812
|
-
const s = suggestion.approvals;
|
|
3813
|
-
if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
|
|
3814
|
-
const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
|
|
3815
|
-
if (!covered) return pr;
|
|
3816
|
-
const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
|
|
3817
|
-
return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
|
|
3627
|
+
function canonicalJson(value) {
|
|
3628
|
+
return JSON.stringify(sortKeys(value));
|
|
3818
3629
|
}
|
|
3819
|
-
function
|
|
3820
|
-
if (
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
|
|
3827
|
-
}
|
|
3828
|
-
const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
3829
|
-
function canOverride(login, policy) {
|
|
3830
|
-
if (!login || !policy?.allowed.length) return false;
|
|
3831
|
-
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
3832
|
-
}
|
|
3833
|
-
function overrideHolders(policy, owners) {
|
|
3834
|
-
const allowed = policy?.allowed ?? [];
|
|
3835
|
-
const all = [...owners.design, ...owners.tech];
|
|
3836
|
-
if (!all.length || all.some((o) => o.includes("/"))) return allowed;
|
|
3837
|
-
const named = all.map(normLogin);
|
|
3838
|
-
return allowed.filter((l) => named.includes(normLogin(l)));
|
|
3839
|
-
}
|
|
3840
|
-
const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
|
|
3841
|
-
function mergeGate(input) {
|
|
3842
|
-
const blocking = [];
|
|
3843
|
-
const missing = [];
|
|
3844
|
-
const approvalReasons = [];
|
|
3845
|
-
const lifecycle = pullState(input);
|
|
3846
|
-
if (lifecycle === "merged" || lifecycle === "closed") {
|
|
3847
|
-
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) };
|
|
3848
|
-
}
|
|
3849
|
-
if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
|
|
3850
|
-
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
3851
|
-
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
3852
|
-
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3853
|
-
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3854
|
-
const tier = input.tier ?? 3;
|
|
3855
|
-
if (tier >= 2) {
|
|
3856
|
-
if (!input.approvals.design) {
|
|
3857
|
-
approvalReasons.push("It needs a design owner approval.");
|
|
3858
|
-
missing.push("design approval");
|
|
3859
|
-
}
|
|
3860
|
-
if (!input.approvals.tech && !input.approvals.valuesOnly) {
|
|
3861
|
-
approvalReasons.push("It needs a tech owner approval.");
|
|
3862
|
-
missing.push("tech approval");
|
|
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);
|
|
3863
3637
|
}
|
|
3864
|
-
|
|
3865
|
-
approvalReasons.push("It needs one owner approval.");
|
|
3866
|
-
missing.push("approval");
|
|
3638
|
+
return out;
|
|
3867
3639
|
}
|
|
3868
|
-
|
|
3869
|
-
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
3870
|
-
const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
3871
|
-
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
3872
|
-
const allowed = reasons.length === 0;
|
|
3873
|
-
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
3874
|
-
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
3640
|
+
return v;
|
|
3875
3641
|
}
|
|
3876
|
-
function
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
|
|
3880
|
-
const override = ownerOverridePolicy(input.override, input.owners);
|
|
3881
|
-
const state = input.state ?? "open";
|
|
3882
|
-
const draft = isDraft(input);
|
|
3883
|
-
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
3884
|
-
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;
|
|
3885
|
-
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
3886
|
-
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed) && state === "open" && !input.merged_at && !input.exploration;
|
|
3887
|
-
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
3888
|
-
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
3642
|
+
function shaForm(p) {
|
|
3643
|
+
if (!p.files) return p;
|
|
3644
|
+
return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
|
|
3889
3645
|
}
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
function overrideReasonMin(policy) {
|
|
3893
|
-
return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
|
|
3646
|
+
function suggestionPayloadSha(p) {
|
|
3647
|
+
return sha256Hex(canonicalJson(shaForm(p)));
|
|
3894
3648
|
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
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];
|
|
3901
3664
|
}
|
|
3902
|
-
function
|
|
3903
|
-
const
|
|
3904
|
-
return
|
|
3905
|
-
|
|
3906
|
-
Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
|
|
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);
|
|
3907
3668
|
}
|
|
3908
|
-
function
|
|
3909
|
-
return
|
|
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
|
+
];
|
|
3910
3683
|
}
|
|
3911
|
-
function
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
return
|
|
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];
|
|
3688
|
+
}
|
|
3689
|
+
return void 0;
|
|
3917
3690
|
}
|
|
3918
|
-
function
|
|
3919
|
-
|
|
3920
|
-
if (repo?.allow_squash_merge !== false) out.push("squash");
|
|
3921
|
-
if (repo?.allow_merge_commit !== false) out.push("merge");
|
|
3922
|
-
if (repo?.allow_rebase_merge !== false) out.push("rebase");
|
|
3923
|
-
return out;
|
|
3691
|
+
function suggestionStateFromLabels(labels) {
|
|
3692
|
+
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
3924
3693
|
}
|
|
3925
|
-
function
|
|
3926
|
-
|
|
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"));
|
|
3927
3698
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
"
|
|
3932
|
-
"
|
|
3933
|
-
"needs-
|
|
3934
|
-
"needs-approval": 5,
|
|
3935
|
-
ready: 6,
|
|
3936
|
-
"ci-running": 7,
|
|
3937
|
-
checking: 8,
|
|
3938
|
-
unknown: 9,
|
|
3939
|
-
draft: 10,
|
|
3940
|
-
merged: 11,
|
|
3941
|
-
closed: 12
|
|
3942
|
-
};
|
|
3943
|
-
function reviewStatusKind(input) {
|
|
3944
|
-
const out = (kind) => kind;
|
|
3945
|
-
const lifecycle = pullState(input);
|
|
3946
|
-
if (lifecycle === "merged") return out("merged");
|
|
3947
|
-
if (lifecycle === "closed") return out("closed");
|
|
3948
|
-
if (lifecycle === "draft") return out("draft");
|
|
3949
|
-
if (input.ci?.state === "fail") return out("ci-failing");
|
|
3950
|
-
if (input.approvals?.changesRequested.length) return out("changes-requested");
|
|
3951
|
-
if (input.mergeable === false) return out("conflict");
|
|
3952
|
-
if (input.ci?.state === "pending") return out("ci-running");
|
|
3953
|
-
if (!input.ci || !input.approvals) return out("unknown");
|
|
3954
|
-
const tier = input.tier ?? 3;
|
|
3955
|
-
if (tier >= 2) {
|
|
3956
|
-
if (!input.approvals.design) return out("needs-design");
|
|
3957
|
-
if (!input.approvals.tech) return out("needs-tech");
|
|
3958
|
-
} else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
|
|
3959
|
-
if (input.mergeable === null) return out("checking");
|
|
3960
|
-
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 });
|
|
3961
|
-
return gate.allowed ? out("ready") : out("unknown");
|
|
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";
|
|
3962
3705
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
closed: "Closed",
|
|
3966
|
-
draft: "Draft",
|
|
3967
|
-
"ci-failing": "Blocked by tests",
|
|
3968
|
-
"changes-requested": "Changes requested",
|
|
3969
|
-
"ci-running": "Tests running",
|
|
3970
|
-
"needs-design": "Needs design approval",
|
|
3971
|
-
"needs-tech": "Needs tech approval",
|
|
3972
|
-
"needs-approval": "Needs approval",
|
|
3973
|
-
conflict: "Conflict with main",
|
|
3974
|
-
checking: "Checking for conflicts",
|
|
3975
|
-
ready: "Ready to merge",
|
|
3976
|
-
unknown: "Status unknown"
|
|
3977
|
-
};
|
|
3978
|
-
const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
|
|
3979
|
-
function reviewStatusLabel(kind, lookAgain) {
|
|
3980
|
-
return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
|
|
3706
|
+
function withOutdated(status, outdated) {
|
|
3707
|
+
return outdated && (status === "needs-review" || status === "changes-requested" || status === "approved") ? "outdated" : status;
|
|
3981
3708
|
}
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
pending: "Tests running",
|
|
3986
|
-
none: "No automated checks"
|
|
3987
|
-
};
|
|
3988
|
-
const BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
|
|
3989
|
-
const isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
|
|
3990
|
-
function isAutomationPull(p) {
|
|
3991
|
-
return isReleasePull(p) || BOTS.has((p.user?.login ?? "").toLowerCase());
|
|
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;
|
|
3992
3712
|
}
|
|
3993
|
-
const
|
|
3994
|
-
function
|
|
3995
|
-
|
|
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
|
+
};
|
|
3996
3721
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
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
|
+
];
|
|
3999
3738
|
}
|
|
4000
|
-
function
|
|
4001
|
-
const
|
|
4002
|
-
if (
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
if (
|
|
4014
|
-
if (
|
|
4015
|
-
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
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]);
|
|
4021
|
-
}
|
|
4022
|
-
function pendingApprovals(tier, approvals) {
|
|
4023
|
-
if (tier == null || !approvals) return [];
|
|
4024
|
-
return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4025
|
-
}
|
|
4026
|
-
const uniqLogins = (xs) => xs.filter((x, i) => xs.findIndex((y) => normLogin(y) === normLogin(x)) === i);
|
|
4027
|
-
function eligibleReviewers(role, p) {
|
|
4028
|
-
const raw = role === "design" ? p.owners.design : role === "tech" ? p.owners.tech : uniqLogins([...p.owners.design, ...p.owners.tech]);
|
|
4029
|
-
const listed = raw.map((x) => x.replace(/^@/, ""));
|
|
4030
|
-
const approvers = (p.approvers ?? []).map(normLogin);
|
|
4031
|
-
const logins = listed.filter((l) => normLogin(l) !== normLogin(p.author) && !approvers.includes(normLogin(l)));
|
|
4032
|
-
return { open: listed.length === 0, logins, listed };
|
|
4033
|
-
}
|
|
4034
|
-
function reviewersToAsk(x) {
|
|
4035
|
-
const roles = pendingApprovals(x.tier, x.approvals);
|
|
4036
|
-
const all = roles.flatMap((r) => eligibleReviewers(r, { owners: x.owners, author: x.author, approvers: x.approvals.approvers }).logins);
|
|
4037
|
-
return uniqLogins(all).filter((l) => normLogin(l) !== normLogin(x.viewer ?? "") && normLogin(l) !== normLogin(x.author));
|
|
4038
|
-
}
|
|
4039
|
-
const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6}$/;
|
|
4040
|
-
function parseJiraKey(input) {
|
|
4041
|
-
const t = (input ?? "").trim().toUpperCase();
|
|
4042
|
-
if (!t) return null;
|
|
4043
|
-
if (!JIRA_KEY.test(t)) throw new Error("Write the Jira ticket as its key, like DS-42, or leave it empty.");
|
|
4044
|
-
return t;
|
|
4045
|
-
}
|
|
4046
|
-
const isJiraKey = (input) => JIRA_KEY.test(input.trim().toUpperCase());
|
|
4047
|
-
const LEADING_KEY = /^\[?([A-Z][A-Z0-9_]{0,9}-[1-9]\d{0,6})\]?[:\s]\s*/;
|
|
4048
|
-
function jiraKeyOf(title) {
|
|
4049
|
-
return LEADING_KEY.exec(title.trim())?.[1] ?? null;
|
|
4050
|
-
}
|
|
4051
|
-
const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
|
|
4052
|
-
function titleWithJiraKey(title, key) {
|
|
4053
|
-
if (!key) return title;
|
|
4054
|
-
return `${key} ${withoutJiraKey(title)}`;
|
|
4055
|
-
}
|
|
4056
|
-
function branchWithJiraKey(branch, key) {
|
|
4057
|
-
if (!key || branch.includes(key)) return branch;
|
|
4058
|
-
const i = branch.lastIndexOf("/");
|
|
4059
|
-
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
3739
|
+
function changesSection(p, max) {
|
|
3740
|
+
const out = [];
|
|
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
|
+
}
|
|
3758
|
+
return out;
|
|
4060
3759
|
}
|
|
4061
|
-
|
|
4062
|
-
const
|
|
4063
|
-
const
|
|
4064
|
-
const
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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
|
+
];
|
|
4072
3774
|
}
|
|
4073
|
-
function
|
|
4074
|
-
const
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
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
|
+
);
|
|
4087
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, ""] : [];
|
|
4088
3797
|
}
|
|
4089
|
-
function
|
|
4090
|
-
const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
|
|
4091
|
-
const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
|
|
3798
|
+
function humanPart(p, details, max, repo) {
|
|
4092
3799
|
return [
|
|
4093
|
-
|
|
3800
|
+
`## Suggested change: ${safeText(p.title.trim())}`,
|
|
4094
3801
|
"",
|
|
4095
|
-
|
|
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>`
|
|
4096
3817
|
].join("\n");
|
|
4097
3818
|
}
|
|
4098
|
-
function
|
|
4099
|
-
const
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
const r = JSON.parse(m[1]);
|
|
4103
|
-
return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
|
|
4104
|
-
} catch {
|
|
4105
|
-
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;
|
|
4106
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;
|
|
4107
3828
|
}
|
|
4108
|
-
const
|
|
4109
|
-
const
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
|
|
4124
|
-
if (!counts) {
|
|
4125
|
-
ignored.add(c.author);
|
|
4126
|
-
return;
|
|
4127
|
-
}
|
|
4128
|
-
const at = String(i).padStart(6, "0");
|
|
4129
|
-
if (r.payloadSha !== opts.payloadSha) {
|
|
4130
|
-
latestOld.set(c.author, r.event);
|
|
4131
|
-
reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
|
|
4132
|
-
return;
|
|
4133
|
-
}
|
|
4134
|
-
latestOld.delete(c.author);
|
|
4135
|
-
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
4136
|
-
});
|
|
4137
|
-
const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
|
|
4138
|
-
const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
|
|
4139
|
-
return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
|
|
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");
|
|
4140
3844
|
}
|
|
4141
|
-
function
|
|
4142
|
-
|
|
4143
|
-
|
|
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");
|
|
4144
3854
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
}
|
|
4167
|
-
|
|
4168
|
-
const
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
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;
|
|
3874
|
+
}
|
|
3875
|
+
return i;
|
|
3876
|
+
}
|
|
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;
|
|
3916
|
+
}
|
|
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);
|
|
3935
|
+
});
|
|
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 };
|
|
3942
|
+
}
|
|
3943
|
+
function hasSuggestionBlock(body) {
|
|
3944
|
+
return new RegExp(`^<!-- ${MARK}:v\\d+ sha256=`, "m").test(body ?? "");
|
|
3945
|
+
}
|
|
3946
|
+
function fencedAfter(text, from) {
|
|
3947
|
+
const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
|
|
3948
|
+
return m?.[1];
|
|
3949
|
+
}
|
|
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 };
|
|
4011
|
+
}
|
|
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}/`);
|
|
4178
4066
|
};
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
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);
|
|
4184
4276
|
}
|
|
4185
|
-
|
|
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;
|
|
4186
4313
|
}
|
|
4187
|
-
|
|
4188
|
-
const
|
|
4189
|
-
const
|
|
4190
|
-
|
|
4191
|
-
const
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
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;
|
|
4200
4372
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
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]
|
|
4266
4446
|
];
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
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));
|
|
4280
4493
|
}
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
const
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
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;
|
|
4293
4510
|
}
|
|
4294
|
-
h[0] = h[0] + a | 0;
|
|
4295
|
-
h[1] = h[1] + b | 0;
|
|
4296
|
-
h[2] = h[2] + c | 0;
|
|
4297
|
-
h[3] = h[3] + d | 0;
|
|
4298
|
-
h[4] = h[4] + e | 0;
|
|
4299
|
-
h[5] = h[5] + f | 0;
|
|
4300
|
-
h[6] = h[6] + g | 0;
|
|
4301
|
-
h[7] = h[7] + hh | 0;
|
|
4302
4511
|
}
|
|
4303
|
-
return
|
|
4512
|
+
return void 0;
|
|
4304
4513
|
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
const
|
|
4308
|
-
const
|
|
4309
|
-
const
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
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 } });
|
|
4544
|
+
}
|
|
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);
|
|
4328
4645
|
}
|
|
4329
|
-
|
|
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) : [];
|
|
4330
4649
|
}
|
|
4331
|
-
function
|
|
4332
|
-
|
|
4333
|
-
const
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
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) };
|
|
4338
4658
|
}
|
|
4339
|
-
function
|
|
4340
|
-
|
|
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 };
|
|
4341
4663
|
}
|
|
4342
|
-
function
|
|
4343
|
-
if (
|
|
4344
|
-
|
|
4345
|
-
const out = {};
|
|
4346
|
-
for (const k of Object.keys(v).sort()) {
|
|
4347
|
-
const x = v[k];
|
|
4348
|
-
if (x !== void 0) out[k] = sortKeys(x);
|
|
4349
|
-
}
|
|
4350
|
-
return out;
|
|
4351
|
-
}
|
|
4352
|
-
return v;
|
|
4664
|
+
function withTokenValueApprovals(pr, valuesOnly) {
|
|
4665
|
+
if (!valuesOnly) return pr;
|
|
4666
|
+
return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
|
|
4353
4667
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
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));
|
|
4357
4672
|
}
|
|
4358
|
-
function
|
|
4359
|
-
|
|
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)));
|
|
4360
4679
|
}
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
};
|
|
4369
|
-
function suggestionSubject(p) {
|
|
4370
|
-
if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
|
|
4371
|
-
if (p.kind === "docs" && p.docs?.length) {
|
|
4372
|
-
const names2 = [...new Set(p.docs.map((d) => d.component))];
|
|
4373
|
-
return names2.length === 1 ? names2[0] : KIND_SUBJECT.docs;
|
|
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) };
|
|
4374
4688
|
}
|
|
4375
|
-
|
|
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");
|
|
4699
|
+
}
|
|
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");
|
|
4707
|
+
}
|
|
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 };
|
|
4376
4715
|
}
|
|
4377
|
-
function
|
|
4378
|
-
const
|
|
4379
|
-
|
|
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 };
|
|
4380
4730
|
}
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
`suggestion: ${p.kind}`,
|
|
4386
|
-
`source: ${p.source}`,
|
|
4387
|
-
`tier: ${p.tier.tier}`,
|
|
4388
|
-
SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
|
|
4389
|
-
...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
|
|
4390
|
-
...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
|
|
4391
|
-
...p.links?.exploration ? [EXPLORATION_LABEL] : [],
|
|
4392
|
-
...opts.labels ?? []
|
|
4393
|
-
])
|
|
4394
|
-
];
|
|
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;
|
|
4395
4735
|
}
|
|
4396
|
-
function
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
}
|
|
4401
|
-
return
|
|
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;
|
|
4402
4742
|
}
|
|
4403
|
-
function
|
|
4404
|
-
|
|
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.`;
|
|
4405
4748
|
}
|
|
4406
|
-
function
|
|
4407
|
-
|
|
4408
|
-
if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
|
|
4409
|
-
return (p.files ?? []).every((f) => f.path.endsWith(".md"));
|
|
4749
|
+
function overrideRefusedComment(body, message) {
|
|
4750
|
+
return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
|
|
4410
4751
|
}
|
|
4411
|
-
function
|
|
4412
|
-
const
|
|
4413
|
-
if (
|
|
4414
|
-
|
|
4415
|
-
if (
|
|
4416
|
-
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") };
|
|
4417
4758
|
}
|
|
4418
|
-
function
|
|
4419
|
-
|
|
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;
|
|
4420
4765
|
}
|
|
4421
|
-
function
|
|
4422
|
-
|
|
4423
|
-
return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
|
|
4766
|
+
function hasWriteAccess(perms) {
|
|
4767
|
+
return Boolean(perms && (perms.admin || perms.maintain || perms.push));
|
|
4424
4768
|
}
|
|
4425
|
-
const
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
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");
|
|
4803
|
+
}
|
|
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];
|
|
4822
|
+
}
|
|
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());
|
|
4433
4833
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
const PART_SCHEMA = "bridge.suggestion-part/1";
|
|
4437
|
-
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
4438
|
-
const safeText = (s) => s.replace(/<!--/g, "<!--");
|
|
4439
|
-
const sourceLabel = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
4440
|
-
const show$1 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
4441
|
-
function table(head, rows, max) {
|
|
4442
|
-
if (!rows.length) return [];
|
|
4443
|
-
const shown = rows.slice(0, max);
|
|
4444
|
-
return [
|
|
4445
|
-
`| ${head.join(" | ")} |`,
|
|
4446
|
-
`| ${head.map(() => "---").join(" | ")} |`,
|
|
4447
|
-
...shown.map((r) => `| ${r.map((c) => cell(c)).join(" | ")} |`),
|
|
4448
|
-
...rows.length > max ? ["", `and ${rows.length - max} more`] : []
|
|
4449
|
-
];
|
|
4834
|
+
function reviewQueue(pulls) {
|
|
4835
|
+
return openPulls(pulls).filter((p) => !isAutomationPull(p) && !isHeldExploration(p));
|
|
4450
4836
|
}
|
|
4451
|
-
function
|
|
4452
|
-
|
|
4453
|
-
if (p.tokens?.changes.length) {
|
|
4454
|
-
const rows = p.tokens.changes.map((c) => [
|
|
4455
|
-
`\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
|
|
4456
|
-
c.beforeCss ?? show$1(c.before),
|
|
4457
|
-
c.afterCss ?? show$1(c.after)
|
|
4458
|
-
]);
|
|
4459
|
-
out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
|
|
4460
|
-
} else if (p.tokens?.edits.length) {
|
|
4461
|
-
const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show$1(e.value)]);
|
|
4462
|
-
out.push(...table(["Token", "Edit", "Value"], rows, max), "");
|
|
4463
|
-
}
|
|
4464
|
-
if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
|
|
4465
|
-
if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
|
|
4466
|
-
if (p.files?.length) {
|
|
4467
|
-
const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
|
|
4468
|
-
out.push(...table(["File", "Change"], rows, max), "");
|
|
4469
|
-
}
|
|
4470
|
-
return out;
|
|
4837
|
+
function releasePull(pulls) {
|
|
4838
|
+
return openPulls(pulls).find((p) => isReleasePull(p)) ?? null;
|
|
4471
4839
|
}
|
|
4472
|
-
function
|
|
4473
|
-
const
|
|
4474
|
-
|
|
4475
|
-
const
|
|
4476
|
-
return
|
|
4477
|
-
"## Checks when suggested",
|
|
4478
|
-
"",
|
|
4479
|
-
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
4480
|
-
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
4481
|
-
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
|
|
4482
|
-
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
4483
|
-
"",
|
|
4484
|
-
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
4485
|
-
];
|
|
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);
|
|
4486
4845
|
}
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
out.push(`- ${what}: ${url ? `[node ${n.nodeId}](${url})` : `node ${n.nodeId}`}`);
|
|
4498
|
-
}
|
|
4499
|
-
if (key && (nodes.length || l.figma?.nodeId)) {
|
|
4500
|
-
const ids = nodes.length ? nodes.map((n) => n.nodeId) : [l.figma.nodeId];
|
|
4501
|
-
out.push(
|
|
4502
|
-
`- 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.`
|
|
4503
|
-
);
|
|
4504
|
-
}
|
|
4505
|
-
if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
|
|
4506
|
-
if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
|
|
4507
|
-
for (const v of p.previews ?? []) out.push(`- ${cell(v.label)}: ${v.url}`);
|
|
4508
|
-
return out.length ? ["## Previews and links", "", ...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" };
|
|
4509
4856
|
}
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
safeText(p.summary.trim()),
|
|
4515
|
-
"",
|
|
4516
|
-
...changesSection(p, max),
|
|
4517
|
-
...details?.trim() ? [safeText(details.trim()), ""] : [],
|
|
4518
|
-
...checksSection(p, max),
|
|
4519
|
-
...linksSection(p),
|
|
4520
|
-
"## How to apply",
|
|
4521
|
-
"",
|
|
4522
|
-
"- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
|
|
4523
|
-
'- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
|
|
4524
|
-
` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
|
|
4525
|
-
"",
|
|
4526
|
-
"The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
|
|
4527
|
-
"",
|
|
4528
|
-
`<sub>Suggested from ${sourceLabel(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
|
|
4529
|
-
].join("\n");
|
|
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]);
|
|
4530
4861
|
}
|
|
4531
|
-
function
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
if (h2.length <= SUGGESTION_LIMITS.human) return h2;
|
|
4535
|
-
}
|
|
4536
|
-
const h = humanPart(p, void 0, 3, repo);
|
|
4537
|
-
if (h.length <= SUGGESTION_LIMITS.human) return h;
|
|
4538
|
-
const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
|
|
4539
|
-
return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
|
|
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]);
|
|
4540
4865
|
}
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
const
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
return [
|
|
4549
|
-
"<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
|
|
4550
|
-
"",
|
|
4551
|
-
`<!-- ${MARK}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
|
|
4552
|
-
fenced(json),
|
|
4553
|
-
"",
|
|
4554
|
-
"</details>"
|
|
4555
|
-
].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 };
|
|
4556
4873
|
}
|
|
4557
|
-
function
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
`<!-- ${MARK}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
|
|
4562
|
-
fenced(json),
|
|
4563
|
-
"",
|
|
4564
|
-
"</details>"
|
|
4565
|
-
].join("\n");
|
|
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));
|
|
4566
4878
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
let len = 1;
|
|
4574
|
-
if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
|
|
4575
|
-
else if (c < 32) len = 6;
|
|
4576
|
-
else if (c >= 55296 && c <= 56319) {
|
|
4577
|
-
const d = s.charCodeAt(i + 1);
|
|
4578
|
-
if (d >= 56320 && d <= 57343) {
|
|
4579
|
-
w = 2;
|
|
4580
|
-
len = 2;
|
|
4581
|
-
} else len = 6;
|
|
4582
|
-
} else if (c >= 56320 && c <= 57343) len = 6;
|
|
4583
|
-
if (used + len > budget) break;
|
|
4584
|
-
used += len;
|
|
4585
|
-
i += w;
|
|
4586
|
-
}
|
|
4587
|
-
return i;
|
|
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;
|
|
4588
4885
|
}
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
const full = shaForm(payload);
|
|
4594
|
-
const sha = suggestionPayloadSha(full);
|
|
4595
|
-
const human = fitHuman(full, opts.details, opts.repo);
|
|
4596
|
-
const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
|
|
4597
|
-
const inline = fit(full);
|
|
4598
|
-
if (inline.length <= SUGGESTION_LIMITS.inline) {
|
|
4599
|
-
return { ...base, body: `${human}
|
|
4600
|
-
|
|
4601
|
-
${payloadBlock(inline, sha, 0)}
|
|
4602
|
-
`, parts: [] };
|
|
4603
|
-
}
|
|
4604
|
-
const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
|
|
4605
|
-
const parts = [];
|
|
4606
|
-
const firstPart = /* @__PURE__ */ new Map();
|
|
4607
|
-
const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
|
|
4608
|
-
const emptyOverhead = partJson([], 9, 9).length;
|
|
4609
|
-
let cur = [];
|
|
4610
|
-
let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
4611
|
-
for (const { f, i } of moved) {
|
|
4612
|
-
const text = f.content;
|
|
4613
|
-
let at = 0;
|
|
4614
|
-
while (at < text.length) {
|
|
4615
|
-
const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
|
|
4616
|
-
if (room - entryOverhead < 256) {
|
|
4617
|
-
parts.push(cur);
|
|
4618
|
-
cur = [];
|
|
4619
|
-
room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
4620
|
-
}
|
|
4621
|
-
const end = escapedEnd(text, at, room - entryOverhead);
|
|
4622
|
-
if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4623
|
-
if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
|
|
4624
|
-
const chunk = { i, path: f.path, text: text.slice(at, end) };
|
|
4625
|
-
cur.push(chunk);
|
|
4626
|
-
room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
|
|
4627
|
-
at = end;
|
|
4628
|
-
}
|
|
4629
|
-
}
|
|
4630
|
-
if (cur.length) parts.push(cur);
|
|
4631
|
-
if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4632
|
-
const skeleton = {
|
|
4633
|
-
...full,
|
|
4634
|
-
files: (full.files ?? []).map((f, i) => {
|
|
4635
|
-
if (!firstPart.has(i)) return f;
|
|
4636
|
-
const { content: _content, ...rest } = f;
|
|
4637
|
-
return { ...rest, part: firstPart.get(i) };
|
|
4638
|
-
})
|
|
4639
|
-
};
|
|
4640
|
-
const skeletonJson = fit(skeleton);
|
|
4641
|
-
if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4642
|
-
const n = parts.length;
|
|
4643
|
-
const partBodies = parts.map((chunks, k) => {
|
|
4644
|
-
const json = partJson(chunks, k + 1, n);
|
|
4645
|
-
if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4646
|
-
return partComment(k + 1, n, json);
|
|
4647
|
-
});
|
|
4648
|
-
const body = `${human}
|
|
4649
|
-
|
|
4650
|
-
${payloadBlock(skeletonJson, sha, n)}
|
|
4651
|
-
`;
|
|
4652
|
-
if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
4653
|
-
return { ...base, body, parts: partBodies };
|
|
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;
|
|
4654
4890
|
}
|
|
4655
|
-
|
|
4656
|
-
|
|
4891
|
+
const withoutJiraKey = (title) => title.trim().replace(LEADING_KEY, "");
|
|
4892
|
+
function titleWithJiraKey(title, key) {
|
|
4893
|
+
if (!key) return title;
|
|
4894
|
+
return `${key} ${withoutJiraKey(title)}`;
|
|
4657
4895
|
}
|
|
4658
|
-
function
|
|
4659
|
-
|
|
4660
|
-
|
|
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)}`;
|
|
4661
4900
|
}
|
|
4662
|
-
const
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
const
|
|
4670
|
-
|
|
4671
|
-
|
|
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");
|
|
4912
|
+
}
|
|
4913
|
+
function parseSuggestionReviewComment(body) {
|
|
4914
|
+
const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4915
|
+
if (!m) return null;
|
|
4672
4916
|
try {
|
|
4673
|
-
|
|
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
|
+
};
|
|
4674
4925
|
} catch {
|
|
4675
|
-
return
|
|
4676
|
-
}
|
|
4677
|
-
const schema = raw?.schema;
|
|
4678
|
-
if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
|
|
4679
|
-
const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
|
|
4680
|
-
return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
|
|
4926
|
+
return null;
|
|
4681
4927
|
}
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
const
|
|
4685
|
-
const
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
if (sha256Hex(pj) !== m[3]) edited = true;
|
|
4701
|
-
try {
|
|
4702
|
-
const part = JSON.parse(pj);
|
|
4703
|
-
if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
|
|
4704
|
-
found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
|
|
4705
|
-
} catch {
|
|
4706
|
-
continue;
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
for (let i = 1; i <= want; i++) {
|
|
4710
|
-
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.`);
|
|
4711
|
-
}
|
|
4712
|
-
const files = (payload.files ?? []).map((f, idx) => {
|
|
4713
|
-
if (f.part === void 0) return f;
|
|
4714
|
-
let content = "";
|
|
4715
|
-
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;
|
|
4716
|
-
const { part: _part, ...rest } = f;
|
|
4717
|
-
return { ...rest, content };
|
|
4718
|
-
});
|
|
4719
|
-
payload.files = files;
|
|
4928
|
+
}
|
|
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");
|
|
4937
|
+
}
|
|
4938
|
+
function parseSuggestionAppliedComment(body) {
|
|
4939
|
+
const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4940
|
+
if (!m) return null;
|
|
4941
|
+
try {
|
|
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;
|
|
4944
|
+
} catch {
|
|
4945
|
+
return null;
|
|
4720
4946
|
}
|
|
4721
|
-
const sha = suggestionPayloadSha(payload);
|
|
4722
|
-
return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
|
|
4723
4947
|
}
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
const
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
if (!
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
4743
|
-
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" }.');
|
|
4744
|
-
const spec = p.componentSpec;
|
|
4745
|
-
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" }.');
|
|
4746
|
-
const nodes = p.links?.figma?.nodes;
|
|
4747
|
-
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"? }.');
|
|
4748
|
-
if (p.files !== void 0) {
|
|
4749
|
-
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
4750
|
-
else {
|
|
4751
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4752
|
-
for (const f of p.files) {
|
|
4753
|
-
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);
|
|
4754
|
-
if (!ok) {
|
|
4755
|
-
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
4756
|
-
break;
|
|
4757
|
-
}
|
|
4758
|
-
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
4759
|
-
seen.add(f.path);
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
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;
|
|
4762
4966
|
}
|
|
4763
|
-
return out;
|
|
4764
4967
|
}
|
|
4765
|
-
function
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
if (
|
|
4769
|
-
if (
|
|
4770
|
-
const
|
|
4771
|
-
if (
|
|
4772
|
-
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.";
|
|
4773
4975
|
return null;
|
|
4774
4976
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
const tokenRoot2 = tokensPath(config) || ".";
|
|
4784
|
-
const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
|
|
4785
|
-
switch (kind) {
|
|
4786
|
-
case "token-change":
|
|
4787
|
-
return [tokenRoot2, ...changesets];
|
|
4788
|
-
case "docs":
|
|
4789
|
-
return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
|
|
4790
|
-
case "component-spec":
|
|
4791
|
-
return [".bridge/figma"];
|
|
4792
|
-
case "brand":
|
|
4793
|
-
return [tokenRoot2, ...changesets, CONFIG_FILE];
|
|
4794
|
-
case "config":
|
|
4795
|
-
return [CONFIG_FILE];
|
|
4796
|
-
case "revert":
|
|
4797
|
-
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;
|
|
4798
4985
|
}
|
|
4986
|
+
return null;
|
|
4799
4987
|
}
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
const
|
|
4804
|
-
const
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
if (
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
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";
|
|
4821
5026
|
}
|
|
4822
5027
|
const SUGGESTION_APPLY_EXIT = {
|
|
4823
5028
|
ready: 0,
|
|
@@ -4952,7 +5157,7 @@ async function planSuggestionApply(input) {
|
|
|
4952
5157
|
for (const p of new Set(targets)) if (await ws.isSymlink(p)) links.push(`"${p}" is a symbolic link (or inside one): Bridge never writes through links.`);
|
|
4953
5158
|
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
4954
5159
|
}
|
|
4955
|
-
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
5160
|
+
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth, override: config.governance.override ?? null };
|
|
4956
5161
|
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
|
|
4957
5162
|
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
4958
5163
|
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
@@ -5248,6 +5453,27 @@ async function reviewSuggestionOnGitHub(gh, n, input, gov) {
|
|
|
5248
5453
|
if (labelWarning) warnings.push(labelWarning);
|
|
5249
5454
|
return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
|
|
5250
5455
|
}
|
|
5456
|
+
async function overrideSuggestionOnGitHub(gh, n, input, gov) {
|
|
5457
|
+
if (!gov.override) throw new GitHubError(422, "The review rules allow no owner override.");
|
|
5458
|
+
const before = await getSuggestionOnGitHub(gh, n, gov);
|
|
5459
|
+
if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be approved: ${before.parsed.message}`);
|
|
5460
|
+
if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed.`);
|
|
5461
|
+
const sha = before.parsed.sha;
|
|
5462
|
+
if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and approve the new version.`);
|
|
5463
|
+
const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
|
|
5464
|
+
const problem = suggestionOverrideProblem({ viewer: by, author: before.issue.author, gov }) ?? overrideReasonProblem(input.reason, gov.override);
|
|
5465
|
+
if (problem) throw new GitHubError(422, problem);
|
|
5466
|
+
if (before.approvals?.changesRequested.length) throw new GitHubError(422, `Changes were asked for on #${n}: the owner override covers missing approvals only.`);
|
|
5467
|
+
const missing = pendingApprovals(before.parsed.payload.tier.tier, before.approvals ?? { approvers: [], changesRequested: [], design: false, tech: false }).map((r) => r === "design" ? "design approval" : r === "tech" ? "tech approval" : "approval");
|
|
5468
|
+
const posted = await gh.createIssueComment(n, composeSuggestionOverrideComment({ login: by, reason: input.reason, missing, payloadSha: sha }));
|
|
5469
|
+
const after = await getSuggestionOnGitHub(gh, n, gov);
|
|
5470
|
+
const counts = Boolean(after.approvals?.override);
|
|
5471
|
+
const warnings = [];
|
|
5472
|
+
if (!counts) warnings.push("The owner override is posted but does not count: check the owners and the override in bridge.config.json.");
|
|
5473
|
+
const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, after.status);
|
|
5474
|
+
if (labelWarning) warnings.push(labelWarning);
|
|
5475
|
+
return { commentUrl: posted.url, payloadSha: sha, status: after.status, counts, warnings };
|
|
5476
|
+
}
|
|
5251
5477
|
async function linkSuggestionPrOnGitHub(gh, n, link) {
|
|
5252
5478
|
if (!link.pr && !link.branch && !link.commit) throw new GitHubError(422, "Say where it is applied: a pull request number, a branch or a commit.");
|
|
5253
5479
|
const issue = await gh.getIssue(n);
|
|
@@ -5678,317 +5904,337 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
|
|
|
5678
5904
|
return out;
|
|
5679
5905
|
}
|
|
5680
5906
|
export {
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5907
|
+
overrideComment as $,
|
|
5908
|
+
explorationNote as A,
|
|
5909
|
+
explorationNotePath as B,
|
|
5910
|
+
explorationPrTitle as C,
|
|
5685
5911
|
DRAFT_LINE_MAX as D,
|
|
5686
|
-
|
|
5687
|
-
|
|
5912
|
+
EXPLORATION_ARCHIVED_LABEL as E,
|
|
5913
|
+
explorationPrBody as F,
|
|
5688
5914
|
GitHubError as G,
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
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,
|
|
5697
5923
|
PROSE_KEYS as P,
|
|
5698
|
-
|
|
5924
|
+
explorationRefLabel as Q,
|
|
5699
5925
|
REQUESTS_BRANCH as R,
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
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 _,
|
|
5709
5935
|
parseChangelog as a,
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
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,
|
|
5774
6000
|
buildReport as b,
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
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
|
-
|
|
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,
|
|
5839
6065
|
collectHistory as c,
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
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
|
-
|
|
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,
|
|
5904
6130
|
applyTokenEdits as d,
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
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
|
-
|
|
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,
|
|
5968
6194
|
buildHistoryEntry as e,
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
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,
|
|
5992
6238
|
coerceValue as f,
|
|
5993
6239
|
checkLayeredPair as g,
|
|
5994
6240
|
formatChangeset as h,
|
|
@@ -5998,17 +6244,17 @@ export {
|
|
|
5998
6244
|
REQUEST_KINDS as l,
|
|
5999
6245
|
mergeReleases as m,
|
|
6000
6246
|
parseExplorationBody as n,
|
|
6001
|
-
|
|
6247
|
+
EXPLORATION_PREFIX as o,
|
|
6002
6248
|
packageDirCandidates as p,
|
|
6003
|
-
|
|
6249
|
+
explorationSlug as q,
|
|
6004
6250
|
releasesFromTags as r,
|
|
6005
6251
|
sanitizeDraftLine as s,
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
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
|
|
6013
6259
|
};
|
|
6014
|
-
//# sourceMappingURL=repo-source-
|
|
6260
|
+
//# sourceMappingURL=repo-source-CEBBBtm1.js.map
|