@bridge-ds/core 0.18.0 → 0.20.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 +21 -0
- package/dist/audit/index.js +3 -3
- package/dist/audit/report.d.ts +17 -2
- package/dist/branch-actions.d.ts +7 -1
- package/dist/build-requests-github.d.ts +25 -0
- package/dist/build-requests.d.ts +403 -0
- package/dist/changelog-model.d.ts +102 -0
- package/dist/component-spec.d.ts +3 -1
- package/dist/config.d.ts +26 -0
- package/dist/css-color.d.ts +25 -0
- package/dist/docs-proposal.d.ts +1 -0
- package/dist/explorations.d.ts +1 -0
- package/dist/fidelity-references.d.ts +12 -0
- package/dist/fidelity.d.ts +12 -1
- package/dist/figma-fix-loop.d.ts +153 -0
- package/dist/figma-fix-run.d.ts +103 -0
- package/dist/figma-fixes.d.ts +169 -0
- package/dist/figma-push-github.d.ts +7 -2
- package/dist/figma-push.d.ts +15 -2
- package/dist/gap-case.d.ts +143 -0
- package/dist/github-client.d.ts +31 -0
- package/dist/id-map.d.ts +10 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +3996 -818
- package/dist/index.js.map +1 -1
- package/dist/layered-review.d.ts +2 -0
- package/dist/measure-location.d.ts +42 -0
- package/dist/measure-status.d.ts +127 -0
- package/dist/measure-workflow.d.ts +48 -0
- package/dist/measured-only.d.ts +30 -0
- package/dist/measured-spec.d.ts +349 -0
- package/dist/measured-tokens.d.ts +25 -0
- package/dist/motion-docs.d.ts +51 -0
- package/dist/node/audit.d.ts +11 -3
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1640 -24
- package/dist/node/index.js.map +1 -1
- package/dist/node/measure-script.d.ts +1 -0
- package/dist/node/measure.d.ts +212 -0
- package/dist/node/spec-css.d.ts +9 -1
- package/dist/node/tokens-script.d.ts +3 -0
- package/dist/pr-composer.d.ts +5 -0
- package/dist/previews.d.ts +4 -1
- package/dist/{prod-branch-DenQkr_e.js → prod-branch-CJZwMfVD.js} +223 -183
- package/dist/prod-branch-CJZwMfVD.js.map +1 -0
- package/dist/prod-branch.d.ts +1 -1
- package/dist/repo-model.d.ts +7 -1
- package/dist/{repo-source-CRfhLO87.js → repo-source-CshjjC7x.js} +1485 -629
- package/dist/repo-source-CshjjC7x.js.map +1 -0
- package/dist/repo-source-github.d.ts +8 -0
- package/dist/repo-source.d.ts +6 -0
- package/dist/{report-DVEmUr0p.js → report-CE-jHAuw.js} +17 -6
- package/dist/{report-DVEmUr0p.js.map → report-CE-jHAuw.js.map} +1 -1
- package/dist/requester.d.ts +14 -0
- package/dist/requests.d.ts +2 -0
- package/dist/review-gate.d.ts +189 -2
- package/dist/risk.d.ts +12 -0
- package/dist/share-branch.d.ts +1 -1
- package/dist/snapshot.d.ts +34 -0
- package/dist/suggestion-review.d.ts +45 -2
- package/dist/suggestions-github.d.ts +33 -1
- package/dist/suggestions.d.ts +6 -0
- package/dist/tier1-publish.d.ts +48 -0
- package/dist/token-proposal.d.ts +7 -0
- package/dist/try-in-product.d.ts +9 -0
- package/package.json +1 -1
- package/dist/prod-branch-DenQkr_e.js.map +0 -1
- package/dist/repo-source-CRfhLO87.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cr as tokenBump, aW as changeKind, L as LayeredTokenSet, aO as aliasOf, c4 as rgbaToDtcg, bu as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bx as inheritedTypeAt, u as upsertToken, H as readTokenAt, b0 as comboKey, c3 as renderedColor, b8 as contrastRatio, z as rgbaToHex, U as classifyRisk, bh as figmaPushNextLine, W as RISK_LABELS, bi as figmaPushTierLabel, V as CONFIG_FILE, N as loadConfig, ai as ConfigError, $ as DEFAULT_TOKEN_PATHS, ah as globToRegExp, af as slugify, Q as validateLayered, bP as parseJson, c8 as serializeJson, bU as plainEqual, cq as toPlain, br as getEntry, bp as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a2 as readPreviewComment, aV as canShip, cz as whoCanShipSentence, ct as unreadableShipTeams, cb as soloApprover, aS as approverLists, O as tokensPath, a7 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-CJZwMfVD.js";
|
|
2
2
|
function changesetBump(changes) {
|
|
3
3
|
return tokenBump(changes) ?? "patch";
|
|
4
4
|
}
|
|
@@ -177,9 +177,9 @@ function checkLayeredPair(set, pair, combo) {
|
|
|
177
177
|
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
178
178
|
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
179
179
|
}
|
|
180
|
-
function runLayeredContrast(set,
|
|
180
|
+
function runLayeredContrast(set, pairs2, modes) {
|
|
181
181
|
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
182
|
-
return combos.flatMap((c) =>
|
|
182
|
+
return combos.flatMap((c) => pairs2.map((p) => checkLayeredPair(set, p, c)));
|
|
183
183
|
}
|
|
184
184
|
function contrastDelta(before, after) {
|
|
185
185
|
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
@@ -295,14 +295,15 @@ _${deltas.length - limit} more pair(s) not shown._` : "";
|
|
|
295
295
|
}
|
|
296
296
|
function composeProposalBody(args) {
|
|
297
297
|
const t = args.tier.tier;
|
|
298
|
-
const
|
|
298
|
+
const figma = Boolean(args.fromFigma);
|
|
299
|
+
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${figma && n === t ? figmaPushTierLabel(n, args.changes) : RISK_LABELS[n]}`;
|
|
299
300
|
const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
|
|
300
301
|
return [
|
|
301
302
|
"## What changed and why",
|
|
302
303
|
"",
|
|
303
304
|
args.summary.trim(),
|
|
304
305
|
"",
|
|
305
|
-
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
306
|
+
figma ? `Pushed from Figma through ${args.tool ?? "the Bridge Figma plugin"}${args.author ? ` by ${args.author}` : ""}. ${figmaPushNextLine(args.changes)}` : `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
306
307
|
"",
|
|
307
308
|
"## Risk tier",
|
|
308
309
|
"",
|
|
@@ -332,8 +333,8 @@ function composeProposalBody(args) {
|
|
|
332
333
|
"",
|
|
333
334
|
"## Authored by",
|
|
334
335
|
"",
|
|
335
|
-
|
|
336
|
-
"
|
|
336
|
+
`- [${figma ? "x" : " "}] Human`,
|
|
337
|
+
`- [${figma ? " " : "x"}] AI agent (Claude), reviewed by a human before merge`,
|
|
337
338
|
"",
|
|
338
339
|
"## Design review checklist",
|
|
339
340
|
"",
|
|
@@ -368,6 +369,7 @@ class GitHubError extends Error {
|
|
|
368
369
|
this.name = "GitHubError";
|
|
369
370
|
this.auth = info.auth;
|
|
370
371
|
this.permission = info.permission;
|
|
372
|
+
this.resetsAt = info.resetsAt;
|
|
371
373
|
}
|
|
372
374
|
status;
|
|
373
375
|
detail;
|
|
@@ -375,6 +377,8 @@ class GitHubError extends Error {
|
|
|
375
377
|
auth;
|
|
376
378
|
/** With auth "missing-permission": the permission the call needed, when known. */
|
|
377
379
|
permission;
|
|
380
|
+
/** For a rate limit (status 429): when the limit resets, when known. */
|
|
381
|
+
resetsAt;
|
|
378
382
|
}
|
|
379
383
|
const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
|
|
380
384
|
const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
|
|
@@ -656,7 +660,7 @@ class GitHubClient {
|
|
|
656
660
|
return copy(cached.body);
|
|
657
661
|
}
|
|
658
662
|
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
659
|
-
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
663
|
+
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail, { resetsAt });
|
|
660
664
|
hourlyLimitErrors.add(limited);
|
|
661
665
|
throw limited;
|
|
662
666
|
}
|
|
@@ -693,6 +697,31 @@ class GitHubClient {
|
|
|
693
697
|
);
|
|
694
698
|
return { fullName: r.full_name, defaultBranch: r.default_branch, private: r.private, permissions: r.permissions };
|
|
695
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* What the token may write here, found without writing anything: each probe is invalid on purpose (a zero sha, a
|
|
702
|
+
* missing head branch, an empty title), so GitHub answers 422 when the token has the permission and 403 or 404 when
|
|
703
|
+
* it does not. repo.permissions.push says what the user may do, not what a fine-grained token may do.
|
|
704
|
+
*/
|
|
705
|
+
async writeAccess(base) {
|
|
706
|
+
this.assertToken("Checking the token");
|
|
707
|
+
const probe = async (path, body) => {
|
|
708
|
+
try {
|
|
709
|
+
await this.request("POST", `${this.repoPath}${path}`, body);
|
|
710
|
+
return true;
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (!(err instanceof GitHubError)) throw err;
|
|
713
|
+
if (err.status === 422) return true;
|
|
714
|
+
if (err.auth === "missing-permission" || err.status === 404 || err.status === 410) return false;
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const [contents, pullRequests, issues] = await Promise.all([
|
|
719
|
+
probe("/git/refs", { ref: "refs/heads/bridge-permission-probe", sha: "0".repeat(40) }),
|
|
720
|
+
probe("/pulls", { title: "Bridge permission probe", head: "bridge-permission-probe-missing-branch", base }),
|
|
721
|
+
probe("/issues", { title: "" })
|
|
722
|
+
]);
|
|
723
|
+
return { contents, pullRequests, issues };
|
|
724
|
+
}
|
|
696
725
|
/** Commit sha a branch points to, or undefined when the branch does not exist. */
|
|
697
726
|
async getBranchSha(branch) {
|
|
698
727
|
try {
|
|
@@ -703,6 +732,14 @@ class GitHubClient {
|
|
|
703
732
|
throw err;
|
|
704
733
|
}
|
|
705
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Whether a branch exists, through the matching-refs list: it answers 200 with an empty list when the branch is
|
|
737
|
+
* missing, so asking never leaves a 404 in the browser console (an optional branch like bridge-data).
|
|
738
|
+
*/
|
|
739
|
+
async hasBranch(branch) {
|
|
740
|
+
const refs = await this.request("GET", `${this.repoPath}/git/matching-refs/heads/${encodeRef(branch)}`);
|
|
741
|
+
return Array.isArray(refs) && refs.some((r) => r.ref === `refs/heads/${branch}`);
|
|
742
|
+
}
|
|
706
743
|
async getCommitTree(commitSha) {
|
|
707
744
|
const r = await this.request("GET", `${this.repoPath}/git/commits/${commitSha}`);
|
|
708
745
|
return r.tree.sha;
|
|
@@ -989,6 +1026,7 @@ class GitHubClient {
|
|
|
989
1026
|
const q = ref ? `?ref=${encodeURIComponent(ref)}` : "";
|
|
990
1027
|
const r = await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}${q}`);
|
|
991
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 });
|
|
992
1030
|
if (r.encoding === "base64" && r.content !== void 0) return base64ToUtf8(r.content);
|
|
993
1031
|
return r.content;
|
|
994
1032
|
} catch (err) {
|
|
@@ -1208,7 +1246,7 @@ class GitHubClient {
|
|
|
1208
1246
|
}
|
|
1209
1247
|
async getPullRequest(n) {
|
|
1210
1248
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1211
|
-
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login) };
|
|
1249
|
+
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login), autoMerge: Boolean(r.auto_merge) };
|
|
1212
1250
|
}
|
|
1213
1251
|
async listPullRequestFiles(n) {
|
|
1214
1252
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
|
|
@@ -1219,7 +1257,7 @@ class GitHubClient {
|
|
|
1219
1257
|
}
|
|
1220
1258
|
async listReviews(n) {
|
|
1221
1259
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/reviews`);
|
|
1222
|
-
return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {} }));
|
|
1260
|
+
return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {}, ...x.commit_id ? { commitId: x.commit_id } : {} }));
|
|
1223
1261
|
}
|
|
1224
1262
|
/** Submit a review. `COMMENT` works for any collaborator; approving your own PR is refused by GitHub. */
|
|
1225
1263
|
async createReview(n, input) {
|
|
@@ -1413,7 +1451,10 @@ function toSummary(r) {
|
|
|
1413
1451
|
labels: (r.labels ?? []).map((l) => l.name),
|
|
1414
1452
|
url: r.html_url,
|
|
1415
1453
|
updatedAt: r.updated_at,
|
|
1416
|
-
...typeof r.body === "string" ? { body: r.body } : {}
|
|
1454
|
+
...typeof r.body === "string" ? { body: r.body } : {},
|
|
1455
|
+
...r.created_at ? { createdAt: r.created_at } : {},
|
|
1456
|
+
...r.merged_at ? { mergedAt: r.merged_at } : {},
|
|
1457
|
+
...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {}
|
|
1417
1458
|
};
|
|
1418
1459
|
}
|
|
1419
1460
|
function friendlyMessage(status, detail, info = {}) {
|
|
@@ -1490,9 +1531,37 @@ function toIssue(r) {
|
|
|
1490
1531
|
labels: r.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
1491
1532
|
author: r.user?.login ?? "",
|
|
1492
1533
|
createdAt: r.created_at,
|
|
1493
|
-
updatedAt: r.updated_at
|
|
1534
|
+
updatedAt: r.updated_at,
|
|
1535
|
+
...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {},
|
|
1536
|
+
...r.closed_at ? { closedAt: r.closed_at } : {}
|
|
1494
1537
|
};
|
|
1495
1538
|
}
|
|
1539
|
+
const FORMER_MEMBER = "A former member";
|
|
1540
|
+
const requesterFirstName = (name) => name.trim() === FORMER_MEMBER ? FORMER_MEMBER : name.trim().split(/\s+/)[0] ?? name;
|
|
1541
|
+
const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
1542
|
+
const LINE = /(?:^|\n)[ \t>*_-]*(?:<sub>)?\s*Requested (?:from [^\n<]{1,40}? )?by ([^\n<]{1,120}?)(?:\s+with Bridge)?\.?\s*(?:<\/sub>)?[ \t]*(?=\n|$)/i;
|
|
1543
|
+
const SUGGESTED = new RegExp(LINE.source.replace("Requested (?:from", "Suggested (?:from"), "i");
|
|
1544
|
+
function requesterOf(body) {
|
|
1545
|
+
const m = LINE.exec(body ?? "") ?? SUGGESTED.exec(body ?? "");
|
|
1546
|
+
if (!m) return null;
|
|
1547
|
+
const who = m[1].trim().replace(/\.$/, "").replace(/\s+through Bridge$/i, "").replace(/\s*\([^()\s@]+@[^()\s]+\)$/, "");
|
|
1548
|
+
const both = /^(.+?)\s*\(@([^)\s]+)\)$/.exec(who);
|
|
1549
|
+
if (both) return LOGIN.test(both[2]) ? { name: both[1].trim(), login: both[2] } : null;
|
|
1550
|
+
if (who.startsWith("@")) return LOGIN.test(who.slice(1)) ? { name: null, login: who.slice(1) } : null;
|
|
1551
|
+
return /^[\p{L}][\p{L}'’. -]{0,60}$/u.test(who) && who.split(/\s+/).length <= 5 ? { name: who, login: null } : null;
|
|
1552
|
+
}
|
|
1553
|
+
function requesterLine(r) {
|
|
1554
|
+
const name = r.name?.trim();
|
|
1555
|
+
const login = r.login?.replace(/^@/, "").trim();
|
|
1556
|
+
if (name && login) return `Requested by ${name} (@${login}).`;
|
|
1557
|
+
if (login) return `Requested by @${login}.`;
|
|
1558
|
+
if (name) return `Requested by ${name}.`;
|
|
1559
|
+
throw new Error("A requester needs a name or a login.");
|
|
1560
|
+
}
|
|
1561
|
+
function requesterName(body) {
|
|
1562
|
+
const r = requesterOf(body);
|
|
1563
|
+
return r ? r.name ?? r.login : null;
|
|
1564
|
+
}
|
|
1496
1565
|
const BULK_LIMIT = 60;
|
|
1497
1566
|
const UNRELEASED = "Unreleased";
|
|
1498
1567
|
const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
@@ -1557,7 +1626,9 @@ function buildHistoryEntry(input) {
|
|
|
1557
1626
|
sha: commit.sha,
|
|
1558
1627
|
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
1559
1628
|
date: commit.date,
|
|
1560
|
-
|
|
1629
|
+
// Who asked, as Bridge recorded it in the pull request ("Requested by Priya Shah ... through Bridge."), before the
|
|
1630
|
+
// account that opened it (the Bridge app for an email member, an agent): History names the person.
|
|
1631
|
+
author: requesterName(commit.pr?.body) ?? commit.pr?.author ?? commit.author,
|
|
1561
1632
|
title,
|
|
1562
1633
|
why,
|
|
1563
1634
|
...pr ? { pr } : {},
|
|
@@ -1910,9 +1981,9 @@ function finishPlan(p) {
|
|
|
1910
1981
|
if (!changes.length) return nothing("The edits leave every token as it is now.");
|
|
1911
1982
|
const prev = new Set(validateLayered(current).map(issueKey));
|
|
1912
1983
|
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
|
|
1913
|
-
const
|
|
1984
|
+
const pairs2 = p.pairs ?? [];
|
|
1914
1985
|
const modes = config.contrast?.modes;
|
|
1915
|
-
const contrast =
|
|
1986
|
+
const contrast = pairs2.length ? contrastDelta(runLayeredContrast(current, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
|
|
1916
1987
|
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
1917
1988
|
const tier = classifyLayered(changes, [], config);
|
|
1918
1989
|
const impact = layeredImpact(current, after, changes);
|
|
@@ -2177,11 +2248,11 @@ function propShape(s) {
|
|
|
2177
2248
|
}
|
|
2178
2249
|
const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
|
|
2179
2250
|
function shortVariant(key) {
|
|
2180
|
-
const
|
|
2251
|
+
const pairs2 = key.split(", ").map((p) => {
|
|
2181
2252
|
const at = p.indexOf("=");
|
|
2182
2253
|
return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
|
|
2183
2254
|
});
|
|
2184
|
-
const kept =
|
|
2255
|
+
const kept = pairs2.filter((p) => p.v && !/^default$/i.test(p.v));
|
|
2185
2256
|
if (!kept.length) return key;
|
|
2186
2257
|
const rank = (k) => {
|
|
2187
2258
|
const i = VARIANT_FIRST.indexOf(k.toLowerCase());
|
|
@@ -2222,6 +2293,14 @@ function firstChange(component, kind, c, all) {
|
|
|
2222
2293
|
}
|
|
2223
2294
|
case "new-variant": {
|
|
2224
2295
|
if (!c) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
|
|
2296
|
+
if (c.field === "Variants") {
|
|
2297
|
+
const gone = (c.before ?? "").split("; ").filter(Boolean);
|
|
2298
|
+
const born = (c.after ?? "").split("; ").filter(Boolean);
|
|
2299
|
+
if (gone.length === 1 && !born.length) return { title: `Remove the ${shortVariant(gone[0])} variant`, summary: `${component} no longer has the ${shortVariant(gone[0])} variant (${gone[0]}).` };
|
|
2300
|
+
if (born.length === 1 && !gone.length) return { title: `Add the ${shortVariant(born[0])} variant`, summary: `${component} gets a new variant, ${shortVariant(born[0])} (${born[0]}).` };
|
|
2301
|
+
const parts = [born.length ? `gets ${plural(born.length, "new variant")}: ${andList(born.map(shortVariant))}` : "", gone.length ? `no longer has ${andList(gone.map(shortVariant))}` : ""].filter(Boolean);
|
|
2302
|
+
return { title: `Update the ${component} variants`, summary: `${component} ${parts.join(", and ")}.` };
|
|
2303
|
+
}
|
|
2225
2304
|
const axis = c.field.replace(/^Variant /, "");
|
|
2226
2305
|
const before = values(c.before);
|
|
2227
2306
|
const after = values(c.after);
|
|
@@ -2250,7 +2329,7 @@ function firstChange(component, kind, c, all) {
|
|
|
2250
2329
|
if (!c) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
|
|
2251
2330
|
const m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
|
|
2252
2331
|
const part = lower(m[1]);
|
|
2253
|
-
const where2 = m[2] ? ` in ${m[2]}` : "";
|
|
2332
|
+
const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
|
|
2254
2333
|
const after = c.after && c.after !== "none" ? c.after : "";
|
|
2255
2334
|
const before = c.before && c.before !== "none" ? c.before : "";
|
|
2256
2335
|
if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where2} no longer uses ${before || "a token"}.` };
|
|
@@ -2300,6 +2379,11 @@ function sanitizeDraftLine(raw) {
|
|
|
2300
2379
|
}
|
|
2301
2380
|
function changePhrase(c) {
|
|
2302
2381
|
let m;
|
|
2382
|
+
if (c.field === "Variants") {
|
|
2383
|
+
const gone = (c.before ?? "").split("; ").filter(Boolean).map(shortVariant);
|
|
2384
|
+
const born = (c.after ?? "").split("; ").filter(Boolean).map(shortVariant);
|
|
2385
|
+
return [born.length ? `New variant${born.length === 1 ? "" : "s"} ${andList(born)}` : "", gone.length ? `${andList(gone)} variant${gone.length === 1 ? "" : "s"} removed` : ""].filter(Boolean).join("; ");
|
|
2386
|
+
}
|
|
2303
2387
|
if (m = /^Variant (.+)$/.exec(c.field)) {
|
|
2304
2388
|
const axis = m[1];
|
|
2305
2389
|
const before2 = values(c.before);
|
|
@@ -2325,7 +2409,7 @@ function changePhrase(c) {
|
|
|
2325
2409
|
const layer = parseStructureChange(c);
|
|
2326
2410
|
if (layer) return structurePhrase(layer);
|
|
2327
2411
|
m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
|
|
2328
|
-
const where2 = m[2] ? ` in ${m[2]}` : "";
|
|
2412
|
+
const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
|
|
2329
2413
|
const before = c.before && c.before !== "none" ? c.before : "";
|
|
2330
2414
|
const after = c.after && c.after !== "none" ? c.after : "";
|
|
2331
2415
|
if (!after) return `${m[1]} no longer uses ${before || "a token"}${where2}`;
|
|
@@ -2403,10 +2487,10 @@ const EXPLORATION_NOTES_DIR = ".bridge/explorations";
|
|
|
2403
2487
|
const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
|
|
2404
2488
|
const EXPLORATION_CHECK = "Exploration not merged as is";
|
|
2405
2489
|
const EXPLORATION_USE_CASES = [
|
|
2406
|
-
{ value: "user-test", label: "Prototype for a user test", hint: "A coded prototype
|
|
2407
|
-
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone
|
|
2408
|
-
{ value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a
|
|
2409
|
-
{ value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server
|
|
2490
|
+
{ value: "user-test", label: "Prototype for a user test", hint: "A coded prototype in a test version of a product, with the real components and tokens." },
|
|
2491
|
+
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
|
|
2492
|
+
{ value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
|
|
2493
|
+
{ 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." },
|
|
2410
2494
|
{ value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
|
|
2411
2495
|
];
|
|
2412
2496
|
const EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
|
|
@@ -2433,8 +2517,8 @@ function explorationProblems(e) {
|
|
|
2433
2517
|
const name = e.name.trim();
|
|
2434
2518
|
const slug = e.slug?.trim() || explorationSlug(name);
|
|
2435
2519
|
if (!name) out.push('Name the exploration, for example "Denser data tables".');
|
|
2436
|
-
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the
|
|
2437
|
-
else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the
|
|
2520
|
+
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
|
|
2521
|
+
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.");
|
|
2438
2522
|
if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
|
|
2439
2523
|
return out;
|
|
2440
2524
|
}
|
|
@@ -2565,7 +2649,8 @@ function explorationShareMessage(input) {
|
|
|
2565
2649
|
const name = input.name.trim().replace(/"/g, "'");
|
|
2566
2650
|
const head = input.command ? `Exploration "${name}" (${input.branch}): try it in a staging branch of your product with: ${input.command}.` : `Exploration "${name}" (${input.branch}): its test build is being prepared, the install command for a staging branch of your product will be on the pull request in a few minutes.`;
|
|
2567
2651
|
const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
|
|
2568
|
-
|
|
2652
|
+
const local = !previews.length && input.local ? `Try it locally: ${input.local}, then start Storybook or the app with its dev command.` : "";
|
|
2653
|
+
return [head, previews.length ? `Previews: ${previews.join(", ")}.` : local, `Pull request: ${input.url}.`].filter(Boolean).join(" ");
|
|
2569
2654
|
}
|
|
2570
2655
|
function explorationRules(branch) {
|
|
2571
2656
|
return [
|
|
@@ -2800,6 +2885,7 @@ const SPEC_FIELDS = [
|
|
|
2800
2885
|
"minWidth",
|
|
2801
2886
|
"minHeight",
|
|
2802
2887
|
"opacity",
|
|
2888
|
+
"shadow",
|
|
2803
2889
|
"textFill",
|
|
2804
2890
|
"fontFamily",
|
|
2805
2891
|
"fontSize",
|
|
@@ -2996,139 +3082,841 @@ function figmaCell(entry) {
|
|
|
2996
3082
|
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
2997
3083
|
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
2998
3084
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
if (m) found.push(Number(m[1]));
|
|
3004
|
-
}
|
|
3005
|
-
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
3006
|
-
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
3007
|
-
return found.length ? Math.max(...found) : null;
|
|
3008
|
-
}
|
|
3009
|
-
function pullState(p) {
|
|
3010
|
-
if (p.merged_at || p.merged) return "merged";
|
|
3011
|
-
if (p.state && p.state !== "open") return "closed";
|
|
3012
|
-
if (p.draft) return "draft";
|
|
3013
|
-
return "open";
|
|
3014
|
-
}
|
|
3015
|
-
const PULL_STATE_PILL = {
|
|
3016
|
-
merged: { label: "Merged", cls: "" },
|
|
3017
|
-
closed: { label: "Closed", cls: "" },
|
|
3018
|
-
draft: { label: "Draft", cls: "" },
|
|
3019
|
-
open: { label: "Open", cls: "" }
|
|
3085
|
+
const px = (v) => {
|
|
3086
|
+
if (!v) return void 0;
|
|
3087
|
+
const m = /^(-?[\d.]+)px$/.exec(v.trim());
|
|
3088
|
+
return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
|
|
3020
3089
|
};
|
|
3021
|
-
function
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3090
|
+
function evalCalc(expr, resolve) {
|
|
3091
|
+
const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
|
|
3092
|
+
if (!body) return void 0;
|
|
3093
|
+
let missing = false;
|
|
3094
|
+
const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
|
|
3095
|
+
const v2 = resolve(path);
|
|
3096
|
+
const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
|
|
3097
|
+
if (n === void 0 || Number.isNaN(n)) missing = true;
|
|
3098
|
+
return String(n);
|
|
3025
3099
|
});
|
|
3100
|
+
if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
|
|
3101
|
+
const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
|
|
3102
|
+
let i = 0;
|
|
3103
|
+
const factor = () => {
|
|
3104
|
+
const t = tokens[i++];
|
|
3105
|
+
if (t === "(") {
|
|
3106
|
+
const v2 = sum();
|
|
3107
|
+
i++;
|
|
3108
|
+
return v2;
|
|
3109
|
+
}
|
|
3110
|
+
if (t === "-") return -factor();
|
|
3111
|
+
return Number(t);
|
|
3112
|
+
};
|
|
3113
|
+
const product = () => {
|
|
3114
|
+
let v2 = factor();
|
|
3115
|
+
while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
|
|
3116
|
+
return v2;
|
|
3117
|
+
};
|
|
3118
|
+
const sum = () => {
|
|
3119
|
+
let v2 = product();
|
|
3120
|
+
while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
|
|
3121
|
+
return v2;
|
|
3122
|
+
};
|
|
3123
|
+
const v = sum();
|
|
3124
|
+
return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
|
|
3125
|
+
}
|
|
3126
|
+
const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
3127
|
+
function normColor(v) {
|
|
3128
|
+
const t = v.trim().toLowerCase();
|
|
3129
|
+
const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
|
|
3130
|
+
if (hex2) {
|
|
3131
|
+
const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
|
|
3132
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
3133
|
+
const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
|
|
3134
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
|
|
3135
|
+
}
|
|
3136
|
+
const m = /^rgba?\(([^)]*)\)$/.exec(t);
|
|
3137
|
+
if (m) {
|
|
3138
|
+
const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
|
|
3139
|
+
const [r, g, b] = parts;
|
|
3140
|
+
const a = parts[3] ?? 1;
|
|
3141
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
|
|
3142
|
+
}
|
|
3143
|
+
return t.replace(/\s+/g, "");
|
|
3144
|
+
}
|
|
3145
|
+
const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3146
|
+
const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
|
|
3147
|
+
function withoutAddedWrappers(parts, measured) {
|
|
3148
|
+
const real = new Set(measured.parts.map((m) => m.id));
|
|
3149
|
+
const added = /* @__PURE__ */ new Map();
|
|
3150
|
+
for (const p of parts) {
|
|
3151
|
+
if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
|
|
3152
|
+
const kids = parts.filter((c) => c.parent === p.id);
|
|
3153
|
+
if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
|
|
3154
|
+
}
|
|
3155
|
+
if (!added.size) return parts;
|
|
3156
|
+
const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
|
|
3157
|
+
return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
|
|
3158
|
+
}
|
|
3159
|
+
function pairParts(staticParts, measured, o = {}) {
|
|
3160
|
+
const pairs2 = /* @__PURE__ */ new Map();
|
|
3161
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3162
|
+
const mParts = new Map(measured.parts.map((p) => [p.id, p]));
|
|
3163
|
+
const sParts = new Map(staticParts.map((p) => [p.id, p]));
|
|
3164
|
+
const take = (s, m) => {
|
|
3165
|
+
pairs2.set(s, m);
|
|
3166
|
+
taken.add(m);
|
|
3167
|
+
};
|
|
3168
|
+
const ancestors = (id) => {
|
|
3169
|
+
const out = [];
|
|
3170
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
3171
|
+
for (let p = mParts.get(id)?.parent; p && !seen.has(p); p = mParts.get(p)?.parent) {
|
|
3172
|
+
seen.add(p);
|
|
3173
|
+
out.push(p);
|
|
3174
|
+
}
|
|
3175
|
+
return out;
|
|
3176
|
+
};
|
|
3177
|
+
const pairedAncestor = (p) => {
|
|
3178
|
+
const seen = /* @__PURE__ */ new Set([p.id]);
|
|
3179
|
+
for (let a = p.parent; a && !seen.has(a); a = sParts.get(a)?.parent) {
|
|
3180
|
+
if (pairs2.has(a)) return pairs2.get(a);
|
|
3181
|
+
seen.add(a);
|
|
3182
|
+
}
|
|
3183
|
+
return void 0;
|
|
3184
|
+
};
|
|
3185
|
+
const best = (p, cands) => {
|
|
3186
|
+
if (!cands.length) return void 0;
|
|
3187
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3188
|
+
if (parent) {
|
|
3189
|
+
const direct = cands.find((c) => c.parent === parent);
|
|
3190
|
+
if (direct) return direct;
|
|
3191
|
+
}
|
|
3192
|
+
const anc = pairedAncestor(p);
|
|
3193
|
+
if (anc) {
|
|
3194
|
+
const under2 = cands.find((c) => ancestors(c.id).includes(anc));
|
|
3195
|
+
if (under2) return under2;
|
|
3196
|
+
}
|
|
3197
|
+
return cands[0];
|
|
3198
|
+
};
|
|
3199
|
+
const root = staticParts.find((p) => !p.parent);
|
|
3200
|
+
if (root) take(root.id, "root");
|
|
3201
|
+
const textOf = (p) => {
|
|
3202
|
+
if (p.characters) return normText(p.characters);
|
|
3203
|
+
const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
|
|
3204
|
+
return typeof prop?.default === "string" ? normText(prop.default) : "";
|
|
3205
|
+
};
|
|
3206
|
+
const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
|
|
3207
|
+
const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
|
|
3208
|
+
const shared = (p, m) => {
|
|
3209
|
+
const mine = new Set(classesOf(p));
|
|
3210
|
+
return mClasses(m).filter((c) => mine.has(c)).length;
|
|
3211
|
+
};
|
|
3212
|
+
const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
|
|
3213
|
+
const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
|
|
3214
|
+
for (const p of staticParts) {
|
|
3215
|
+
if (p.kind !== "text" || pairs2.has(p.id)) continue;
|
|
3216
|
+
const t = textOf(p);
|
|
3217
|
+
if (!t) continue;
|
|
3218
|
+
const hit = best(p, measured.parts.filter((m) => m.kind === "text" && !taken.has(m.id) && normText(m.characters) === t && !(classesOf(p).length && mClasses(m).length && !shared(p, m))));
|
|
3219
|
+
if (hit) take(p.id, hit.id);
|
|
3220
|
+
}
|
|
3221
|
+
const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
|
|
3222
|
+
for (const p of staticParts) {
|
|
3223
|
+
if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
|
|
3224
|
+
const sp = sParts.get(p.parent);
|
|
3225
|
+
if (!sp || sp.kind !== "frame" || !sp.parent) continue;
|
|
3226
|
+
const cls = classOf(sp);
|
|
3227
|
+
const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
|
|
3228
|
+
if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
|
|
3229
|
+
}
|
|
3230
|
+
for (const p of staticParts) {
|
|
3231
|
+
if (pairs2.has(p.id) || !classesOf(p).length) continue;
|
|
3232
|
+
if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
|
|
3233
|
+
const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
|
|
3234
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3235
|
+
const under2 = parent ? all.filter((m) => m.parent === parent) : [];
|
|
3236
|
+
const cands = under2.length ? under2 : all;
|
|
3237
|
+
const most = Math.max(0, ...cands.map((m) => shared(p, m)));
|
|
3238
|
+
const hit = best(p, cands.filter((m) => shared(p, m) === most));
|
|
3239
|
+
if (hit) take(p.id, hit.id);
|
|
3240
|
+
}
|
|
3241
|
+
for (const p of staticParts) {
|
|
3242
|
+
if (pairs2.has(p.id)) continue;
|
|
3243
|
+
if (p.parent && !pairs2.has(p.parent)) continue;
|
|
3244
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3245
|
+
const known = p.kind === "text" && !!textOf(p);
|
|
3246
|
+
const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
|
|
3247
|
+
const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
|
|
3248
|
+
if (hit) take(p.id, hit.id);
|
|
3249
|
+
}
|
|
3250
|
+
for (const p of staticParts) {
|
|
3251
|
+
if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
|
|
3252
|
+
const mp = pairs2.get(p.parent);
|
|
3253
|
+
const t = textOf(p);
|
|
3254
|
+
if (mp && mParts.get(mp)?.kind === "text" && staticParts.filter((x) => x.parent === p.parent).length === 1 && (!t || normText(mParts.get(mp).characters) === t)) pairs2.set(p.id, mp);
|
|
3255
|
+
}
|
|
3256
|
+
return pairs2;
|
|
3257
|
+
}
|
|
3258
|
+
function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
|
|
3259
|
+
const pairs2 = pairParts(staticParts, measured, o);
|
|
3260
|
+
const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
|
|
3261
|
+
const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
|
|
3262
|
+
const near = (a, b) => {
|
|
3263
|
+
if (a === void 0 || b === void 0) return false;
|
|
3264
|
+
const na0 = num2(a);
|
|
3265
|
+
const nb0 = num2(b);
|
|
3266
|
+
if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
|
|
3267
|
+
if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
|
|
3268
|
+
const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
|
|
3269
|
+
const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
|
|
3270
|
+
return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
|
|
3271
|
+
};
|
|
3272
|
+
for (const sp of staticParts) {
|
|
3273
|
+
const mid = pairs2.get(sp.id);
|
|
3274
|
+
if (!mid) {
|
|
3275
|
+
out.unmatchedParts.push(sp.id);
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
const sv = staticValues[sp.id] ?? {};
|
|
3279
|
+
const mv = mid ? measured.values[mid] ?? {} : {};
|
|
3280
|
+
const mr = mid ? measured.raw[mid] ?? {} : {};
|
|
3281
|
+
const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
|
|
3282
|
+
for (const f of fields) {
|
|
3283
|
+
if ((f === "width" || f === "height") && sv[f] === void 0) continue;
|
|
3284
|
+
const s = sv[f];
|
|
3285
|
+
const m = mv[f] !== void 0 ? mv[f] : mr[f];
|
|
3286
|
+
if (s === void 0) {
|
|
3287
|
+
if (m !== void 0 && m !== 0) out.measuredOnly++;
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
if (m === void 0 && !(f in mr)) {
|
|
3291
|
+
if (s === 0 || s === null) continue;
|
|
3292
|
+
const r = typeof s === "string" && resolve ? resolve(s) : s;
|
|
3293
|
+
if (r === 0 || r === "0px" || r === null) continue;
|
|
3294
|
+
out.staticOnly++;
|
|
3295
|
+
out.diffs.push({ part: sp.id, field: f, static: s, same: false });
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
out.compared++;
|
|
3299
|
+
let same2 = s === m;
|
|
3300
|
+
if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
|
|
3301
|
+
if (same2 && typeof s === "string") out.sameToken++;
|
|
3302
|
+
if (!same2 && resolve) {
|
|
3303
|
+
let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
|
|
3304
|
+
const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
|
|
3305
|
+
if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
|
|
3306
|
+
same2 = near(rs, mr[f]);
|
|
3307
|
+
}
|
|
3308
|
+
if (same2) out.same++;
|
|
3309
|
+
else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
|
|
3313
|
+
const box = measured.boxes.root;
|
|
3314
|
+
if (box) out.rootSize = { static: { width: typeof root.width === "number" ? root.width : void 0, height: typeof root.height === "number" ? root.height : void 0 }, measured: { width: box.width, height: box.height } };
|
|
3315
|
+
return out;
|
|
3026
3316
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3317
|
+
function tokenVariablePath(name, prefix) {
|
|
3318
|
+
let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
|
|
3319
|
+
if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
|
|
3320
|
+
return parts.join("/");
|
|
3321
|
+
}
|
|
3322
|
+
function sharedTokenPrefix(names2) {
|
|
3323
|
+
if (names2.length < 20) return void 0;
|
|
3324
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3325
|
+
for (const n2 of names2) {
|
|
3326
|
+
const first = n2.replace(/^--/, "").split("-")[0];
|
|
3327
|
+
counts.set(first, (counts.get(first) ?? 0) + 1);
|
|
3036
3328
|
}
|
|
3329
|
+
const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
3330
|
+
return top && n / names2.length >= 0.6 ? top : void 0;
|
|
3331
|
+
}
|
|
3332
|
+
function markUsedTokens(tokens, stories) {
|
|
3333
|
+
const byName = new Map(tokens.properties.map((p) => [p.name, p]));
|
|
3037
3334
|
const seen = /* @__PURE__ */ new Set();
|
|
3038
|
-
|
|
3039
|
-
if (seen.has(
|
|
3040
|
-
seen.add(
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3335
|
+
const use = (name) => {
|
|
3336
|
+
if (seen.has(name)) return;
|
|
3337
|
+
seen.add(name);
|
|
3338
|
+
const p = byName.get(name);
|
|
3339
|
+
if (!p) return;
|
|
3340
|
+
p.used = true;
|
|
3341
|
+
for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
|
|
3342
|
+
if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
|
|
3343
|
+
};
|
|
3344
|
+
for (const s of stories)
|
|
3345
|
+
for (const spec of [s.spec, ...s.instances ?? []])
|
|
3346
|
+
for (const part of Object.values(spec?.tokens ?? {}))
|
|
3347
|
+
for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
|
|
3348
|
+
const used = tokens.properties.filter((p) => p.used).length;
|
|
3349
|
+
tokens.counts.used = used;
|
|
3350
|
+
return used;
|
|
3351
|
+
}
|
|
3352
|
+
class MeasuredSpecsError extends Error {
|
|
3353
|
+
}
|
|
3354
|
+
const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3355
|
+
function parseMeasuredSpecsFile(input) {
|
|
3356
|
+
let data = input;
|
|
3357
|
+
if (typeof input === "string") {
|
|
3358
|
+
try {
|
|
3359
|
+
data = JSON.parse(input);
|
|
3360
|
+
} catch (e) {
|
|
3361
|
+
throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
|
|
3362
|
+
}
|
|
3044
3363
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3364
|
+
if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
|
|
3365
|
+
if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
|
|
3366
|
+
if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
|
|
3367
|
+
if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
|
|
3368
|
+
data.stories.forEach((s, i) => {
|
|
3369
|
+
if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
|
|
3370
|
+
const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
|
|
3371
|
+
for (const sp of specs) {
|
|
3372
|
+
if (!isObj$1(sp) || !Array.isArray(sp.parts) || !isObj$1(sp.values)) throw new MeasuredSpecsError(`measured specs: story ${s.id} has a spec without parts and values`);
|
|
3373
|
+
for (const p of sp.parts) if (!isObj$1(p) || typeof p.id !== "string" || typeof p.kind !== "string") throw new MeasuredSpecsError(`measured specs: story ${s.id} has a part without id or kind`);
|
|
3374
|
+
}
|
|
3375
|
+
if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
|
|
3376
|
+
});
|
|
3377
|
+
if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
|
|
3378
|
+
if (data.tokens !== void 0) {
|
|
3379
|
+
const t = data.tokens;
|
|
3380
|
+
if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
|
|
3381
|
+
for (const p of t.properties) if (!isObj$1(p) || typeof p.name !== "string" || typeof p.path !== "string" || p.tier !== "primitive" && p.tier !== "semantic") throw new MeasuredSpecsError("measured specs: a token has no name, path or tier");
|
|
3382
|
+
}
|
|
3383
|
+
return data;
|
|
3384
|
+
}
|
|
3385
|
+
const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
|
|
3386
|
+
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
3387
|
+
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
3388
|
+
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
3389
|
+
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
3390
|
+
];
|
|
3391
|
+
const D50_TO_D65 = [
|
|
3392
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
3393
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
3394
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
3395
|
+
];
|
|
3396
|
+
const LINEAR_P3_TO_XYZ_D65 = [
|
|
3397
|
+
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
3398
|
+
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
3399
|
+
[0, 0.04511338185890264, 1.043944368900976]
|
|
3400
|
+
];
|
|
3401
|
+
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
3402
|
+
const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
|
|
3403
|
+
const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
|
|
3404
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
3405
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
3406
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
3407
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
3408
|
+
return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
3409
|
+
}
|
|
3410
|
+
function labToXyzD50(L, a, b) {
|
|
3411
|
+
const e = 216 / 24389;
|
|
3412
|
+
const k = 24389 / 27;
|
|
3413
|
+
const fy = (L + 16) / 116;
|
|
3414
|
+
const fx = a / 500 + fy;
|
|
3415
|
+
const fz = fy - b / 200;
|
|
3416
|
+
const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
3417
|
+
const y = L > k * e ? fy ** 3 : L / k;
|
|
3418
|
+
const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
3419
|
+
return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
|
|
3420
|
+
}
|
|
3421
|
+
function num(s, full = 1) {
|
|
3422
|
+
if (s === void 0 || s === "none") return 0;
|
|
3423
|
+
if (s.endsWith("%")) return parseFloat(s) / 100 * full;
|
|
3424
|
+
if (/deg$/.test(s)) return parseFloat(s);
|
|
3425
|
+
if (/turn$/.test(s)) return parseFloat(s) * 360;
|
|
3426
|
+
if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
|
|
3427
|
+
return parseFloat(s);
|
|
3428
|
+
}
|
|
3429
|
+
function linearSrgb(fn, args) {
|
|
3430
|
+
switch (fn) {
|
|
3431
|
+
case "oklab":
|
|
3432
|
+
return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
|
|
3433
|
+
case "oklch": {
|
|
3434
|
+
const c = num(args[1], 0.4);
|
|
3435
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
3436
|
+
return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
|
|
3437
|
+
}
|
|
3438
|
+
case "lab":
|
|
3439
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
|
|
3440
|
+
case "lch": {
|
|
3441
|
+
const c = num(args[1], 150);
|
|
3442
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
3443
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
|
|
3444
|
+
}
|
|
3445
|
+
case "color": {
|
|
3446
|
+
const [space, ...c] = args;
|
|
3447
|
+
const v = [num(c[0]), num(c[1]), num(c[2])];
|
|
3448
|
+
if (space === "srgb") return v.map(toLinear);
|
|
3449
|
+
if (space === "srgb-linear") return v;
|
|
3450
|
+
if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
|
|
3451
|
+
if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
|
|
3452
|
+
if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
|
|
3453
|
+
return void 0;
|
|
3454
|
+
}
|
|
3056
3455
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
const
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3456
|
+
return void 0;
|
|
3457
|
+
}
|
|
3458
|
+
const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
|
|
3459
|
+
function cssColorToRgb(fn, inner) {
|
|
3460
|
+
const [main, alpha] = inner.split("/");
|
|
3461
|
+
const args = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
3462
|
+
const lin = linearSrgb(fn.toLowerCase(), args);
|
|
3463
|
+
if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
|
|
3464
|
+
const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
|
|
3465
|
+
const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
|
|
3466
|
+
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
|
|
3467
|
+
}
|
|
3468
|
+
function cssColorsToRgb(text) {
|
|
3469
|
+
if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
|
|
3470
|
+
return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
|
|
3471
|
+
}
|
|
3472
|
+
function withSrgbColors(value) {
|
|
3473
|
+
if (typeof value === "string") return cssColorsToRgb(value);
|
|
3474
|
+
if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
|
|
3475
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
|
|
3476
|
+
return value;
|
|
3477
|
+
}
|
|
3478
|
+
function parseBoxShadow(css) {
|
|
3479
|
+
if (!css || css.trim() === "none") return [];
|
|
3480
|
+
const out = [];
|
|
3481
|
+
for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
|
|
3482
|
+
const color = /rgba?\(([^)]*)\)/i.exec(one);
|
|
3483
|
+
const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
|
|
3484
|
+
const a = c[3] ?? 1;
|
|
3485
|
+
const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
|
|
3486
|
+
if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
|
|
3487
|
+
out.push({ inset: /\binset\b/i.test(one), x: lengths[0], y: lengths[1], blur: lengths[2] ?? 0, spread: lengths[3] ?? 0, color: { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a } });
|
|
3073
3488
|
}
|
|
3074
|
-
return
|
|
3489
|
+
return out;
|
|
3075
3490
|
}
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3491
|
+
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3492
|
+
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3493
|
+
const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
3494
|
+
function rawIssue(label, r, values2) {
|
|
3495
|
+
const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
|
|
3496
|
+
const todo = !matches.length ? "No variable has that value: bind it to one so it follows the library, or create the variable and send it with the variables." : matches.length === 1 ? `${matches[0]} has that value: bind it so it follows the library.` : `${and(matches)} have that value: bind one so it follows the library.`;
|
|
3497
|
+
return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
|
|
3080
3498
|
}
|
|
3081
|
-
function
|
|
3082
|
-
const
|
|
3083
|
-
const
|
|
3084
|
-
const
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3092
|
-
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3093
|
-
const tier = input.tier ?? 3;
|
|
3094
|
-
if (tier >= 2) {
|
|
3095
|
-
if (!input.approvals.design) {
|
|
3096
|
-
approvalReasons.push("It needs a design owner approval.");
|
|
3097
|
-
missing.push("design approval");
|
|
3499
|
+
function planComponentPush(d, ctx) {
|
|
3500
|
+
const issues = [...d.unknown];
|
|
3501
|
+
const reasons = [];
|
|
3502
|
+
const edits = /* @__PURE__ */ new Map();
|
|
3503
|
+
const also = /* @__PURE__ */ new Set();
|
|
3504
|
+
for (const r of d.rebinds) {
|
|
3505
|
+
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3506
|
+
if (r.raw?.length) {
|
|
3507
|
+
issues.push(rawIssue(label, r, r.raw));
|
|
3508
|
+
continue;
|
|
3098
3509
|
}
|
|
3099
|
-
if (!
|
|
3100
|
-
|
|
3101
|
-
|
|
3510
|
+
if (!r.after.length) {
|
|
3511
|
+
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and send it with the variables, then scan again." });
|
|
3512
|
+
continue;
|
|
3102
3513
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3514
|
+
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3515
|
+
if (missing.length) {
|
|
3516
|
+
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Send the variable first (Variables, above), then scan again." });
|
|
3517
|
+
continue;
|
|
3518
|
+
}
|
|
3519
|
+
if (!r.before.length) {
|
|
3520
|
+
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3524
|
+
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
const [from, to] = [r.before[0], r.after[0]];
|
|
3528
|
+
if (ctx.tierOf(from) !== "component") {
|
|
3529
|
+
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
|
|
3530
|
+
continue;
|
|
3531
|
+
}
|
|
3532
|
+
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3533
|
+
if (elsewhere.variants.length) {
|
|
3534
|
+
const v = elsewhere.variants;
|
|
3535
|
+
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3536
|
+
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
for (const c of elsewhere.components) also.add(c);
|
|
3540
|
+
const prev = edits.get(from);
|
|
3541
|
+
if (prev && prev.value !== `{${to}}`) {
|
|
3542
|
+
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
|
|
3543
|
+
edits.delete(from);
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3106
3547
|
}
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
if (
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3548
|
+
const list = [...edits.values()];
|
|
3549
|
+
if (d.needsCode || reasons.length) {
|
|
3550
|
+
return {
|
|
3551
|
+
route: "claude-draft",
|
|
3552
|
+
said: `A developer builds it with Claude from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3553
|
+
edits: list,
|
|
3554
|
+
reasons,
|
|
3555
|
+
issues,
|
|
3556
|
+
alsoChanges: [...also]
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
if (list.length) {
|
|
3560
|
+
const n = list.length;
|
|
3561
|
+
const others = [...also];
|
|
3562
|
+
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3563
|
+
const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
3564
|
+
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
3565
|
+
}
|
|
3566
|
+
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
3123
3567
|
}
|
|
3124
|
-
function
|
|
3125
|
-
const
|
|
3126
|
-
return
|
|
3127
|
-
|
|
3128
|
-
|
|
3568
|
+
function specsUsage(specs, component) {
|
|
3569
|
+
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3570
|
+
return (path, r) => {
|
|
3571
|
+
const variants = [];
|
|
3572
|
+
const components = [];
|
|
3573
|
+
for (const c of specs.components) {
|
|
3574
|
+
for (const v of c.variants) {
|
|
3575
|
+
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3576
|
+
const key = variantKeyOf(v.props);
|
|
3577
|
+
if (c === own) {
|
|
3578
|
+
if (!r.variants.includes(key)) variants.push(key);
|
|
3579
|
+
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return { variants, components };
|
|
3583
|
+
};
|
|
3129
3584
|
}
|
|
3130
|
-
function
|
|
3131
|
-
|
|
3585
|
+
function componentContrastPairs(edits, pairs2, tokens) {
|
|
3586
|
+
const out = /* @__PURE__ */ new Map();
|
|
3587
|
+
for (const e of edits) {
|
|
3588
|
+
const target = tokens.aliasOf(e.path);
|
|
3589
|
+
if (!target) continue;
|
|
3590
|
+
const group = e.path.split(".")[0];
|
|
3591
|
+
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3592
|
+
for (const p of pairs2) {
|
|
3593
|
+
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3594
|
+
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return [...out.values()];
|
|
3598
|
+
}
|
|
3599
|
+
const FROM_FIGMA_LABEL = "from-figma";
|
|
3600
|
+
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3601
|
+
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3602
|
+
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3603
|
+
const ORIGIN_MARK = "bridge-from-figma";
|
|
3604
|
+
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3605
|
+
function parseFigmaOrigin(body) {
|
|
3606
|
+
if (!body) return void 0;
|
|
3607
|
+
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3608
|
+
if (!m) return void 0;
|
|
3609
|
+
try {
|
|
3610
|
+
const o = JSON.parse(m[1]);
|
|
3611
|
+
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3612
|
+
} catch {
|
|
3613
|
+
return void 0;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function figmaOriginSection(o) {
|
|
3617
|
+
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3618
|
+
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3619
|
+
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted with Claude** from the Figma spec. A person reviews it before it ships." : `**Waiting for a developer:** this branch carries the spec only, as a request. A developer builds the code here with Claude (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3620
|
+
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the Bridge Figma plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3621
|
+
}
|
|
3622
|
+
function withFigmaOrigin(body, o) {
|
|
3623
|
+
let b = (body ?? "").trim();
|
|
3624
|
+
const at = b.indexOf("**From Figma:**");
|
|
3625
|
+
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3626
|
+
if (at >= 0 && mark > at) {
|
|
3627
|
+
const end = b.indexOf("-->", mark);
|
|
3628
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3629
|
+
}
|
|
3630
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
3631
|
+
|
|
3632
|
+
${b}` : ""}`;
|
|
3633
|
+
}
|
|
3634
|
+
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3635
|
+
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3636
|
+
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3637
|
+
function formatFigmaSpec(d, plan) {
|
|
3638
|
+
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3639
|
+
`;
|
|
3640
|
+
}
|
|
3641
|
+
function parseFigmaSpec(text) {
|
|
3642
|
+
if (!text) return void 0;
|
|
3643
|
+
try {
|
|
3644
|
+
const s = JSON.parse(text);
|
|
3645
|
+
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3646
|
+
} catch {
|
|
3647
|
+
return void 0;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
function figmaDraftBrief(d, ctx) {
|
|
3651
|
+
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
3652
|
+
const suggested = "suggestion" in ctx;
|
|
3653
|
+
const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
|
|
3654
|
+
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
3655
|
+
return [
|
|
3656
|
+
suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
|
|
3657
|
+
"",
|
|
3658
|
+
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
3659
|
+
`Kind: ${kindLabel(d.kind)}`,
|
|
3660
|
+
`Change: ${d.title}`,
|
|
3661
|
+
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
3662
|
+
"",
|
|
3663
|
+
d.summary.trim(),
|
|
3664
|
+
"",
|
|
3665
|
+
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
3666
|
+
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
3667
|
+
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
3668
|
+
...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
|
|
3669
|
+
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3670
|
+
`Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
|
|
3671
|
+
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
3672
|
+
...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
|
|
3673
|
+
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
3674
|
+
"",
|
|
3675
|
+
"Steps:",
|
|
3676
|
+
"suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
|
|
3677
|
+
`2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
|
|
3678
|
+
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
3679
|
+
"suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The pull request is then ready for review, labelled ${DRAFTED_BY_CLAUDE_LABEL}, and a person reviews it.`,
|
|
3680
|
+
"",
|
|
3681
|
+
"Rules:",
|
|
3682
|
+
"- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
|
|
3683
|
+
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
3684
|
+
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
3685
|
+
].join("\n");
|
|
3686
|
+
}
|
|
3687
|
+
function figmaDraftBody(d, plan, o) {
|
|
3688
|
+
const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3689
|
+
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
|
|
3690
|
+
return withFigmaOrigin(
|
|
3691
|
+
[
|
|
3692
|
+
`## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
3693
|
+
"",
|
|
3694
|
+
d.summary.trim(),
|
|
3695
|
+
"",
|
|
3696
|
+
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
3697
|
+
...rows,
|
|
3698
|
+
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
3699
|
+
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
3700
|
+
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3701
|
+
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
3702
|
+
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
3703
|
+
].join("\n"),
|
|
3704
|
+
o
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
const figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
3708
|
+
function figmaPushStatus(x) {
|
|
3709
|
+
const labels = x.labels ?? [];
|
|
3710
|
+
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
3711
|
+
if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
|
|
3712
|
+
if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for a developer", sentence: "A developer builds it with Claude from its spec, then a person reviews it.", rebuild: false };
|
|
3713
|
+
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted with Claude", sentence: "The code is drafted. A person reviews it before it ships.", rebuild: false };
|
|
3714
|
+
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3715
|
+
}
|
|
3716
|
+
function parseTier(labels, body) {
|
|
3717
|
+
const found = [];
|
|
3718
|
+
for (const l of labels) {
|
|
3719
|
+
const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
|
|
3720
|
+
if (m) found.push(Number(m[1]));
|
|
3721
|
+
}
|
|
3722
|
+
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
3723
|
+
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
3724
|
+
return found.length ? Math.max(...found) : null;
|
|
3725
|
+
}
|
|
3726
|
+
function claudeStage(labels) {
|
|
3727
|
+
const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
|
|
3728
|
+
if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
|
|
3729
|
+
if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
|
|
3730
|
+
return null;
|
|
3731
|
+
}
|
|
3732
|
+
const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
|
|
3733
|
+
function pullState(p) {
|
|
3734
|
+
if (p.merged_at || p.merged) return "merged";
|
|
3735
|
+
if (p.state && p.state !== "open") return "closed";
|
|
3736
|
+
if (isDraft(p)) return "draft";
|
|
3737
|
+
return "open";
|
|
3738
|
+
}
|
|
3739
|
+
const PULL_STATE_PILL = {
|
|
3740
|
+
merged: { label: "Merged", cls: "" },
|
|
3741
|
+
closed: { label: "Closed", cls: "" },
|
|
3742
|
+
draft: { label: "Draft", cls: "" },
|
|
3743
|
+
open: { label: "Open", cls: "" }
|
|
3744
|
+
};
|
|
3745
|
+
function openPulls(pulls) {
|
|
3746
|
+
return pulls.filter((p) => {
|
|
3747
|
+
const s = pullState(p);
|
|
3748
|
+
return s === "open" || s === "draft";
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
function isPlatformRefusal(s) {
|
|
3752
|
+
return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
|
|
3753
|
+
}
|
|
3754
|
+
const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
3755
|
+
function summarizeCi(runs, statuses = []) {
|
|
3756
|
+
const failed = [];
|
|
3757
|
+
const pending = [];
|
|
3758
|
+
let passed = 0;
|
|
3759
|
+
for (const r of runs) {
|
|
3760
|
+
if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
|
|
3761
|
+
else if (r.conclusion && OK.has(r.conclusion)) passed++;
|
|
3762
|
+
else failed.push({ name: r.name, url: r.html_url });
|
|
3763
|
+
}
|
|
3764
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3765
|
+
for (const s of statuses) {
|
|
3766
|
+
if (seen.has(s.context)) continue;
|
|
3767
|
+
seen.add(s.context);
|
|
3768
|
+
if (isPlatformRefusal(s)) continue;
|
|
3769
|
+
if (s.state === "success") passed++;
|
|
3770
|
+
else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
3771
|
+
else failed.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
3772
|
+
}
|
|
3773
|
+
const total = passed + failed.length + pending.length;
|
|
3774
|
+
const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
|
|
3775
|
+
return { state, total, passed, failed, pending };
|
|
3776
|
+
}
|
|
3777
|
+
const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
|
|
3778
|
+
function summarizeApprovals(reviews, owners, author, rules = {}) {
|
|
3779
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3780
|
+
const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
3781
|
+
for (const r of sorted) {
|
|
3782
|
+
const login = r.user?.login;
|
|
3783
|
+
if (!login || login === author) continue;
|
|
3784
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
|
|
3785
|
+
}
|
|
3786
|
+
const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
|
|
3787
|
+
const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
|
|
3788
|
+
const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
|
|
3789
|
+
const d = lower2(owners.design);
|
|
3790
|
+
const t = lower2(owners.tech);
|
|
3791
|
+
const a = lower2(approvers);
|
|
3792
|
+
let design;
|
|
3793
|
+
let tech;
|
|
3794
|
+
const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
|
|
3795
|
+
if (d.length || t.length) {
|
|
3796
|
+
design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
|
|
3797
|
+
const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
|
|
3798
|
+
const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
|
|
3799
|
+
tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
|
|
3800
|
+
} else {
|
|
3801
|
+
design = a.length >= 1;
|
|
3802
|
+
tech = a.length >= 2;
|
|
3803
|
+
}
|
|
3804
|
+
return { approvers, changesRequested, design, tech, owner };
|
|
3805
|
+
}
|
|
3806
|
+
function lookAgainReviewers(reviews, headSha, author) {
|
|
3807
|
+
if (!headSha) return [];
|
|
3808
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3809
|
+
const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
3810
|
+
for (const r of sorted) {
|
|
3811
|
+
const login = r.user?.login;
|
|
3812
|
+
if (!login || login === author) continue;
|
|
3813
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
|
|
3814
|
+
}
|
|
3815
|
+
const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
|
|
3816
|
+
if (!asked.length) return [];
|
|
3817
|
+
return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
|
|
3818
|
+
}
|
|
3819
|
+
function withSuggestionApprovals(pr, suggestion, prTier) {
|
|
3820
|
+
if (!suggestion) return pr;
|
|
3821
|
+
const s = suggestion.approvals;
|
|
3822
|
+
if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
|
|
3823
|
+
const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
|
|
3824
|
+
if (!covered) return pr;
|
|
3825
|
+
const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
|
|
3826
|
+
return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
|
|
3827
|
+
}
|
|
3828
|
+
function withFigmaPushApprovals(pr, push) {
|
|
3829
|
+
if (!push || push.scope === "outside") return pr;
|
|
3830
|
+
const tech = pr.tech || push.scope === "token-values";
|
|
3831
|
+
return { ...pr, design: true, tech, owner: true };
|
|
3832
|
+
}
|
|
3833
|
+
function withTokenValueApprovals(pr, valuesOnly) {
|
|
3834
|
+
if (!valuesOnly) return pr;
|
|
3835
|
+
return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
|
|
3836
|
+
}
|
|
3837
|
+
const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
3838
|
+
function canOverride(login, policy) {
|
|
3839
|
+
if (!login || !policy?.allowed.length) return false;
|
|
3840
|
+
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
3841
|
+
}
|
|
3842
|
+
function overrideHolders(policy, owners) {
|
|
3843
|
+
const allowed = policy?.allowed ?? [];
|
|
3844
|
+
const all = [...owners.design, ...owners.tech];
|
|
3845
|
+
if (!all.length || all.some((o) => o.includes("/"))) return allowed;
|
|
3846
|
+
const named = all.map(normLogin);
|
|
3847
|
+
return allowed.filter((l) => named.includes(normLogin(l)));
|
|
3848
|
+
}
|
|
3849
|
+
const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
|
|
3850
|
+
function mergeGate(input) {
|
|
3851
|
+
const blocking = [];
|
|
3852
|
+
const missing = [];
|
|
3853
|
+
const approvalReasons = [];
|
|
3854
|
+
const lifecycle = pullState(input);
|
|
3855
|
+
if (lifecycle === "merged" || lifecycle === "closed") {
|
|
3856
|
+
return { allowed: false, reasons: [lifecycle === "merged" ? "This branch is already merged." : "This branch is closed."], overridable: false, missing: [], canShip: !input.viewer || canShip(input.viewer, input.ship) };
|
|
3857
|
+
}
|
|
3858
|
+
if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
|
|
3859
|
+
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
3860
|
+
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
3861
|
+
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3862
|
+
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3863
|
+
const tier = input.tier ?? 3;
|
|
3864
|
+
if (tier >= 2) {
|
|
3865
|
+
if (!input.approvals.design) {
|
|
3866
|
+
approvalReasons.push("It needs a design owner approval.");
|
|
3867
|
+
missing.push("design approval");
|
|
3868
|
+
}
|
|
3869
|
+
if (!input.approvals.tech && !input.approvals.valuesOnly) {
|
|
3870
|
+
approvalReasons.push("It needs a tech owner approval.");
|
|
3871
|
+
missing.push("tech approval");
|
|
3872
|
+
}
|
|
3873
|
+
} else if (!hasOwnerApproval(input.approvals)) {
|
|
3874
|
+
approvalReasons.push("It needs one owner approval.");
|
|
3875
|
+
missing.push("approval");
|
|
3876
|
+
}
|
|
3877
|
+
const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
|
|
3878
|
+
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
3879
|
+
const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
3880
|
+
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
3881
|
+
const allowed = reasons.length === 0;
|
|
3882
|
+
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
3883
|
+
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
3884
|
+
}
|
|
3885
|
+
function branchVerdict(input) {
|
|
3886
|
+
const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
|
|
3887
|
+
const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
|
|
3888
|
+
const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
|
|
3889
|
+
const override = ownerOverridePolicy(input.override, input.owners);
|
|
3890
|
+
const state = input.state ?? "open";
|
|
3891
|
+
const draft = isDraft(input);
|
|
3892
|
+
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
3893
|
+
const gate = input.ci && approvals ? mergeGate({ tier: input.tier, ci: input.ci, approvals, draft, mergeable: input.mergeable, state, merged_at: input.merged_at, viewer: input.viewer, override, exploration: input.exploration, ship: input.ship }) : null;
|
|
3894
|
+
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
3895
|
+
const tier1Armed = Boolean(input.tier1Published && input.autoMerge && input.tier === 1 && input.ci?.state !== "fail");
|
|
3896
|
+
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed || tier1Armed) && state === "open" && !input.merged_at && !input.exploration;
|
|
3897
|
+
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
3898
|
+
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
3899
|
+
}
|
|
3900
|
+
const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
|
|
3901
|
+
const OVERRIDE_HEADING = "**Bridge owner override**";
|
|
3902
|
+
function overrideReasonMin(policy) {
|
|
3903
|
+
return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
|
|
3904
|
+
}
|
|
3905
|
+
function overrideReasonProblem(reason, policy) {
|
|
3906
|
+
const min = overrideReasonMin(policy);
|
|
3907
|
+
const n = reason.trim().length;
|
|
3908
|
+
if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
|
|
3909
|
+
if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
|
|
3910
|
+
return null;
|
|
3911
|
+
}
|
|
3912
|
+
function overrideComment(login, reason, missing) {
|
|
3913
|
+
const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
|
|
3914
|
+
return `${OVERRIDE_HEADING}
|
|
3915
|
+
|
|
3916
|
+
Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
|
|
3917
|
+
}
|
|
3918
|
+
function overrideRefusedComment(body, message) {
|
|
3919
|
+
return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
|
|
3132
3920
|
}
|
|
3133
3921
|
function parseOverrideComment(body) {
|
|
3134
3922
|
const text = (body ?? "").trim();
|
|
@@ -3177,9 +3965,9 @@ function reviewStatusKind(input) {
|
|
|
3177
3965
|
if (tier >= 2) {
|
|
3178
3966
|
if (!input.approvals.design) return out("needs-design");
|
|
3179
3967
|
if (!input.approvals.tech) return out("needs-tech");
|
|
3180
|
-
} else if (!input.approvals
|
|
3968
|
+
} else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
|
|
3181
3969
|
if (input.mergeable === null) return out("checking");
|
|
3182
|
-
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft:
|
|
3970
|
+
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: isDraft(input), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
|
|
3183
3971
|
return gate.allowed ? out("ready") : out("unknown");
|
|
3184
3972
|
}
|
|
3185
3973
|
const REVIEW_STATUS_LABEL = {
|
|
@@ -3197,6 +3985,10 @@ const REVIEW_STATUS_LABEL = {
|
|
|
3197
3985
|
ready: "Ready to merge",
|
|
3198
3986
|
unknown: "Status unknown"
|
|
3199
3987
|
};
|
|
3988
|
+
const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
|
|
3989
|
+
function reviewStatusLabel(kind, lookAgain) {
|
|
3990
|
+
return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
|
|
3991
|
+
}
|
|
3200
3992
|
const CI_STATE_LABEL = {
|
|
3201
3993
|
pass: "Tests pass",
|
|
3202
3994
|
fail: "Tests fail",
|
|
@@ -3234,8 +4026,8 @@ function reopenEligibility(x) {
|
|
|
3234
4026
|
}
|
|
3235
4027
|
const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
|
|
3236
4028
|
const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
|
|
3237
|
-
function requiredApprovals(tier) {
|
|
3238
|
-
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4029
|
+
function requiredApprovals(tier, valuesOnly) {
|
|
4030
|
+
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false, ...valuesOnly ? { valuesOnly: true } : {} }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
3239
4031
|
}
|
|
3240
4032
|
function pendingApprovals(tier, approvals) {
|
|
3241
4033
|
if (tier == null || !approvals) return [];
|
|
@@ -3323,6 +4115,46 @@ function parseSuggestionAppliedComment(body) {
|
|
|
3323
4115
|
return null;
|
|
3324
4116
|
}
|
|
3325
4117
|
}
|
|
4118
|
+
const SUGGESTION_OVERRIDE_MARK = "bridge-suggestion-override";
|
|
4119
|
+
function composeSuggestionOverrideComment(r) {
|
|
4120
|
+
const audit = overrideComment(r.login, r.reason, r.missing).replace(/ Merged from Bridge\.$/, " Approved as owner from Bridge.");
|
|
4121
|
+
const record = { payloadSha: r.payloadSha, login: r.login.replace(/^@/, "").toLowerCase(), missing: r.missing };
|
|
4122
|
+
return [audit, "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_OVERRIDE_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
4123
|
+
}
|
|
4124
|
+
function parseSuggestionOverrideComment(body) {
|
|
4125
|
+
const text = (body ?? "").trim();
|
|
4126
|
+
if (!text.startsWith(OVERRIDE_HEADING)) return null;
|
|
4127
|
+
const m = new RegExp(`<!-- ${SUGGESTION_OVERRIDE_MARK} (\\{[^\\n]*?\\}) -->`).exec(text);
|
|
4128
|
+
if (!m) return null;
|
|
4129
|
+
try {
|
|
4130
|
+
const r = JSON.parse(m[1]);
|
|
4131
|
+
if (typeof r.payloadSha !== "string" || typeof r.login !== "string") return null;
|
|
4132
|
+
const why = /Owner override by @[\w-]+: ([\s\S]*?)\. Missing:/.exec(text);
|
|
4133
|
+
return { login: r.login, reason: why?.[1] ?? "", missing: Array.isArray(r.missing) ? r.missing.map(String) : [], payloadSha: r.payloadSha };
|
|
4134
|
+
} catch {
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
function suggestionOverrideProblem(x) {
|
|
4139
|
+
const lists = approverLists(x.gov.owners, x.gov.rights ?? null, x.gov.teams);
|
|
4140
|
+
const holders = overrideHolders(x.gov.override, lists).map(norm);
|
|
4141
|
+
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.";
|
|
4142
|
+
if (norm(x.viewer) !== norm(x.author)) return "The owner override is for your own suggestion: approve this one the usual way.";
|
|
4143
|
+
const others = eligibleReviewers("approval", { owners: lists, author: x.author });
|
|
4144
|
+
if (others.open || others.logins.length) return "Someone else can review it: ask them instead.";
|
|
4145
|
+
return null;
|
|
4146
|
+
}
|
|
4147
|
+
function suggestionOverrideOf(comments, gov, opts) {
|
|
4148
|
+
if (!gov.override || !opts.author) return null;
|
|
4149
|
+
for (const c of [...comments].reverse()) {
|
|
4150
|
+
const r = parseSuggestionOverrideComment(c.body);
|
|
4151
|
+
if (!r || r.payloadSha !== opts.payloadSha || !c.author || norm(c.author) !== norm(r.login)) continue;
|
|
4152
|
+
if (suggestionOverrideProblem({ viewer: c.author, author: opts.author, gov })) continue;
|
|
4153
|
+
if (overrideReasonProblem(r.reason, gov.override)) continue;
|
|
4154
|
+
return r;
|
|
4155
|
+
}
|
|
4156
|
+
return null;
|
|
4157
|
+
}
|
|
3326
4158
|
const norm = (l) => l.trim().replace(/^@/, "").toLowerCase();
|
|
3327
4159
|
const COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
3328
4160
|
function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
@@ -3352,229 +4184,16 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
|
3352
4184
|
latestOld.delete(c.author);
|
|
3353
4185
|
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
3354
4186
|
});
|
|
3355
|
-
const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
|
|
4187
|
+
const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
|
|
3356
4188
|
const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
|
|
3357
|
-
|
|
4189
|
+
const out = { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
|
|
4190
|
+
const override = summary.changesRequested.length ? null : suggestionOverrideOf(comments, gov, { payloadSha: opts.payloadSha, author: opts.author });
|
|
4191
|
+
return override ? { ...out, design: true, tech: true, owner: true, override } : out;
|
|
3358
4192
|
}
|
|
3359
4193
|
function suggestionReviewState(tier, approvals) {
|
|
3360
4194
|
if (approvals.changesRequested.length) return "changes-requested";
|
|
3361
4195
|
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
3362
4196
|
}
|
|
3363
|
-
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3364
|
-
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3365
|
-
function planComponentPush(d, ctx) {
|
|
3366
|
-
const issues = [...d.unknown];
|
|
3367
|
-
const reasons = [];
|
|
3368
|
-
const edits = /* @__PURE__ */ new Map();
|
|
3369
|
-
const also = /* @__PURE__ */ new Set();
|
|
3370
|
-
for (const r of d.rebinds) {
|
|
3371
|
-
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3372
|
-
if (!r.after.length) {
|
|
3373
|
-
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 push it with the variables, then push again." });
|
|
3374
|
-
continue;
|
|
3375
|
-
}
|
|
3376
|
-
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3377
|
-
if (missing.length) {
|
|
3378
|
-
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Push the variable first (Variables, above), then push this component again." });
|
|
3379
|
-
continue;
|
|
3380
|
-
}
|
|
3381
|
-
if (!r.before.length) {
|
|
3382
|
-
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: Claude AI adds it to the CSS.`);
|
|
3383
|
-
continue;
|
|
3384
|
-
}
|
|
3385
|
-
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3386
|
-
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so Claude AI writes the CSS.`);
|
|
3387
|
-
continue;
|
|
3388
|
-
}
|
|
3389
|
-
const [from, to] = [r.before[0], r.after[0]];
|
|
3390
|
-
if (ctx.tierOf(from) !== "component") {
|
|
3391
|
-
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: Claude AI changes the CSS to ${to}.`);
|
|
3392
|
-
continue;
|
|
3393
|
-
}
|
|
3394
|
-
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3395
|
-
if (elsewhere.variants.length) {
|
|
3396
|
-
const v = elsewhere.variants;
|
|
3397
|
-
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3398
|
-
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so Claude AI adds a token for ${where(r)}.`);
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
for (const c of elsewhere.components) also.add(c);
|
|
3402
|
-
const prev = edits.get(from);
|
|
3403
|
-
if (prev && prev.value !== `{${to}}`) {
|
|
3404
|
-
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): Claude AI splits it.`);
|
|
3405
|
-
edits.delete(from);
|
|
3406
|
-
continue;
|
|
3407
|
-
}
|
|
3408
|
-
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3409
|
-
}
|
|
3410
|
-
const list = [...edits.values()];
|
|
3411
|
-
if (d.needsCode || reasons.length) {
|
|
3412
|
-
return {
|
|
3413
|
-
route: "claude-draft",
|
|
3414
|
-
said: `Claude AI drafts the code from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3415
|
-
edits: list,
|
|
3416
|
-
reasons,
|
|
3417
|
-
issues,
|
|
3418
|
-
alsoChanges: [...also]
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
if (list.length) {
|
|
3422
|
-
const n = list.length;
|
|
3423
|
-
const others = [...also];
|
|
3424
|
-
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3425
|
-
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"}.` : "";
|
|
3426
|
-
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 };
|
|
3427
|
-
}
|
|
3428
|
-
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: [] };
|
|
3429
|
-
}
|
|
3430
|
-
function specsUsage(specs, component) {
|
|
3431
|
-
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3432
|
-
return (path, r) => {
|
|
3433
|
-
const variants = [];
|
|
3434
|
-
const components = [];
|
|
3435
|
-
for (const c of specs.components) {
|
|
3436
|
-
for (const v of c.variants) {
|
|
3437
|
-
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3438
|
-
const key = variantKeyOf(v.props);
|
|
3439
|
-
if (c === own) {
|
|
3440
|
-
if (!r.variants.includes(key)) variants.push(key);
|
|
3441
|
-
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
return { variants, components };
|
|
3445
|
-
};
|
|
3446
|
-
}
|
|
3447
|
-
function componentContrastPairs(edits, pairs, tokens) {
|
|
3448
|
-
const out = /* @__PURE__ */ new Map();
|
|
3449
|
-
for (const e of edits) {
|
|
3450
|
-
const target = tokens.aliasOf(e.path);
|
|
3451
|
-
if (!target) continue;
|
|
3452
|
-
const group = e.path.split(".")[0];
|
|
3453
|
-
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3454
|
-
for (const p of pairs) {
|
|
3455
|
-
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3456
|
-
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
return [...out.values()];
|
|
3460
|
-
}
|
|
3461
|
-
const FROM_FIGMA_LABEL = "from-figma";
|
|
3462
|
-
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3463
|
-
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3464
|
-
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3465
|
-
const ORIGIN_MARK = "bridge-from-figma";
|
|
3466
|
-
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3467
|
-
function parseFigmaOrigin(body) {
|
|
3468
|
-
if (!body) return void 0;
|
|
3469
|
-
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3470
|
-
if (!m) return void 0;
|
|
3471
|
-
try {
|
|
3472
|
-
const o = JSON.parse(m[1]);
|
|
3473
|
-
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3474
|
-
} catch {
|
|
3475
|
-
return void 0;
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
function figmaOriginSection(o) {
|
|
3479
|
-
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3480
|
-
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3481
|
-
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted by Claude AI** from the Figma spec. A person reviews it before it ships." : `**Waiting for Claude AI:** this branch carries the spec only, as a request. Claude writes the code here (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3482
|
-
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the DS Bridge plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3483
|
-
}
|
|
3484
|
-
function withFigmaOrigin(body, o) {
|
|
3485
|
-
let b = (body ?? "").trim();
|
|
3486
|
-
const at = b.indexOf("**From Figma:**");
|
|
3487
|
-
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3488
|
-
if (at >= 0 && mark > at) {
|
|
3489
|
-
const end = b.indexOf("-->", mark);
|
|
3490
|
-
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3491
|
-
}
|
|
3492
|
-
return `${figmaOriginSection(o)}${b ? `
|
|
3493
|
-
|
|
3494
|
-
${b}` : ""}`;
|
|
3495
|
-
}
|
|
3496
|
-
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3497
|
-
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3498
|
-
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3499
|
-
function formatFigmaSpec(d, plan) {
|
|
3500
|
-
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3501
|
-
`;
|
|
3502
|
-
}
|
|
3503
|
-
function parseFigmaSpec(text) {
|
|
3504
|
-
if (!text) return void 0;
|
|
3505
|
-
try {
|
|
3506
|
-
const s = JSON.parse(text);
|
|
3507
|
-
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3508
|
-
} catch {
|
|
3509
|
-
return void 0;
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
function figmaDraftBrief(d, ctx) {
|
|
3513
|
-
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
3514
|
-
const suggested = "suggestion" in ctx;
|
|
3515
|
-
const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
|
|
3516
|
-
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
3517
|
-
return [
|
|
3518
|
-
suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
|
|
3519
|
-
"",
|
|
3520
|
-
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
3521
|
-
`Kind: ${kindLabel(d.kind)}`,
|
|
3522
|
-
`Change: ${d.title}`,
|
|
3523
|
-
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
3524
|
-
"",
|
|
3525
|
-
d.summary.trim(),
|
|
3526
|
-
"",
|
|
3527
|
-
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
3528
|
-
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
3529
|
-
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
3530
|
-
...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
|
|
3531
|
-
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3532
|
-
`Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
|
|
3533
|
-
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
3534
|
-
...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
|
|
3535
|
-
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
3536
|
-
"",
|
|
3537
|
-
"Steps:",
|
|
3538
|
-
"suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
|
|
3539
|
-
`2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
|
|
3540
|
-
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
3541
|
-
"suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The branch is then labelled ${DRAFTED_BY_CLAUDE_LABEL} and a person reviews it.`,
|
|
3542
|
-
"",
|
|
3543
|
-
"Rules:",
|
|
3544
|
-
"- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
|
|
3545
|
-
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
3546
|
-
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
3547
|
-
].join("\n");
|
|
3548
|
-
}
|
|
3549
|
-
function figmaDraftBody(d, plan, o) {
|
|
3550
|
-
const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3551
|
-
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
|
|
3552
|
-
return withFigmaOrigin(
|
|
3553
|
-
[
|
|
3554
|
-
`## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
3555
|
-
"",
|
|
3556
|
-
d.summary.trim(),
|
|
3557
|
-
"",
|
|
3558
|
-
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
3559
|
-
...rows,
|
|
3560
|
-
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
3561
|
-
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
3562
|
-
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3563
|
-
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
3564
|
-
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
3565
|
-
].join("\n"),
|
|
3566
|
-
o
|
|
3567
|
-
);
|
|
3568
|
-
}
|
|
3569
|
-
const figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
3570
|
-
function figmaPushStatus(x) {
|
|
3571
|
-
const labels = x.labels ?? [];
|
|
3572
|
-
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
3573
|
-
if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
|
|
3574
|
-
if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for Claude AI", sentence: "The spec is on the library branch. Claude AI writes the code there, then a person reviews it.", rebuild: false };
|
|
3575
|
-
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted by Claude AI", sentence: "Claude AI drafted the code. A person reviews it before it ships.", rebuild: false };
|
|
3576
|
-
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3577
|
-
}
|
|
3578
4197
|
const SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
3579
4198
|
const SUGGESTION_LABEL = "bridge-suggestion";
|
|
3580
4199
|
const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
@@ -3836,6 +4455,11 @@ function suggestionKindFromLabels(labels) {
|
|
|
3836
4455
|
function suggestionStateFromLabels(labels) {
|
|
3837
4456
|
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
3838
4457
|
}
|
|
4458
|
+
function suggestionIsTokenValues(p) {
|
|
4459
|
+
if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
|
|
4460
|
+
if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
|
|
4461
|
+
return (p.files ?? []).every((f) => f.path.endsWith(".md"));
|
|
4462
|
+
}
|
|
3839
4463
|
function suggestionStatus(i) {
|
|
3840
4464
|
const prs = i.linkedPrs ?? [];
|
|
3841
4465
|
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
@@ -3906,7 +4530,7 @@ function checksSection(p, max) {
|
|
|
3906
4530
|
"",
|
|
3907
4531
|
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
3908
4532
|
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
3909
|
-
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one approval" : "design and tech approval"}`,
|
|
4533
|
+
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
|
|
3910
4534
|
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
3911
4535
|
"",
|
|
3912
4536
|
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
@@ -4380,8 +5004,8 @@ async function planSuggestionApply(input) {
|
|
|
4380
5004
|
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.`);
|
|
4381
5005
|
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
4382
5006
|
}
|
|
4383
|
-
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
4384
|
-
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
|
|
5007
|
+
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth, override: config.governance.override ?? null };
|
|
5008
|
+
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
|
|
4385
5009
|
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
4386
5010
|
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
4387
5011
|
const approved = review === "approved";
|
|
@@ -4458,16 +5082,16 @@ async function planSuggestionApply(input) {
|
|
|
4458
5082
|
const issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
4459
5083
|
const prevErrors = new Set(validateLayered(before).map(issueKey2));
|
|
4460
5084
|
const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i))) : [];
|
|
4461
|
-
let
|
|
5085
|
+
let pairs2 = [];
|
|
4462
5086
|
if (config.contrast && tokenChanges.length) {
|
|
4463
5087
|
try {
|
|
4464
|
-
|
|
5088
|
+
pairs2 = parseContrastChecks((await ws.read(config.contrast.path))?.text);
|
|
4465
5089
|
} catch {
|
|
4466
5090
|
notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
|
|
4467
5091
|
}
|
|
4468
5092
|
}
|
|
4469
5093
|
const modes = config.contrast?.modes;
|
|
4470
|
-
const contrast =
|
|
5094
|
+
const contrast = pairs2.length ? contrastDelta(runLayeredContrast(before, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
|
|
4471
5095
|
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
4472
5096
|
const otherFiles = [...writes.keys()].filter((p) => !tokenFileOf(config, p));
|
|
4473
5097
|
const tier = classifyRisk({ tokenChanges: tokenChanges.map((c) => ({ kind: c.kind, path: c.path, ...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((x) => x === "description") } : {} })), files: otherFiles.map((path) => ({ path })), config });
|
|
@@ -4539,6 +5163,7 @@ function listItem(i, pulls) {
|
|
|
4539
5163
|
title: i.title,
|
|
4540
5164
|
url: i.url,
|
|
4541
5165
|
author: i.author,
|
|
5166
|
+
requester: requesterOf(i.body),
|
|
4542
5167
|
createdAt: i.createdAt,
|
|
4543
5168
|
state: i.state,
|
|
4544
5169
|
stateReason: i.stateReason,
|
|
@@ -4554,7 +5179,7 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
|
4554
5179
|
const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
|
|
4555
5180
|
const linkedPrs = linkedPullsOf(n, pulls);
|
|
4556
5181
|
const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
|
|
4557
|
-
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
|
|
5182
|
+
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author, valuesOnly: suggestionIsTokenValues(parsed.payload) }) : void 0;
|
|
4558
5183
|
const status = suggestionStatus({
|
|
4559
5184
|
state: issue.state,
|
|
4560
5185
|
stateReason: issue.stateReason,
|
|
@@ -4565,6 +5190,19 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
|
4565
5190
|
});
|
|
4566
5191
|
return { issue, parsed, comments, approvals, linkedPrs, applied, status };
|
|
4567
5192
|
}
|
|
5193
|
+
async function approvedSuggestionOnGitHub(gh, body, gov) {
|
|
5194
|
+
for (const n of closingIssueNumbers(body).slice(0, 3)) {
|
|
5195
|
+
const found = await approvedSuggestionNumber(gh, n, gov).catch(() => null);
|
|
5196
|
+
if (found) return found;
|
|
5197
|
+
}
|
|
5198
|
+
return null;
|
|
5199
|
+
}
|
|
5200
|
+
async function approvedSuggestionNumber(gh, n, gov) {
|
|
5201
|
+
const d = await getSuggestionOnGitHub(gh, n, gov, { pulls: [] });
|
|
5202
|
+
if (!d.parsed.ok || !d.approvals) return null;
|
|
5203
|
+
const tier = d.parsed.payload.tier.tier;
|
|
5204
|
+
return suggestionReviewState(tier, d.approvals) === "approved" ? { number: n, approvals: d.approvals, tier } : null;
|
|
5205
|
+
}
|
|
4568
5206
|
async function syncSuggestionStateLabel(gh, n, labels, status) {
|
|
4569
5207
|
const { add, remove } = suggestionLabelChanges(labels, status);
|
|
4570
5208
|
let ok = true;
|
|
@@ -4662,6 +5300,27 @@ async function reviewSuggestionOnGitHub(gh, n, input, gov) {
|
|
|
4662
5300
|
if (labelWarning) warnings.push(labelWarning);
|
|
4663
5301
|
return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
|
|
4664
5302
|
}
|
|
5303
|
+
async function overrideSuggestionOnGitHub(gh, n, input, gov) {
|
|
5304
|
+
if (!gov.override) throw new GitHubError(422, "The review rules allow no owner override.");
|
|
5305
|
+
const before = await getSuggestionOnGitHub(gh, n, gov);
|
|
5306
|
+
if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be approved: ${before.parsed.message}`);
|
|
5307
|
+
if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed.`);
|
|
5308
|
+
const sha = before.parsed.sha;
|
|
5309
|
+
if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and approve the new version.`);
|
|
5310
|
+
const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
|
|
5311
|
+
const problem = suggestionOverrideProblem({ viewer: by, author: before.issue.author, gov }) ?? overrideReasonProblem(input.reason, gov.override);
|
|
5312
|
+
if (problem) throw new GitHubError(422, problem);
|
|
5313
|
+
if (before.approvals?.changesRequested.length) throw new GitHubError(422, `Changes were asked for on #${n}: the owner override covers missing approvals only.`);
|
|
5314
|
+
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");
|
|
5315
|
+
const posted = await gh.createIssueComment(n, composeSuggestionOverrideComment({ login: by, reason: input.reason, missing, payloadSha: sha }));
|
|
5316
|
+
const after = await getSuggestionOnGitHub(gh, n, gov);
|
|
5317
|
+
const counts = Boolean(after.approvals?.override);
|
|
5318
|
+
const warnings = [];
|
|
5319
|
+
if (!counts) warnings.push("The owner override is posted but does not count: check the owners and the override in bridge.config.json.");
|
|
5320
|
+
const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, after.status);
|
|
5321
|
+
if (labelWarning) warnings.push(labelWarning);
|
|
5322
|
+
return { commentUrl: posted.url, payloadSha: sha, status: after.status, counts, warnings };
|
|
5323
|
+
}
|
|
4665
5324
|
async function linkSuggestionPrOnGitHub(gh, n, link) {
|
|
4666
5325
|
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.");
|
|
4667
5326
|
const issue = await gh.getIssue(n);
|
|
@@ -4689,6 +5348,11 @@ const DOCS_MCP_ORIGIN = {
|
|
|
4689
5348
|
footer: 'Proposed with the Bridge MCP server on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
4690
5349
|
label: "source: mcp"
|
|
4691
5350
|
};
|
|
5351
|
+
const DOCS_CLI_ORIGIN = {
|
|
5352
|
+
where: "with the bridge CLI",
|
|
5353
|
+
footer: 'Proposed with the bridge CLI on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
5354
|
+
label: "source: cli"
|
|
5355
|
+
};
|
|
4692
5356
|
const EM_DASH = /\u2014/;
|
|
4693
5357
|
function validateDocs(d) {
|
|
4694
5358
|
const out = [];
|
|
@@ -4821,7 +5485,7 @@ ${opts.notes.trim()}
|
|
|
4821
5485
|
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
4822
5486
|
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
4823
5487
|
}
|
|
4824
|
-
const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : "studio";
|
|
5488
|
+
const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : origin.label === DOCS_CLI_ORIGIN.label ? "cli" : "studio";
|
|
4825
5489
|
function docsSuggestionPayload(edits, opts) {
|
|
4826
5490
|
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
4827
5491
|
const date = opts.date ?? /* @__PURE__ */ new Date();
|
|
@@ -4898,6 +5562,149 @@ async function commitFileOnGitHub(gh, input) {
|
|
|
4898
5562
|
if (content === cur.text) return "";
|
|
4899
5563
|
return gh.putFile({ path: input.path, content, branch: input.branch, message: input.message, sha: cur.sha });
|
|
4900
5564
|
}
|
|
5565
|
+
class StorybookIndexError extends Error {
|
|
5566
|
+
constructor(code, message, status) {
|
|
5567
|
+
super(message);
|
|
5568
|
+
this.code = code;
|
|
5569
|
+
this.status = status;
|
|
5570
|
+
this.name = "StorybookIndexError";
|
|
5571
|
+
}
|
|
5572
|
+
code;
|
|
5573
|
+
status;
|
|
5574
|
+
}
|
|
5575
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5576
|
+
const str = (v) => typeof v === "string" && v.trim() ? v : void 0;
|
|
5577
|
+
function entryFrom(key, raw, v3) {
|
|
5578
|
+
if (!isObj(raw)) return null;
|
|
5579
|
+
const id = str(raw.id) ?? key;
|
|
5580
|
+
const title = str(raw.title) ?? str(raw.kind);
|
|
5581
|
+
const name = str(raw.name) ?? str(raw.story);
|
|
5582
|
+
if (!id || !title || !name) return null;
|
|
5583
|
+
const params = isObj(raw.parameters) ? raw.parameters : {};
|
|
5584
|
+
const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
|
|
5585
|
+
const e = {
|
|
5586
|
+
id,
|
|
5587
|
+
title,
|
|
5588
|
+
name,
|
|
5589
|
+
type,
|
|
5590
|
+
importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
|
|
5591
|
+
tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : []
|
|
5592
|
+
};
|
|
5593
|
+
const subtype = str(raw.subtype);
|
|
5594
|
+
if (subtype) e.subtype = subtype;
|
|
5595
|
+
const exportName = str(raw.exportName);
|
|
5596
|
+
if (exportName) e.exportName = exportName;
|
|
5597
|
+
const componentPath = str(raw.componentPath);
|
|
5598
|
+
if (componentPath) e.componentPath = componentPath;
|
|
5599
|
+
return e;
|
|
5600
|
+
}
|
|
5601
|
+
function groupStorybookComponents(entries) {
|
|
5602
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
5603
|
+
for (const e of entries) {
|
|
5604
|
+
let c = byTitle.get(e.title);
|
|
5605
|
+
if (!c) {
|
|
5606
|
+
const parts = e.title.split("/").map((p) => p.trim()).filter(Boolean);
|
|
5607
|
+
c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
|
|
5608
|
+
byTitle.set(e.title, c);
|
|
5609
|
+
}
|
|
5610
|
+
if (e.type === "docs") c.docs.push(e);
|
|
5611
|
+
else if (e.subtype !== "test") c.stories.push(e);
|
|
5612
|
+
for (const t of e.tags) if (!c.tags.includes(t)) c.tags.push(t);
|
|
5613
|
+
}
|
|
5614
|
+
const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
|
|
5615
|
+
for (const c of out) c.tags.sort();
|
|
5616
|
+
return out.sort((a, b) => a.title.localeCompare(b.title));
|
|
5617
|
+
}
|
|
5618
|
+
function parseStorybookIndex(json, source) {
|
|
5619
|
+
if (!isObj(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
|
|
5620
|
+
if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
|
|
5621
|
+
throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
|
|
5622
|
+
}
|
|
5623
|
+
const map = isObj(json.entries) ? json.entries : isObj(json.stories) ? json.stories : null;
|
|
5624
|
+
if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
|
|
5625
|
+
const v3 = !isObj(json.entries);
|
|
5626
|
+
const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
|
|
5627
|
+
if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
|
|
5628
|
+
const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
|
|
5629
|
+
return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
|
|
5630
|
+
}
|
|
5631
|
+
function normalizeStorybookUrl(input) {
|
|
5632
|
+
let u;
|
|
5633
|
+
try {
|
|
5634
|
+
u = new URL(input.trim());
|
|
5635
|
+
} catch {
|
|
5636
|
+
throw new StorybookIndexError("invalid-url", "Enter the full Storybook address, for example https://storybook.example.com");
|
|
5637
|
+
}
|
|
5638
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
5639
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
5640
|
+
throw new StorybookIndexError("invalid-url", "The Storybook address must start with https://");
|
|
5641
|
+
}
|
|
5642
|
+
if (u.username || u.password) throw new StorybookIndexError("invalid-url", "Remove the user name and password from the address.");
|
|
5643
|
+
const path = u.pathname.replace(/\/(index\.html|iframe\.html|index\.json|stories\.json)$/i, "/").replace(/\/+$/, "");
|
|
5644
|
+
return `${u.protocol}//${u.host}${path}`;
|
|
5645
|
+
}
|
|
5646
|
+
const storybookIndexUrls = (base) => {
|
|
5647
|
+
const b = base.replace(/\/+$/, "");
|
|
5648
|
+
return [
|
|
5649
|
+
{ url: `${b}/index.json`, source: "index.json" },
|
|
5650
|
+
{ url: `${b}/stories.json`, source: "stories.json" }
|
|
5651
|
+
];
|
|
5652
|
+
};
|
|
5653
|
+
const pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
|
|
5654
|
+
function storybookIframeUrl(base, id, o = {}) {
|
|
5655
|
+
let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
|
|
5656
|
+
if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
|
|
5657
|
+
if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
|
|
5658
|
+
return url;
|
|
5659
|
+
}
|
|
5660
|
+
function storybookManagerUrl(base, id, viewMode = "story") {
|
|
5661
|
+
return `${base.replace(/\/+$/, "")}/?path=/${viewMode}/${encodeURIComponent(id)}`;
|
|
5662
|
+
}
|
|
5663
|
+
async function fetchStorybookIndex(base, o = {}) {
|
|
5664
|
+
const doFetch = o.fetch ?? ((url, init) => fetch(url, init));
|
|
5665
|
+
const rank = { private: 6, http: 5, network: 4, empty: 3, "not-json": 2, "not-found": 1, "invalid-url": 0 };
|
|
5666
|
+
let worst = null;
|
|
5667
|
+
const keep = (e) => {
|
|
5668
|
+
if (!worst || rank[e.code] > rank[worst.code]) worst = e;
|
|
5669
|
+
};
|
|
5670
|
+
for (const { url, source } of storybookIndexUrls(base)) {
|
|
5671
|
+
let res;
|
|
5672
|
+
try {
|
|
5673
|
+
res = await doFetch(url, { headers: { accept: "application/json" }, ...o.signal ? { signal: o.signal } : {} });
|
|
5674
|
+
} catch (err) {
|
|
5675
|
+
if (o.signal?.aborted) throw err;
|
|
5676
|
+
keep(new StorybookIndexError("network", `Could not reach ${url}. The host may be down or may not allow reading from another site.`));
|
|
5677
|
+
continue;
|
|
5678
|
+
}
|
|
5679
|
+
if (res.status === 404 || res.status === 410) {
|
|
5680
|
+
keep(new StorybookIndexError("not-found", `No Storybook index at ${base}: neither index.json nor stories.json was found.`, res.status));
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
if (res.status === 401 || res.status === 403) {
|
|
5684
|
+
keep(new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.", res.status));
|
|
5685
|
+
continue;
|
|
5686
|
+
}
|
|
5687
|
+
if (!res.ok) {
|
|
5688
|
+
keep(new StorybookIndexError("http", `The Storybook host answered ${res.status} for ${url}.`, res.status));
|
|
5689
|
+
continue;
|
|
5690
|
+
}
|
|
5691
|
+
const text = await res.text();
|
|
5692
|
+
let json;
|
|
5693
|
+
try {
|
|
5694
|
+
json = JSON.parse(text);
|
|
5695
|
+
} catch {
|
|
5696
|
+
keep(new StorybookIndexError("not-json", `${url} is not a Storybook index (the host answered with a web page).`));
|
|
5697
|
+
continue;
|
|
5698
|
+
}
|
|
5699
|
+
try {
|
|
5700
|
+
return parseStorybookIndex(json, source);
|
|
5701
|
+
} catch (err) {
|
|
5702
|
+
if (err instanceof StorybookIndexError) keep(err);
|
|
5703
|
+
else throw err;
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
throw worst ?? new StorybookIndexError("not-found", `No Storybook index at ${base}.`);
|
|
5707
|
+
}
|
|
4901
5708
|
function normalizeRepoPath(path) {
|
|
4902
5709
|
return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
|
|
4903
5710
|
}
|
|
@@ -4944,274 +5751,323 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
|
|
|
4944
5751
|
return out;
|
|
4945
5752
|
}
|
|
4946
5753
|
export {
|
|
4947
|
-
|
|
5754
|
+
classifyLayered as $,
|
|
4948
5755
|
explorationPrTitle as A,
|
|
4949
5756
|
explorationPrBody as B,
|
|
4950
|
-
|
|
5757
|
+
compareSpecToMeasured as C,
|
|
4951
5758
|
DRAFT_LINE_MAX as D,
|
|
4952
5759
|
EXPLORATION_PREFIX as E,
|
|
4953
|
-
|
|
5760
|
+
variantKeyOf as F,
|
|
4954
5761
|
GitHubError as G,
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
5762
|
+
withoutAddedWrappers as H,
|
|
5763
|
+
pairParts as I,
|
|
5764
|
+
COLOR_FIELDS as J,
|
|
5765
|
+
hashSpec as K,
|
|
5766
|
+
DRAFTED_BY_CLAUDE_LABEL as L,
|
|
5767
|
+
overrideComment as M,
|
|
5768
|
+
overrideRefusedComment as N,
|
|
4962
5769
|
OVERRIDE_HEADING as O,
|
|
4963
5770
|
PROSE_KEYS as P,
|
|
4964
|
-
|
|
5771
|
+
suggestionStateFromLabels as Q,
|
|
4965
5772
|
REQUESTS_BRANCH as R,
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5773
|
+
SPEC_FIELDS as S,
|
|
5774
|
+
suggestionStatus as T,
|
|
5775
|
+
composeRequestIssue as U,
|
|
5776
|
+
EXPLORATION_NOTES_DIR as V,
|
|
5777
|
+
isExplorationBranch as W,
|
|
5778
|
+
diffLayered as X,
|
|
5779
|
+
contrastDelta as Y,
|
|
5780
|
+
runLayeredContrast as Z,
|
|
5781
|
+
layeredImpact as _,
|
|
4975
5782
|
parseChangelog as a,
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5783
|
+
REVIEW_STATUS_ORDER as a$,
|
|
5784
|
+
composeProposalBody as a0,
|
|
5785
|
+
createSuggestionOnGitHub as a1,
|
|
5786
|
+
gitBlobSha as a2,
|
|
5787
|
+
SUGGESTION_SCHEMA as a3,
|
|
5788
|
+
branchWithJiraKey as a4,
|
|
5789
|
+
titleWithJiraKey as a5,
|
|
5790
|
+
figmaSpecPath as a6,
|
|
5791
|
+
formatFigmaSpec as a7,
|
|
5792
|
+
figmaDraftBrief as a8,
|
|
5793
|
+
parseFigmaOrigin as a9,
|
|
5794
|
+
requestIssueTitle as aA,
|
|
5795
|
+
pullState as aB,
|
|
5796
|
+
summarizeCi as aC,
|
|
5797
|
+
summarizeApprovals as aD,
|
|
5798
|
+
BULK_LIMIT as aE,
|
|
5799
|
+
CI_STATE_LABEL as aF,
|
|
5800
|
+
COMPONENT_SPECS_VERSION as aG,
|
|
5801
|
+
CONFLICT_REASON as aH,
|
|
5802
|
+
DEFAULT_COMPONENT_SPECS_FILE as aI,
|
|
5803
|
+
DEFAULT_PROTECTED_BRANCHES as aJ,
|
|
5804
|
+
DOCS_CHANGE_BUMP as aK,
|
|
5805
|
+
DOCS_CLI_ORIGIN as aL,
|
|
5806
|
+
DOCS_EDITOR_ORIGIN as aM,
|
|
5807
|
+
DOCS_KEYS as aN,
|
|
5808
|
+
DOCS_MCP_ORIGIN as aO,
|
|
5809
|
+
EXPLORATION_CHECK as aP,
|
|
5810
|
+
EXPLORATION_STATUS_LABEL as aQ,
|
|
5811
|
+
EXPLORATION_TIER_LABEL as aR,
|
|
5812
|
+
EXPLORATION_USE_CASES as aS,
|
|
5813
|
+
FORMER_MEMBER as aT,
|
|
5814
|
+
LOOK_AGAIN_STATUS_LABEL as aU,
|
|
5815
|
+
MeasuredSpecsError as aV,
|
|
5816
|
+
PULL_STATE_PILL as aW,
|
|
5817
|
+
ProtectedBranchError as aX,
|
|
5818
|
+
REDUCED_MOTION_NOTE as aY,
|
|
5819
|
+
REQUEST_STATUS_LABEL as aZ,
|
|
5820
|
+
REVIEW_STATUS_LABEL as a_,
|
|
5821
|
+
figmaPushStatus as aa,
|
|
5822
|
+
parseFigmaSpec as ab,
|
|
5823
|
+
FIGMA_SPEC_DIR as ac,
|
|
5824
|
+
FROM_FIGMA_LABEL as ad,
|
|
5825
|
+
withFigmaOrigin as ae,
|
|
5826
|
+
WAITING_FOR_CLAUDE_LABEL as af,
|
|
5827
|
+
planComponentPush as ag,
|
|
5828
|
+
figmaOriginSection as ah,
|
|
5829
|
+
figmaBranchTitle as ai,
|
|
5830
|
+
figmaSpecSlug as aj,
|
|
5831
|
+
figmaDraftBody as ak,
|
|
5832
|
+
REQUEST_LABEL as al,
|
|
5833
|
+
figmaNodeUrl as am,
|
|
5834
|
+
parentDir as an,
|
|
5835
|
+
parseComponentSpecs as ao,
|
|
5836
|
+
readRepoTexts as ap,
|
|
5837
|
+
normalizeRepoPath as aq,
|
|
5838
|
+
isUnder as ar,
|
|
5839
|
+
componentMotion as as,
|
|
5840
|
+
GitHubClient as at,
|
|
5841
|
+
effectiveTier as au,
|
|
5842
|
+
parseRequestIssue as av,
|
|
5843
|
+
requestStatus as aw,
|
|
5844
|
+
listSuggestionsOnGitHub as ax,
|
|
5845
|
+
getSuggestionOnGitHub as ay,
|
|
5846
|
+
closingIssueNumbers as az,
|
|
5040
5847
|
buildReport as b,
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5848
|
+
docsSlug as b$,
|
|
5849
|
+
SUGGESTION_APPLIED_MARK as b0,
|
|
5850
|
+
SUGGESTION_APPLY_EXIT as b1,
|
|
5851
|
+
SUGGESTION_FINAL_LABELS as b2,
|
|
5852
|
+
SUGGESTION_KINDS as b3,
|
|
5853
|
+
SUGGESTION_LABEL as b4,
|
|
5854
|
+
SUGGESTION_LIMITS as b5,
|
|
5855
|
+
SUGGESTION_OVERRIDE_MARK as b6,
|
|
5856
|
+
SUGGESTION_REVIEW_MARK as b7,
|
|
5857
|
+
SUGGESTION_STATE_LABELS as b8,
|
|
5858
|
+
SUGGESTION_STATUS_LABEL as b9,
|
|
5859
|
+
claudeStage as bA,
|
|
5860
|
+
cleanDocs as bB,
|
|
5861
|
+
clearSharedEtags as bC,
|
|
5862
|
+
commitFileOnGitHub as bD,
|
|
5863
|
+
componentContrastPairs as bE,
|
|
5864
|
+
componentsUsingMotionToken as bF,
|
|
5865
|
+
composeChangeset as bG,
|
|
5866
|
+
composeSuggestionAppliedComment as bH,
|
|
5867
|
+
composeSuggestionIssue as bI,
|
|
5868
|
+
composeSuggestionOverrideComment as bJ,
|
|
5869
|
+
composeSuggestionReviewComment as bK,
|
|
5870
|
+
continueDocsProposalOnGitHub as bL,
|
|
5871
|
+
contrastDeltaTable as bM,
|
|
5872
|
+
countChanges as bN,
|
|
5873
|
+
coverageSummary as bO,
|
|
5874
|
+
cssColorToRgb as bP,
|
|
5875
|
+
cssColorsToRgb as bQ,
|
|
5876
|
+
cssModuleClasses as bR,
|
|
5877
|
+
declineComment as bS,
|
|
5878
|
+
declineReasonProblems as bT,
|
|
5879
|
+
declineRequestOnGitHub as bU,
|
|
5880
|
+
declineSuggestionOnGitHub as bV,
|
|
5881
|
+
defaultDocsFooter as bW,
|
|
5882
|
+
docsAuthorLine as bX,
|
|
5883
|
+
docsFileText as bY,
|
|
5884
|
+
docsProposal as bZ,
|
|
5885
|
+
docsSectionShapes as b_,
|
|
5886
|
+
SUGGESTION_STATUS_LABELS as ba,
|
|
5887
|
+
SUGGESTION_TOO_LARGE as bb,
|
|
5888
|
+
StorybookIndexError as bc,
|
|
5889
|
+
SuggestionError as bd,
|
|
5890
|
+
TOKEN_REJECTED_MESSAGE as be,
|
|
5891
|
+
TOKEN_SETTINGS_URL as bf,
|
|
5892
|
+
TRIGGER_LABELS as bg,
|
|
5893
|
+
UNRELEASED as bh,
|
|
5894
|
+
acceptDocsDraft as bi,
|
|
5895
|
+
acceptDocsText as bj,
|
|
5896
|
+
allowedMergeMethods as bk,
|
|
5897
|
+
anatomyLabel as bl,
|
|
5898
|
+
appliedStatus as bm,
|
|
5899
|
+
approvedSuggestionNumber as bn,
|
|
5900
|
+
approvedSuggestionOnGitHub as bo,
|
|
5901
|
+
assertSafeBranch as bp,
|
|
5902
|
+
assignRequestOnGitHub as bq,
|
|
5903
|
+
assignVersions as br,
|
|
5904
|
+
base64ToUtf8 as bs,
|
|
5905
|
+
branchVerdict as bt,
|
|
5906
|
+
bridgeMcpInstallCommand as bu,
|
|
5907
|
+
canOverride as bv,
|
|
5908
|
+
canonicalJson as bw,
|
|
5909
|
+
changeTable as bx,
|
|
5910
|
+
changesetBump as by,
|
|
5911
|
+
classifyAuth as bz,
|
|
5105
5912
|
collectHistory as c,
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5913
|
+
parseChangesetText as c$,
|
|
5914
|
+
docsSuggestionPayload as c0,
|
|
5915
|
+
docsText as c1,
|
|
5916
|
+
draftChangeLine as c2,
|
|
5917
|
+
draftPushDescription as c3,
|
|
5918
|
+
draftRequestText as c4,
|
|
5919
|
+
eligibleReviewers as c5,
|
|
5920
|
+
evalCalc as c6,
|
|
5921
|
+
explorationBranch as c7,
|
|
5922
|
+
explorationRules as c8,
|
|
5923
|
+
explorationShareMessage as c9,
|
|
5924
|
+
isTokenRejected as cA,
|
|
5925
|
+
jiraIssueFields as cB,
|
|
5926
|
+
jiraKeyOf as cC,
|
|
5927
|
+
joinRepoPath as cD,
|
|
5928
|
+
linkRequestToExplorationOnGitHub as cE,
|
|
5929
|
+
linkSuggestionPrOnGitHub as cF,
|
|
5930
|
+
linkedPullsOf as cG,
|
|
5931
|
+
loadRepoFiles as cH,
|
|
5932
|
+
lookAgainReviewers as cI,
|
|
5933
|
+
lowerFirstWord as cJ,
|
|
5934
|
+
markSuggestionsAppliedOnGitHub as cK,
|
|
5935
|
+
markUsedTokens as cL,
|
|
5936
|
+
memoryEtagStore as cM,
|
|
5937
|
+
memorySource as cN,
|
|
5938
|
+
mergeGate as cO,
|
|
5939
|
+
missingPermissionMessage as cP,
|
|
5940
|
+
missingProse as cQ,
|
|
5941
|
+
modeLabel as cR,
|
|
5942
|
+
needsDocsReview as cS,
|
|
5943
|
+
normalizeStorybookUrl as cT,
|
|
5944
|
+
openPulls as cU,
|
|
5945
|
+
overrideHolders as cV,
|
|
5946
|
+
overrideReasonMin as cW,
|
|
5947
|
+
overrideReasonProblem as cX,
|
|
5948
|
+
overrideSuggestionOnGitHub as cY,
|
|
5949
|
+
ownerOverridePolicy as cZ,
|
|
5950
|
+
parseBoxShadow as c_,
|
|
5951
|
+
explorationSlugFromBranch as ca,
|
|
5952
|
+
fetchStorybookIndex as cb,
|
|
5953
|
+
figmaCell as cc,
|
|
5954
|
+
fileModes as cd,
|
|
5955
|
+
filterHistory as ce,
|
|
5956
|
+
findOpenDocsProposal as cf,
|
|
5957
|
+
findOpenDocsProposalOnGitHub as cg,
|
|
5958
|
+
firstParagraph as ch,
|
|
5959
|
+
formatDocsJson as ci,
|
|
5960
|
+
friendlyMessage as cj,
|
|
5961
|
+
githubSuggestionWorkspace as ck,
|
|
5962
|
+
groupStorybookComponents as cl,
|
|
5963
|
+
hasOwnerApproval as cm,
|
|
5964
|
+
hasSuggestionBlock as cn,
|
|
5965
|
+
hasWriteAccess as co,
|
|
5966
|
+
iconComponentName as cp,
|
|
5967
|
+
installationFor as cq,
|
|
5968
|
+
isActiveExploration as cr,
|
|
5969
|
+
isAutomationPull as cs,
|
|
5970
|
+
isDocsProposalBranch as ct,
|
|
5971
|
+
isDraft as cu,
|
|
5972
|
+
isGuardrailPath as cv,
|
|
5973
|
+
isJiraKey as cw,
|
|
5974
|
+
isMissingPermission as cx,
|
|
5975
|
+
isPlatformRefusal as cy,
|
|
5976
|
+
isReleasePull as cz,
|
|
5170
5977
|
applyTokenEdits as d,
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5978
|
+
suggestionReviewState as d$,
|
|
5979
|
+
parseJiraKey as d0,
|
|
5980
|
+
parseMeasuredSpecsFile as d1,
|
|
5981
|
+
parseOverrideComment as d2,
|
|
5982
|
+
parseRepo as d3,
|
|
5983
|
+
parseStorybookIndex as d4,
|
|
5984
|
+
parseStructureChange as d5,
|
|
5985
|
+
parseSuggestionAppliedComment as d6,
|
|
5986
|
+
parseSuggestionIssue as d7,
|
|
5987
|
+
parseSuggestionOverrideComment as d8,
|
|
5988
|
+
parseSuggestionReviewComment as d9,
|
|
5989
|
+
reviewStatusLabel as dA,
|
|
5990
|
+
reviewSuggestionOnGitHub as dB,
|
|
5991
|
+
reviewersToAsk as dC,
|
|
5992
|
+
sectionsNeedingReview as dD,
|
|
5993
|
+
sha256Hex as dE,
|
|
5994
|
+
sharedTokenPrefix as dF,
|
|
5995
|
+
shortVariant as dG,
|
|
5996
|
+
shortVariants as dH,
|
|
5997
|
+
specHash as dI,
|
|
5998
|
+
specsUsage as dJ,
|
|
5999
|
+
stalenessStatus as dK,
|
|
6000
|
+
storybookIframeUrl as dL,
|
|
6001
|
+
storybookIndexUrls as dM,
|
|
6002
|
+
storybookManagerUrl as dN,
|
|
6003
|
+
subjectTitle as dO,
|
|
6004
|
+
suggestionAllowedRoots as dP,
|
|
6005
|
+
suggestionAppliedComment as dQ,
|
|
6006
|
+
suggestionApplyTable as dR,
|
|
6007
|
+
suggestionIsTokenValues as dS,
|
|
6008
|
+
suggestionIssueTitle as dT,
|
|
6009
|
+
suggestionKindFromLabels as dU,
|
|
6010
|
+
suggestionLabelChanges as dV,
|
|
6011
|
+
suggestionLabels as dW,
|
|
6012
|
+
suggestionOverrideOf as dX,
|
|
6013
|
+
suggestionOverrideProblem as dY,
|
|
6014
|
+
suggestionPathProblems as dZ,
|
|
6015
|
+
suggestionPayloadSha as d_,
|
|
6016
|
+
parseTier as da,
|
|
6017
|
+
patchDocsJson as db,
|
|
6018
|
+
pendingApprovals as dc,
|
|
6019
|
+
permissionLabel as dd,
|
|
6020
|
+
planRestore as de,
|
|
6021
|
+
planRevert as df,
|
|
6022
|
+
planSuggestionApply as dg,
|
|
6023
|
+
planToJson as dh,
|
|
6024
|
+
prNumberFromSubject as di,
|
|
6025
|
+
prefillDocs as dj,
|
|
6026
|
+
proposeDocsOnGitHub as dk,
|
|
6027
|
+
propsTable as dl,
|
|
6028
|
+
pushChangeRows as dm,
|
|
6029
|
+
rateLimitMessage as dn,
|
|
6030
|
+
releasePull as dp,
|
|
6031
|
+
reopenEligibility as dq,
|
|
6032
|
+
requestBrief as dr,
|
|
6033
|
+
requestImagePath as ds,
|
|
6034
|
+
requesterFirstName as dt,
|
|
6035
|
+
requesterLine as du,
|
|
6036
|
+
requesterOf as dv,
|
|
6037
|
+
requiredApprovals as dw,
|
|
6038
|
+
requiredPermission as dx,
|
|
6039
|
+
reviewQueue as dy,
|
|
6040
|
+
reviewStatusKind as dz,
|
|
5214
6041
|
buildHistoryEntry as e,
|
|
6042
|
+
suggestionShapeProblems as e0,
|
|
6043
|
+
suggestionStalenessOnGitHub as e1,
|
|
6044
|
+
suggestionStateLabel as e2,
|
|
6045
|
+
suggestionSubject as e3,
|
|
6046
|
+
suggestionTouches as e4,
|
|
6047
|
+
summarizeSuggestionApprovals as e5,
|
|
6048
|
+
syncSuggestionStateLabel as e6,
|
|
6049
|
+
titleOf as e7,
|
|
6050
|
+
toChangeSummary as e8,
|
|
6051
|
+
tokenCssVar as e9,
|
|
6052
|
+
tokenFingerprint as ea,
|
|
6053
|
+
tokenHistory as eb,
|
|
6054
|
+
tokenVariablePath as ec,
|
|
6055
|
+
unsafePathProblem as ed,
|
|
6056
|
+
utf8ToBase64 as ee,
|
|
6057
|
+
validateComponentSpec as ef,
|
|
6058
|
+
validateDocs as eg,
|
|
6059
|
+
validateIntegrations as eh,
|
|
6060
|
+
variantName as ei,
|
|
6061
|
+
versionTimeline as ej,
|
|
6062
|
+
webStorageEtagStore as ek,
|
|
6063
|
+
withAuthorLine as el,
|
|
6064
|
+
withFigmaPushApprovals as em,
|
|
6065
|
+
withOutdated as en,
|
|
6066
|
+
withRequestExploration as eo,
|
|
6067
|
+
withSrgbColors as ep,
|
|
6068
|
+
withSuggestionApprovals as eq,
|
|
6069
|
+
withTokenValueApprovals as er,
|
|
6070
|
+
withoutJiraKey as es,
|
|
5215
6071
|
coerceValue as f,
|
|
5216
6072
|
checkLayeredPair as g,
|
|
5217
6073
|
formatChangeset as h,
|
|
@@ -5234,4 +6090,4 @@ export {
|
|
|
5234
6090
|
explorationNote as y,
|
|
5235
6091
|
explorationNotePath as z
|
|
5236
6092
|
};
|
|
5237
|
-
//# sourceMappingURL=repo-source-
|
|
6093
|
+
//# sourceMappingURL=repo-source-CshjjC7x.js.map
|