@bridge-ds/mcp 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/branches.d.ts +1 -1
- package/dist/brand.d.ts +132 -1
- package/dist/{chunk-QTO4EP6V.js → chunk-BZ3KXXMM.js} +33 -7
- package/dist/{chunk-MHCXMHCL.js → chunk-MFV5WHB5.js} +3021 -538
- package/dist/figma.d.ts +90 -0
- package/dist/history.d.ts +159 -1
- package/dist/index.js +2 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +7 -3
- package/dist/ops.d.ts +106 -1
- package/dist/pieces.d.ts +4 -1
- package/dist/{src-DHJOM5PQ.js → src-ABI7LARD.js} +1 -1
- package/dist/studio.d.ts +18 -0
- package/dist/suggestions.d.ts +396 -0
- package/package.json +3 -3
|
@@ -14,16 +14,19 @@ import {
|
|
|
14
14
|
__export,
|
|
15
15
|
aliasOf,
|
|
16
16
|
approverEntries,
|
|
17
|
+
approverLists,
|
|
17
18
|
assertSafeBranch,
|
|
18
19
|
baseBranchOf,
|
|
19
20
|
branchExists,
|
|
20
21
|
canShip,
|
|
21
22
|
changeTable,
|
|
23
|
+
changesMode,
|
|
22
24
|
checkLayeredPair,
|
|
23
25
|
classifyLayered,
|
|
24
26
|
classifyRisk,
|
|
25
27
|
comboKey,
|
|
26
28
|
commitToNewLocalBranch,
|
|
29
|
+
commitTree,
|
|
27
30
|
composeProposalBody,
|
|
28
31
|
contrastDelta,
|
|
29
32
|
contrastDeltaTable,
|
|
@@ -71,8 +74,9 @@ import {
|
|
|
71
74
|
unreadableShipTeams,
|
|
72
75
|
unreadableTeams,
|
|
73
76
|
validateLayered,
|
|
77
|
+
variantKeyOf,
|
|
74
78
|
whoCanShipSentence
|
|
75
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-BZ3KXXMM.js";
|
|
76
80
|
|
|
77
81
|
// src/lib.ts
|
|
78
82
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -102,11 +106,11 @@ function parseJson(text) {
|
|
|
102
106
|
const skipWs = () => {
|
|
103
107
|
while (i < n && isWs(text[i])) i++;
|
|
104
108
|
};
|
|
105
|
-
const
|
|
109
|
+
const fail3 = (msg) => {
|
|
106
110
|
throw new JsonParseError(`${msg} at offset ${i}`, i);
|
|
107
111
|
};
|
|
108
112
|
const parseString = () => {
|
|
109
|
-
if (text[i] !== '"')
|
|
113
|
+
if (text[i] !== '"') fail3("Expected string");
|
|
110
114
|
const start = i;
|
|
111
115
|
i++;
|
|
112
116
|
while (i < n) {
|
|
@@ -121,7 +125,7 @@ function parseJson(text) {
|
|
|
121
125
|
}
|
|
122
126
|
i++;
|
|
123
127
|
}
|
|
124
|
-
return
|
|
128
|
+
return fail3("Unterminated string");
|
|
125
129
|
};
|
|
126
130
|
const parseValue = () => {
|
|
127
131
|
skipWs();
|
|
@@ -139,7 +143,7 @@ function parseJson(text) {
|
|
|
139
143
|
skipWs();
|
|
140
144
|
const key = parseString();
|
|
141
145
|
skipWs();
|
|
142
|
-
if (text[i] !== ":")
|
|
146
|
+
if (text[i] !== ":") fail3('Expected ":"');
|
|
143
147
|
i++;
|
|
144
148
|
const value = parseValue();
|
|
145
149
|
entries.push({ key, value });
|
|
@@ -152,7 +156,7 @@ function parseJson(text) {
|
|
|
152
156
|
i++;
|
|
153
157
|
break;
|
|
154
158
|
}
|
|
155
|
-
|
|
159
|
+
fail3('Expected "," or "}"');
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
const raw = text.slice(start, i);
|
|
@@ -175,7 +179,7 @@ function parseJson(text) {
|
|
|
175
179
|
i++;
|
|
176
180
|
break;
|
|
177
181
|
}
|
|
178
|
-
|
|
182
|
+
fail3('Expected "," or "]"');
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
const raw = text.slice(start, i);
|
|
@@ -194,7 +198,7 @@ function parseJson(text) {
|
|
|
194
198
|
node = { kind: "null", value: null, raw: "null" };
|
|
195
199
|
} else {
|
|
196
200
|
const m = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(text.slice(i, i + 64));
|
|
197
|
-
if (!m) return
|
|
201
|
+
if (!m) return fail3("Unexpected token");
|
|
198
202
|
i += m[0].length;
|
|
199
203
|
node = { kind: "number", value: Number(m[0]), raw: m[0] };
|
|
200
204
|
}
|
|
@@ -210,7 +214,7 @@ function parseJson(text) {
|
|
|
210
214
|
const root = parseValue();
|
|
211
215
|
const trailingStart = i;
|
|
212
216
|
skipWs();
|
|
213
|
-
if (i < n)
|
|
217
|
+
if (i < n) fail3("Unexpected content after JSON value");
|
|
214
218
|
const trailing = text.slice(trailingStart);
|
|
215
219
|
return { root, leading, trailing, style: detectStyle(text, root) };
|
|
216
220
|
}
|
|
@@ -956,8 +960,8 @@ function encodePicks(picks) {
|
|
|
956
960
|
var RISK_TIER = RISK_LABELS[3];
|
|
957
961
|
function slugify(text, max = 40) {
|
|
958
962
|
const s = text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
959
|
-
const
|
|
960
|
-
return
|
|
963
|
+
const cut2 = s.length <= max ? s : s.slice(0, max + 1).replace(/-[^-]*$/, "");
|
|
964
|
+
return cut2.replace(/-+$/g, "") || "update";
|
|
961
965
|
}
|
|
962
966
|
|
|
963
967
|
// ../core/src/changeset.ts
|
|
@@ -1571,10 +1575,16 @@ var GitHubClient = class _GitHubClient {
|
|
|
1571
1575
|
async addComment(issueNumber, body) {
|
|
1572
1576
|
await this.request("POST", `${this.repoPath}/issues/${issueNumber}/comments`, { body });
|
|
1573
1577
|
}
|
|
1574
|
-
/**
|
|
1578
|
+
/**
|
|
1579
|
+
* Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early).
|
|
1580
|
+
* `authorAssociation` is GitHub's OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, NONE... (suggestion approvals read it).
|
|
1581
|
+
*/
|
|
1575
1582
|
async listIssueComments(issueNumber) {
|
|
1576
|
-
const r = await this.request(
|
|
1577
|
-
|
|
1583
|
+
const r = await this.request(
|
|
1584
|
+
"GET",
|
|
1585
|
+
`${this.repoPath}/issues/${issueNumber}/comments?per_page=100`
|
|
1586
|
+
);
|
|
1587
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at, createdAt: c.created_at ?? c.updated_at, authorAssociation: c.author_association ?? "NONE" }));
|
|
1578
1588
|
}
|
|
1579
1589
|
// --- Issues (component change requests) ---------------------------------------------------
|
|
1580
1590
|
async createIssue(input) {
|
|
@@ -2498,7 +2508,7 @@ function finishPlan(p) {
|
|
|
2498
2508
|
const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
|
|
2499
2509
|
const changeset = config.changesets ? { path: `${config.changesets.dir.replace(/\/$/, "")}/bridge-${slugify(p.title, 48) || p.kind}.md`, content: formatChangeset(config.changesets.package, tier.bump, p.title.replace(/\.?$/, ".")) } : void 0;
|
|
2500
2510
|
const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
|
|
2501
|
-
const
|
|
2511
|
+
const commitMessage2 = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
|
|
2502
2512
|
|
|
2503
2513
|
${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
|
|
2504
2514
|
const blocked = newErrors.length || newFailures.length;
|
|
@@ -2513,7 +2523,7 @@ ${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Plan
|
|
|
2513
2523
|
tier,
|
|
2514
2524
|
files,
|
|
2515
2525
|
...changeset ? { changeset } : {},
|
|
2516
|
-
commitMessage,
|
|
2526
|
+
commitMessage: commitMessage2,
|
|
2517
2527
|
prBody,
|
|
2518
2528
|
notes
|
|
2519
2529
|
};
|
|
@@ -2741,7 +2751,7 @@ function toOklch(rgb) {
|
|
|
2741
2751
|
return { L, C, h };
|
|
2742
2752
|
}
|
|
2743
2753
|
var isHex = (s) => typeof s === "string" && /^#[0-9a-f]{6}$/i.test(s.trim());
|
|
2744
|
-
var fromHex = (
|
|
2754
|
+
var fromHex = (hex2) => [1, 3, 5].map((i) => parseInt(hex2.slice(i, i + 2), 16) / 255);
|
|
2745
2755
|
var round3 = (x) => Math.round(x * 1e3) / 1e3;
|
|
2746
2756
|
var toHex = (rgb) => "#" + rgb.map((x) => Math.round(x * 255).toString(16).padStart(2, "0")).join("");
|
|
2747
2757
|
function dtcgColor(rgb, alpha) {
|
|
@@ -2763,20 +2773,20 @@ function nearestStep(rgb) {
|
|
|
2763
2773
|
for (const s of STEPS) if (Math.abs(LIGHTNESS[s] - L) < Math.abs(LIGHTNESS[best] - L)) best = s;
|
|
2764
2774
|
return best;
|
|
2765
2775
|
}
|
|
2766
|
-
function anchoredRamp(
|
|
2767
|
-
const rgb = fromHex(
|
|
2776
|
+
function anchoredRamp(hex2, pinStep) {
|
|
2777
|
+
const rgb = fromHex(hex2);
|
|
2768
2778
|
const { L, C, h } = toOklch(rgb);
|
|
2769
2779
|
const step = pinStep ?? nearestStep(rgb);
|
|
2770
2780
|
const peak = Math.min(0.24, Math.max(0.02, C / BELL[step]));
|
|
2771
|
-
return { hue: h, peak, step, distance: Math.abs(L - LIGHTNESS[step]), ramp: generateRamp({ hue: h, chroma: bellChroma(peak), pins: { [step]:
|
|
2781
|
+
return { hue: h, peak, step, distance: Math.abs(L - LIGHTNESS[step]), ramp: generateRamp({ hue: h, chroma: bellChroma(peak), pins: { [step]: hex2.toUpperCase() } }) };
|
|
2772
2782
|
}
|
|
2773
2783
|
function neutralOffset() {
|
|
2774
2784
|
const off = {};
|
|
2775
|
-
for (const [s,
|
|
2785
|
+
for (const [s, hex2] of Object.entries(NEUTRAL_ANCHORS)) off[s] = toOklch(fromHex(hex2)).L - LIGHTNESS[s];
|
|
2776
2786
|
return (s) => off[s] ?? 0;
|
|
2777
2787
|
}
|
|
2778
|
-
function tintedNeutral(
|
|
2779
|
-
const rgb = fromHex(
|
|
2788
|
+
function tintedNeutral(hex2) {
|
|
2789
|
+
const rgb = fromHex(hex2);
|
|
2780
2790
|
const { C, h } = toOklch(rgb);
|
|
2781
2791
|
const ref = neutralChroma()(nearestStep(rgb));
|
|
2782
2792
|
const scale = C < 4e-3 ? 0 : Math.min(2.5, C / ref);
|
|
@@ -2850,19 +2860,19 @@ function rampFor(input, pinStep, kind) {
|
|
|
2850
2860
|
const ramp = kind === "neutral" ? generateRamp({ hue: hue2, chroma: neutralChroma(), offset: neutralOffset() }) : generateRamp({ hue: hue2, chroma: bellChroma(DEFAULT_PRIMARY_CHROMA) });
|
|
2851
2861
|
return { ramp, anchor: `OKLCH hue ${Math.round(hue2)}` };
|
|
2852
2862
|
}
|
|
2853
|
-
const
|
|
2854
|
-
if (!isHex(
|
|
2863
|
+
const hex2 = String(input).trim();
|
|
2864
|
+
if (!isHex(hex2)) throw new Error(`"${hex2}" is not a color. Use #rrggbb or a hue in degrees (0 to 360).`);
|
|
2855
2865
|
if (kind === "neutral") {
|
|
2856
|
-
const n = tintedNeutral(
|
|
2857
|
-
return { ramp: n.ramp, anchor: `${
|
|
2866
|
+
const n = tintedNeutral(hex2);
|
|
2867
|
+
return { ramp: n.ramp, anchor: `${hex2.toUpperCase()} (hue ${Math.round(n.hue)})` };
|
|
2858
2868
|
}
|
|
2859
|
-
const a = anchoredRamp(
|
|
2869
|
+
const a = anchoredRamp(hex2, pinStep);
|
|
2860
2870
|
const issue = a.distance > PIN_TOLERANCE ? {
|
|
2861
2871
|
level: "warning",
|
|
2862
2872
|
code: "pin-off-ladder",
|
|
2863
|
-
message: `${
|
|
2873
|
+
message: `${hex2.toUpperCase()} is ${a.distance > 0.12 ? "much " : ""}${toOklch(fromHex(hex2)).L > LIGHTNESS[a.step] ? "lighter" : "darker"} than the ${a.step} step it is pinned to, so the ramp around it will not read as one family. Primary actions use this step: pick a ${a.step >= 600 ? "deeper" : "lighter"} color.`
|
|
2864
2874
|
} : void 0;
|
|
2865
|
-
return { ramp: a.ramp, anchor: `${
|
|
2875
|
+
return { ramp: a.ramp, anchor: `${hex2.toUpperCase()} pinned at ${a.step}`, ...issue ? { issue } : {} };
|
|
2866
2876
|
}
|
|
2867
2877
|
function aliasRamp(doc, role, ramp, description) {
|
|
2868
2878
|
const group = getAt(doc, `palette.${role}`);
|
|
@@ -3401,14 +3411,30 @@ function patchDocsJson(original, next) {
|
|
|
3401
3411
|
function cleanDraftText(text) {
|
|
3402
3412
|
return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
|
|
3403
3413
|
}
|
|
3414
|
+
var andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
3404
3415
|
var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
3416
|
+
var VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
|
|
3417
|
+
function shortVariant(key) {
|
|
3418
|
+
const pairs = key.split(", ").map((p) => {
|
|
3419
|
+
const at = p.indexOf("=");
|
|
3420
|
+
return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
|
|
3421
|
+
});
|
|
3422
|
+
const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
|
|
3423
|
+
if (!kept.length) return key;
|
|
3424
|
+
const rank2 = (k) => {
|
|
3425
|
+
const i = VARIANT_FIRST.indexOf(k.toLowerCase());
|
|
3426
|
+
return i < 0 ? VARIANT_FIRST.length : i;
|
|
3427
|
+
};
|
|
3428
|
+
return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank2(a.k) - rank2(b.k) || a.i - b.i).map((p) => p.v).join(" ");
|
|
3429
|
+
}
|
|
3430
|
+
var shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
|
|
3405
3431
|
var DRAFT_LINE_MAX = 120;
|
|
3406
3432
|
function clipText(text, max) {
|
|
3407
3433
|
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
3408
3434
|
if (t.length <= max) return t;
|
|
3409
|
-
const
|
|
3410
|
-
const at =
|
|
3411
|
-
return (at > max * 0.5 ?
|
|
3435
|
+
const cut2 = t.slice(0, max + 1);
|
|
3436
|
+
const at = cut2.lastIndexOf(" ");
|
|
3437
|
+
return (at > max * 0.5 ? cut2.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
|
|
3412
3438
|
}
|
|
3413
3439
|
function sanitizeDraftLine(raw) {
|
|
3414
3440
|
if (typeof raw !== "string") return "";
|
|
@@ -3445,8 +3471,8 @@ function validateDocsTask(x) {
|
|
|
3445
3471
|
}
|
|
3446
3472
|
var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
|
|
3447
3473
|
function parseDocsReply(text) {
|
|
3448
|
-
const
|
|
3449
|
-
const body =
|
|
3474
|
+
const fenced2 = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
3475
|
+
const body = fenced2 ? fenced2[1] : text;
|
|
3450
3476
|
const start = body.indexOf("{");
|
|
3451
3477
|
const end = body.lastIndexOf("}");
|
|
3452
3478
|
if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
|
|
@@ -3769,6 +3795,16 @@ function parseRequestIssue(body) {
|
|
|
3769
3795
|
return void 0;
|
|
3770
3796
|
}
|
|
3771
3797
|
}
|
|
3798
|
+
function declineReasonProblems(reason) {
|
|
3799
|
+
const r = reason.trim();
|
|
3800
|
+
if (!r) return ["Say why, in one or two sentences: the requester reads it."];
|
|
3801
|
+
if (r.length < 10) return ["Say a bit more, so the requester knows why: at least 10 characters."];
|
|
3802
|
+
return [];
|
|
3803
|
+
}
|
|
3804
|
+
function declineComment(reason, by) {
|
|
3805
|
+
const quote2 = reason.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n");
|
|
3806
|
+
return [`Declined${by ? ` by @${by.replace(/^@/, "")}` : ""} in Bridge Studio.`, "", quote2, "", "<sub>Closed as not planned. Reopen the issue if things change.</sub>"].join("\n");
|
|
3807
|
+
}
|
|
3772
3808
|
function requestStatus(issue) {
|
|
3773
3809
|
const prs = issue.linkedPrs ?? [];
|
|
3774
3810
|
if (issue.state === "open") return prs.some((p) => p.state === "open") ? "in-progress" : "requested";
|
|
@@ -3905,9 +3941,9 @@ function serializePendingDrafts(p) {
|
|
|
3905
3941
|
function clip(text, max) {
|
|
3906
3942
|
const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
3907
3943
|
if (t.length <= max) return t;
|
|
3908
|
-
const
|
|
3909
|
-
const at =
|
|
3910
|
-
return (at > max * 0.5 ?
|
|
3944
|
+
const cut2 = t.slice(0, max + 1);
|
|
3945
|
+
const at = cut2.lastIndexOf(" ");
|
|
3946
|
+
return (at > max * 0.5 ? cut2.slice(0, at) : t.slice(0, max)).replace(/[\s,;:]+$/, "");
|
|
3911
3947
|
}
|
|
3912
3948
|
function sentences(text) {
|
|
3913
3949
|
const t = text.replace(/\s+/g, " ").trim();
|
|
@@ -3985,13 +4021,13 @@ function draftItemBrief(i) {
|
|
|
3985
4021
|
return [`Item ${i.id}: component docs${i.component ? ` of ${i.component}` : ""} from Bridge Studio (${i.docs.path ?? i.docs.slug}): ${what}.`, "", i.docs.prompt].join("\n");
|
|
3986
4022
|
}
|
|
3987
4023
|
const kind = i.kind ? REQUEST_KINDS.find((k) => k.value === i.kind)?.label : void 0;
|
|
3988
|
-
const
|
|
4024
|
+
const cell4 = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3989
4025
|
const lines2 = [`Item ${i.id}: ${i.scope === "request" ? "a component change request (a GitHub issue developers implement)" : "the description of a pull request with design token (variable) edits from Figma"}.`];
|
|
3990
4026
|
if (i.component) lines2.push(`Component: ${i.component}`);
|
|
3991
4027
|
if (kind) lines2.push(`Kind: ${kind}`);
|
|
3992
4028
|
if (i.components?.length) lines2.push(`Components with a change request: ${i.components.join(", ")}`);
|
|
3993
|
-
if (i.changes.length) lines2.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${
|
|
3994
|
-
if (i.variables?.length) lines2.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${
|
|
4029
|
+
if (i.changes.length) lines2.push("", "| What | Now | In Figma |", "| --- | --- | --- |", ...i.changes.map((c) => `| ${cell4(c.field)} | ${cell4(c.before) || "none"} | ${cell4(c.after) || "removed"} |`));
|
|
4030
|
+
if (i.variables?.length) lines2.push("", "| Token | Now | In Figma |", "| --- | --- | --- |", ...i.variables.map((v) => `| ${cell4(v.path)} | ${cell4(v.before) || "none"} | ${cell4(v.after) || "removed"} |`));
|
|
3995
4031
|
if (!i.changes.length && !i.variables?.length && !i.lines?.length) lines2.push("", "No change detected: rely on the notes and the images.");
|
|
3996
4032
|
if (i.draft.title || i.draft.summary) lines2.push("", "Starting draft, written from the changes only:", `Title: ${i.draft.title}`, `Summary: ${i.draft.summary}`);
|
|
3997
4033
|
if (i.notes?.trim()) lines2.push("", "The designer's notes (keep their intent):", i.notes.trim());
|
|
@@ -4157,6 +4193,14 @@ async function closeExplorationOnGitHub(gh, ref) {
|
|
|
4157
4193
|
return { number: p.number, url: p.url, branch: p.headRef };
|
|
4158
4194
|
}
|
|
4159
4195
|
|
|
4196
|
+
// ../core/src/requests-github.ts
|
|
4197
|
+
async function declineRequestOnGitHub(gh, n, reason, by) {
|
|
4198
|
+
const problems = declineReasonProblems(reason);
|
|
4199
|
+
if (problems.length) throw new GitHubError(422, problems.join(" "));
|
|
4200
|
+
await gh.addComment(n, declineComment(reason, by));
|
|
4201
|
+
await gh.updateIssue(n, { state: "closed", state_reason: "not_planned" });
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4160
4204
|
// ../core/src/review-gate.ts
|
|
4161
4205
|
function parseTier(labels, body) {
|
|
4162
4206
|
const found = [];
|
|
@@ -4614,309 +4658,325 @@ function branchWithJiraKey(branch, key) {
|
|
|
4614
4658
|
return i < 0 ? `${key}-${branch}` : `${branch.slice(0, i + 1)}${key}-${branch.slice(i + 1)}`;
|
|
4615
4659
|
}
|
|
4616
4660
|
|
|
4617
|
-
// ../core/src/
|
|
4618
|
-
var
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
};
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4661
|
+
// ../core/src/suggestion-review.ts
|
|
4662
|
+
var SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
|
|
4663
|
+
var SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
|
|
4664
|
+
var EVENTS = ["approve", "request-changes", "comment"];
|
|
4665
|
+
var ROLE_TEXT = { design: "for design", tech: "for tech", approval: "" };
|
|
4666
|
+
function composeSuggestionReviewComment(r) {
|
|
4667
|
+
const who = r.by ? ` by @${r.by.replace(/^@/, "")}` : "";
|
|
4668
|
+
const role = r.role ? ROLE_TEXT[r.role] : "";
|
|
4669
|
+
const head = r.event === "approve" ? `Approved${role ? ` ${role}` : ""}${who} in Bridge.` : r.event === "request-changes" ? `Changes requested${who} in Bridge.` : `Comment${who} in Bridge.`;
|
|
4670
|
+
const quote2 = r.message?.trim() ? r.message.trim().split("\n").map((l) => `> ${l}`.trimEnd()).join("\n") : "";
|
|
4671
|
+
const record = { event: r.event, ...r.role ? { role: r.role } : {}, payloadSha: r.payloadSha, at: r.at };
|
|
4672
|
+
return [head, ...quote2 ? ["", quote2] : [], "", `<sub>Applies to version \`${r.payloadSha.slice(0, 7)}\` of this suggestion.</sub>`, "", `<!-- ${SUGGESTION_REVIEW_MARK} ${JSON.stringify(record)} -->`].join("\n");
|
|
4673
|
+
}
|
|
4674
|
+
function parseSuggestionReviewComment(body) {
|
|
4675
|
+
const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4676
|
+
if (!m) return null;
|
|
4677
|
+
try {
|
|
4678
|
+
const r = JSON.parse(m[1]);
|
|
4679
|
+
if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
|
|
4680
|
+
return {
|
|
4681
|
+
event: r.event,
|
|
4682
|
+
...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
|
|
4683
|
+
payloadSha: r.payloadSha,
|
|
4684
|
+
at: typeof r.at === "string" ? r.at : ""
|
|
4685
|
+
};
|
|
4686
|
+
} catch {
|
|
4687
|
+
return null;
|
|
4688
|
+
}
|
|
4638
4689
|
}
|
|
4639
|
-
function
|
|
4640
|
-
const
|
|
4641
|
-
const
|
|
4642
|
-
return
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
dos: guide(d.dos),
|
|
4648
|
-
donts: guide(d.donts),
|
|
4649
|
-
accessibility: list(d.accessibility),
|
|
4650
|
-
contentGuidelines: list(d.contentGuidelines),
|
|
4651
|
-
anatomy: Object.fromEntries(Object.entries(d.anatomy ?? {}).filter(([k, v]) => k.trim() && v.trim()))
|
|
4652
|
-
};
|
|
4690
|
+
function composeSuggestionAppliedComment(r) {
|
|
4691
|
+
const where2 = r.pr ? `in pull request #${r.pr}` : r.branch ? `on \`${r.branch}\`` : r.commit ? `in commit ${r.commit.slice(0, 7)}` : "locally";
|
|
4692
|
+
const record = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== void 0));
|
|
4693
|
+
return [
|
|
4694
|
+
`Being applied ${where2}${r.by ? ` by @${r.by.replace(/^@/, "")}` : ""}.${r.forced ? " Applied before every approval was in." : ""}`,
|
|
4695
|
+
"",
|
|
4696
|
+
`<!-- ${SUGGESTION_APPLIED_MARK} ${JSON.stringify(record)} -->`
|
|
4697
|
+
].join("\n");
|
|
4653
4698
|
}
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4699
|
+
function parseSuggestionAppliedComment(body) {
|
|
4700
|
+
const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4701
|
+
if (!m) return null;
|
|
4702
|
+
try {
|
|
4703
|
+
const r = JSON.parse(m[1]);
|
|
4704
|
+
return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
|
|
4705
|
+
} catch {
|
|
4706
|
+
return null;
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
var norm2 = (l) => l.trim().replace(/^@/, "").toLowerCase();
|
|
4710
|
+
var COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
4711
|
+
function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
4712
|
+
const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
|
|
4713
|
+
const owners = new Set([...lists.design, ...lists.tech].map(norm2));
|
|
4714
|
+
const anyOwner = owners.size > 0;
|
|
4715
|
+
const reviews = [];
|
|
4716
|
+
const latestOld = /* @__PURE__ */ new Map();
|
|
4717
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
4718
|
+
const ordered2 = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
|
|
4719
|
+
ordered2.forEach(({ c }, i) => {
|
|
4720
|
+
const r = parseSuggestionReviewComment(c.body);
|
|
4721
|
+
if (!r || r.event === "comment" || !c.author) return;
|
|
4722
|
+
const login = norm2(c.author);
|
|
4723
|
+
if (opts.author && login === norm2(opts.author)) return;
|
|
4724
|
+
const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
|
|
4725
|
+
if (!counts) {
|
|
4726
|
+
ignored.add(c.author);
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
const at = String(i).padStart(6, "0");
|
|
4730
|
+
if (r.payloadSha !== opts.payloadSha) {
|
|
4731
|
+
latestOld.set(c.author, r.event);
|
|
4732
|
+
reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
|
|
4733
|
+
return;
|
|
4664
4734
|
}
|
|
4735
|
+
latestOld.delete(c.author);
|
|
4736
|
+
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
4665
4737
|
});
|
|
4666
|
-
|
|
4738
|
+
const summary2 = summarizeApprovals(reviews, lists, opts.author ?? void 0);
|
|
4739
|
+
const stale = [...latestOld.keys()].filter((l) => !summary2.approvers.includes(l) && !summary2.changesRequested.includes(l));
|
|
4740
|
+
return { ...summary2, stale, ignored: [...ignored].filter((l) => !summary2.approvers.includes(l)) };
|
|
4741
|
+
}
|
|
4742
|
+
function suggestionReviewState(tier, approvals) {
|
|
4743
|
+
if (approvals.changesRequested.length) return "changes-requested";
|
|
4744
|
+
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
4667
4745
|
}
|
|
4668
|
-
function withAuthorLine(body, line) {
|
|
4669
|
-
const i = body.lastIndexOf("\n---\n");
|
|
4670
|
-
return i < 0 ? `${body.trimEnd()}
|
|
4671
4746
|
|
|
4672
|
-
|
|
4673
|
-
|
|
4747
|
+
// ../core/src/figma-push.ts
|
|
4748
|
+
var where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
4749
|
+
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
4750
|
+
function planComponentPush(d, ctx) {
|
|
4751
|
+
const issues = [...d.unknown];
|
|
4752
|
+
const reasons = [];
|
|
4753
|
+
const edits = /* @__PURE__ */ new Map();
|
|
4754
|
+
const also = /* @__PURE__ */ new Set();
|
|
4755
|
+
for (const r of d.rebinds) {
|
|
4756
|
+
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
4757
|
+
if (!r.after.length) {
|
|
4758
|
+
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." });
|
|
4759
|
+
continue;
|
|
4760
|
+
}
|
|
4761
|
+
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
4762
|
+
if (missing.length) {
|
|
4763
|
+
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." });
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
if (!r.before.length) {
|
|
4767
|
+
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: Claude AI adds it to the CSS.`);
|
|
4768
|
+
continue;
|
|
4769
|
+
}
|
|
4770
|
+
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
4771
|
+
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so Claude AI writes the CSS.`);
|
|
4772
|
+
continue;
|
|
4773
|
+
}
|
|
4774
|
+
const [from, to] = [r.before[0], r.after[0]];
|
|
4775
|
+
if (ctx.tierOf(from) !== "component") {
|
|
4776
|
+
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: Claude AI changes the CSS to ${to}.`);
|
|
4777
|
+
continue;
|
|
4778
|
+
}
|
|
4779
|
+
const elsewhere = ctx.usedElsewhere(from, r);
|
|
4780
|
+
if (elsewhere.variants.length) {
|
|
4781
|
+
const v = elsewhere.variants;
|
|
4782
|
+
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
4783
|
+
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so Claude AI adds a token for ${where(r)}.`);
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
for (const c of elsewhere.components) also.add(c);
|
|
4787
|
+
const prev = edits.get(from);
|
|
4788
|
+
if (prev && prev.value !== `{${to}}`) {
|
|
4789
|
+
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): Claude AI splits it.`);
|
|
4790
|
+
edits.delete(from);
|
|
4791
|
+
continue;
|
|
4792
|
+
}
|
|
4793
|
+
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
4794
|
+
}
|
|
4795
|
+
const list = [...edits.values()];
|
|
4796
|
+
if (d.needsCode || reasons.length) {
|
|
4797
|
+
return {
|
|
4798
|
+
route: "claude-draft",
|
|
4799
|
+
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.` : ""}`,
|
|
4800
|
+
edits: list,
|
|
4801
|
+
reasons,
|
|
4802
|
+
issues,
|
|
4803
|
+
alsoChanges: [...also]
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
if (list.length) {
|
|
4807
|
+
const n = list.length;
|
|
4808
|
+
const others = [...also];
|
|
4809
|
+
const names3 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
4810
|
+
const alsoLine = names3.length ? ` It also repaints ${names3.length > 1 ? `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}` : names3[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
4811
|
+
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 };
|
|
4812
|
+
}
|
|
4813
|
+
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: [] };
|
|
4814
|
+
}
|
|
4815
|
+
function specsUsage(specs, component) {
|
|
4816
|
+
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
4817
|
+
return (path, r) => {
|
|
4818
|
+
const variants = [];
|
|
4819
|
+
const components = [];
|
|
4820
|
+
for (const c of specs.components) {
|
|
4821
|
+
for (const v of c.variants) {
|
|
4822
|
+
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
4823
|
+
const key = variantKeyOf(v.props);
|
|
4824
|
+
if (c === own) {
|
|
4825
|
+
if (!r.variants.includes(key)) variants.push(key);
|
|
4826
|
+
} else if (!components.includes(c.name)) components.push(c.name);
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
return { variants, components };
|
|
4830
|
+
};
|
|
4674
4831
|
}
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4832
|
+
function componentContrastPairs(edits, pairs, tokens) {
|
|
4833
|
+
const out = /* @__PURE__ */ new Map();
|
|
4834
|
+
for (const e of edits) {
|
|
4835
|
+
const target = tokens.aliasOf(e.path);
|
|
4836
|
+
if (!target) continue;
|
|
4837
|
+
const group = e.path.split(".")[0];
|
|
4838
|
+
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
4839
|
+
for (const p of pairs) {
|
|
4840
|
+
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
4841
|
+
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
return [...out.values()];
|
|
4679
4845
|
}
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4846
|
+
var FROM_FIGMA_LABEL = "from-figma";
|
|
4847
|
+
var WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
4848
|
+
var DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
4849
|
+
var FIGMA_SPEC_DIR = ".bridge/figma";
|
|
4850
|
+
var ORIGIN_MARK = "bridge-from-figma";
|
|
4851
|
+
var hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4852
|
+
function parseFigmaOrigin(body) {
|
|
4853
|
+
if (!body) return void 0;
|
|
4854
|
+
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4855
|
+
if (!m) return void 0;
|
|
4856
|
+
try {
|
|
4857
|
+
const o = JSON.parse(m[1]);
|
|
4858
|
+
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
4859
|
+
} catch {
|
|
4860
|
+
return void 0;
|
|
4861
|
+
}
|
|
4683
4862
|
}
|
|
4684
|
-
|
|
4685
|
-
|
|
4863
|
+
function figmaOriginSection(o) {
|
|
4864
|
+
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
4865
|
+
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
4866
|
+
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}.`;
|
|
4867
|
+
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");
|
|
4686
4868
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
const
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
await gh.getPullRequest(open.number).then((pr) => gh.updatePullRequest(open.number, { body: withAuthorLine(pr.body, line) })).catch(() => void 0);
|
|
4869
|
+
function withFigmaOrigin(body, o) {
|
|
4870
|
+
let b = (body ?? "").trim();
|
|
4871
|
+
const at = b.indexOf("**From Figma:**");
|
|
4872
|
+
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
4873
|
+
if (at >= 0 && mark > at) {
|
|
4874
|
+
const end = b.indexOf("-->", mark);
|
|
4875
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
4695
4876
|
}
|
|
4696
|
-
return
|
|
4697
|
-
}
|
|
4698
|
-
function docsProposal(edits, opts) {
|
|
4699
|
-
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
4700
|
-
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
4701
|
-
const names3 = edits.map((e) => e.component);
|
|
4702
|
-
const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
|
|
4703
|
-
const title = opts.title?.trim() || `docs(${names3.length === 1 ? edits[0].slug : "components"}): usage guidance for ${list}`;
|
|
4704
|
-
const slug2 = docsSlug(names3.length === 1 ? edits[0].slug : `${edits[0].slug}-and-${names3.length - 1}-more`);
|
|
4705
|
-
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
4706
|
-
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
4707
|
-
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
4708
|
-
const changeset = `---
|
|
4709
|
-
"${pkg}": patch
|
|
4710
|
-
---
|
|
4877
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
4711
4878
|
|
|
4712
|
-
|
|
4879
|
+
${b}` : ""}`;
|
|
4880
|
+
}
|
|
4881
|
+
var figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48) || "component-change";
|
|
4882
|
+
var figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
4883
|
+
var kindLabel2 = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
4884
|
+
function formatFigmaSpec(d, plan) {
|
|
4885
|
+
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
4713
4886
|
`;
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
const
|
|
4728
|
-
|
|
4887
|
+
}
|
|
4888
|
+
function parseFigmaSpec(text) {
|
|
4889
|
+
if (!text) return void 0;
|
|
4890
|
+
try {
|
|
4891
|
+
const s = JSON.parse(text);
|
|
4892
|
+
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
4893
|
+
} catch {
|
|
4894
|
+
return void 0;
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
function figmaDraftBrief(d, ctx) {
|
|
4898
|
+
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
4899
|
+
const suggested = "suggestion" in ctx;
|
|
4900
|
+
const variants = Object.entries(d.variantAxes).map(([axis, values]) => `${axis}: ${values.join(", ")}`);
|
|
4901
|
+
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
4902
|
+
return [
|
|
4903
|
+
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}).`,
|
|
4729
4904
|
"",
|
|
4730
|
-
|
|
4905
|
+
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
4906
|
+
`Kind: ${kindLabel2(d.kind)}`,
|
|
4907
|
+
`Change: ${d.title}`,
|
|
4908
|
+
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
4731
4909
|
"",
|
|
4732
|
-
|
|
4910
|
+
d.summary.trim(),
|
|
4733
4911
|
"",
|
|
4734
|
-
|
|
4912
|
+
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
4913
|
+
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
4914
|
+
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
4915
|
+
...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)}`), ""] : [],
|
|
4916
|
+
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
4917
|
+
`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.`,
|
|
4918
|
+
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
4919
|
+
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
4735
4920
|
"",
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
${
|
|
4739
|
-
|
|
4740
|
-
"
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4921
|
+
"Steps:",
|
|
4922
|
+
"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}`,
|
|
4923
|
+
`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"}.`,
|
|
4924
|
+
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
4925
|
+
"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.`,
|
|
4926
|
+
"",
|
|
4927
|
+
"Rules:",
|
|
4928
|
+
"- 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.",
|
|
4929
|
+
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
4930
|
+
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
4931
|
+
].join("\n");
|
|
4744
4932
|
}
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
4765
|
-
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
4766
|
-
return { number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
4933
|
+
function figmaDraftBody(d, plan, o) {
|
|
4934
|
+
const cell4 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
4935
|
+
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell4(c.field)} | ${cell4(c.before ?? "")} | ${cell4(c.after ?? "")} |`), ""] : [];
|
|
4936
|
+
return withFigmaOrigin(
|
|
4937
|
+
[
|
|
4938
|
+
`## ${d.coded ? `${kindLabel2(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
4939
|
+
"",
|
|
4940
|
+
d.summary.trim(),
|
|
4941
|
+
"",
|
|
4942
|
+
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
4943
|
+
...rows,
|
|
4944
|
+
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
4945
|
+
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
4946
|
+
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
4947
|
+
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
4948
|
+
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
4949
|
+
].join("\n"),
|
|
4950
|
+
o
|
|
4951
|
+
);
|
|
4767
4952
|
}
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
if (
|
|
4773
|
-
|
|
4953
|
+
var figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
4954
|
+
function figmaPushStatus(x) {
|
|
4955
|
+
const labels = x.labels ?? [];
|
|
4956
|
+
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
4957
|
+
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 };
|
|
4958
|
+
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 };
|
|
4959
|
+
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 };
|
|
4960
|
+
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
4774
4961
|
}
|
|
4775
4962
|
|
|
4776
|
-
// ../core/src/
|
|
4777
|
-
var
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
if (!
|
|
4784
|
-
const
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
if (!c) return { kind: "building" };
|
|
4791
|
-
if (c.sha7 && !x.headSha.startsWith(c.sha7)) return { kind: "building" };
|
|
4792
|
-
if (!c.command || c.source === "none") return { kind: "failed" };
|
|
4793
|
-
return { kind: "ready", command: c.command, source: c.source };
|
|
4794
|
-
}
|
|
4795
|
-
var stableCommand = (packages) => `npm i ${packages.map((p) => `${p}@latest`).join(" ")}`;
|
|
4796
|
-
function stateSentence(s) {
|
|
4797
|
-
switch (s.kind) {
|
|
4798
|
-
case "ready":
|
|
4799
|
-
return null;
|
|
4800
|
-
case "building":
|
|
4801
|
-
return "The preview version is being prepared. It is usually ready a couple of minutes after each update to the branch.";
|
|
4802
|
-
case "failed":
|
|
4803
|
-
return "The preview version could not be prepared for the latest update. Update the branch to try again, or ask a developer to run it again on GitHub.";
|
|
4804
|
-
case "no-packages":
|
|
4805
|
-
return "This branch does not touch the library tokens or components, so there is no preview version to install.";
|
|
4806
|
-
case "shipped":
|
|
4807
|
-
return s.stillWorks === true ? "This branch was merged. Its preview version still installs, and products get it with the next library release." : s.stillWorks === false ? "This branch was merged, so its preview version was removed. Products get it with the next library release." : "This branch was merged. Products get it with the next library release.";
|
|
4808
|
-
case "closed":
|
|
4809
|
-
return s.stillWorks === true ? "This branch was closed without merging. Its preview version still installs, but it will not reach the library." : s.stillWorks === false ? "This branch was closed without merging, so its preview version was removed." : "This branch was closed without merging.";
|
|
4810
|
-
}
|
|
4811
|
-
}
|
|
4812
|
-
var quote = (t) => `"${t.trim().replace(/"/g, "'")}"`;
|
|
4813
|
-
function shareMessage(x) {
|
|
4814
|
-
const back = stableCommand(x.packages);
|
|
4815
|
-
const s = x.state;
|
|
4816
|
-
if (s.kind === "ready") {
|
|
4817
|
-
const lasts = s.source === "pkg.pr.new" ? "This preview stays available. Each update to the change makes a new one: ask me for the latest command." : "This preview is replaced on each update to the change and removed when it ships or closes: ask me for the latest command if the install fails.";
|
|
4818
|
-
return {
|
|
4819
|
-
subject: `Try the design system change ${quote(x.title)} before it ships`,
|
|
4820
|
-
body: [
|
|
4821
|
-
"Hi,",
|
|
4822
|
-
"",
|
|
4823
|
-
`Could you try the design system change ${quote(x.title)} in our product before it ships?`,
|
|
4824
|
-
...x.tryUrl ? ["", `See it in the demo product first: ${x.tryUrl}`] : [],
|
|
4825
|
-
"",
|
|
4826
|
-
"Install the preview version in a staging branch (not production):",
|
|
4827
|
-
s.command,
|
|
4828
|
-
"",
|
|
4829
|
-
lasts,
|
|
4830
|
-
"",
|
|
4831
|
-
"To go back to the stable version:",
|
|
4832
|
-
back,
|
|
4833
|
-
"",
|
|
4834
|
-
`Details for developers: ${x.url}`
|
|
4835
|
-
].join("\n")
|
|
4836
|
-
};
|
|
4837
|
-
}
|
|
4838
|
-
if ((s.kind === "shipped" || s.kind === "closed") && s.command) {
|
|
4839
|
-
const what = s.kind === "shipped" ? `The design system change ${quote(x.title)} has shipped. You get it with the next library release.` : `The design system change ${quote(x.title)} was closed without shipping.`;
|
|
4840
|
-
const preview = s.stillWorks === false ? "Its preview version was removed, so reinstalling it fails." : s.stillWorks === true ? "Its preview version still installs, but it is not the released version." : "";
|
|
4841
|
-
return {
|
|
4842
|
-
subject: `Design system change ${quote(x.title)} ${s.kind === "shipped" ? "has shipped" : "was closed"}`,
|
|
4843
|
-
body: ["Hi,", "", [what, preview].filter(Boolean).join(" "), "", "If you installed its preview, go back to the stable version:", back, "", `Details for developers: ${x.url}`].join("\n")
|
|
4844
|
-
};
|
|
4845
|
-
}
|
|
4846
|
-
return null;
|
|
4847
|
-
}
|
|
4848
|
-
|
|
4849
|
-
// ../core/src/try-in-product.ts
|
|
4850
|
-
function tryInProductUrl(preview, pr, screen) {
|
|
4851
|
-
const url = new URL(preview);
|
|
4852
|
-
url.pathname = "/";
|
|
4853
|
-
url.search = "";
|
|
4854
|
-
url.searchParams.set("bridge", String(pr));
|
|
4855
|
-
if (screen) {
|
|
4856
|
-
url.searchParams.set("tenant", screen.tenant);
|
|
4857
|
-
url.searchParams.set("theme", screen.theme);
|
|
4858
|
-
}
|
|
4859
|
-
url.hash = screen ? screen.route : "";
|
|
4860
|
-
return url.href;
|
|
4861
|
-
}
|
|
4862
|
-
|
|
4863
|
-
// ../core/src/combine-branches.ts
|
|
4864
|
-
var MIN_TOGETHER = 2;
|
|
4865
|
-
var MAX_TOGETHER = 4;
|
|
4866
|
-
function onlyTokens(files, tokensRoot) {
|
|
4867
|
-
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
4868
|
-
return files.length > 0 && files.every((f) => f.startsWith(root) && f.endsWith(".tokens.json") || /contrast\.checks\.json$/.test(f) || f.startsWith(".changeset/"));
|
|
4869
|
-
}
|
|
4870
|
-
function tokenFilesOf(files, tokensRoot) {
|
|
4871
|
-
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
4872
|
-
return files.filter((f) => f.startsWith(root) && f.endsWith(".tokens.json")).map((f) => f.slice(root.length));
|
|
4873
|
-
}
|
|
4874
|
-
var changeList = (numbers) => numbers.join(",");
|
|
4875
|
-
function tryTogetherUrl(admin, numbers, picks) {
|
|
4876
|
-
const url = new URL(admin);
|
|
4877
|
-
url.pathname = "/";
|
|
4878
|
-
url.hash = "";
|
|
4879
|
-
const use = encodePicks(picks);
|
|
4880
|
-
url.search = `?bridge=${changeList(numbers)}${use ? `&bridge-use=${encodeURIComponent(use)}` : ""}`;
|
|
4881
|
-
return url.href;
|
|
4882
|
-
}
|
|
4883
|
-
var COMBINE_COMPARE_LIMIT = 300;
|
|
4884
|
-
async function readCombineBranch(gh, pull, opts) {
|
|
4885
|
-
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
4886
|
-
const files = compare.files;
|
|
4887
|
-
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
4888
|
-
const start = compare.mergeBase;
|
|
4889
|
-
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
|
|
4890
|
-
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
4891
|
-
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
4892
|
-
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
4893
|
-
await Promise.all(
|
|
4894
|
-
tokenFiles.map(async (f) => {
|
|
4895
|
-
const rel = tokenFilesOf([f.filename], root)[0];
|
|
4896
|
-
source.base[rel] = f.status === "added" ? void 0 : await gh.readFile(f.previous_filename ?? f.filename, start);
|
|
4897
|
-
source.head[rel] = f.status === "removed" ? void 0 : await gh.readFile(f.filename, pull.headSha);
|
|
4898
|
-
})
|
|
4899
|
-
);
|
|
4900
|
-
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
// ../core/src/prod-branch.ts
|
|
4904
|
-
var PIECES_MARK = "bridge-pieces";
|
|
4905
|
-
var NEEDED_MARK = "bridge-needed-by";
|
|
4906
|
-
var SECTION = "### Missing from the library";
|
|
4907
|
-
var NEEDED_BY_LABEL = "needed-by-product";
|
|
4908
|
-
var hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4909
|
-
var readHidden = (mark, body) => {
|
|
4910
|
-
if (!body) return void 0;
|
|
4911
|
-
const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4912
|
-
if (!m) return void 0;
|
|
4913
|
-
try {
|
|
4914
|
-
return JSON.parse(m[1]);
|
|
4915
|
-
} catch {
|
|
4916
|
-
return void 0;
|
|
4963
|
+
// ../core/src/prod-branch.ts
|
|
4964
|
+
var PIECES_MARK = "bridge-pieces";
|
|
4965
|
+
var NEEDED_MARK = "bridge-needed-by";
|
|
4966
|
+
var SECTION = "### Missing from the library";
|
|
4967
|
+
var NEEDED_BY_LABEL = "needed-by-product";
|
|
4968
|
+
var hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
4969
|
+
var readHidden = (mark, body) => {
|
|
4970
|
+
if (!body) return void 0;
|
|
4971
|
+
const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4972
|
+
if (!m) return void 0;
|
|
4973
|
+
try {
|
|
4974
|
+
return JSON.parse(m[1]);
|
|
4975
|
+
} catch {
|
|
4976
|
+
return void 0;
|
|
4917
4977
|
}
|
|
4918
4978
|
};
|
|
4919
|
-
var KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request" };
|
|
4979
|
+
var KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request", suggestion: "suggested token change" };
|
|
4920
4980
|
function parsePiecesRecord(body) {
|
|
4921
4981
|
const r = readHidden(PIECES_MARK, body);
|
|
4922
4982
|
if (!r || !Array.isArray(r.pieces) || !Array.isArray(r.links) || typeof r.checkedAt !== "string") return void 0;
|
|
@@ -4926,7 +4986,7 @@ function piecesSection(r) {
|
|
|
4926
4986
|
const link = (p) => r.links.find((l) => l.piece === p.id);
|
|
4927
4987
|
const lines2 = r.pieces.length ? r.pieces.map((p) => {
|
|
4928
4988
|
const l = link(p);
|
|
4929
|
-
return `- **${p.title}**: ${p.summary}${l ? ` ${
|
|
4989
|
+
return `- **${p.title}**: ${p.summary}${l ? ` ${cap2(KIND_WORD[l.kind])}: #${l.number}.` : " No library branch yet."}`;
|
|
4930
4990
|
}) : ["- Nothing: every value and component comes from the library."];
|
|
4931
4991
|
return [SECTION, "", `Checked ${r.checkedAt.slice(0, 10)}${r.headSha ? ` on ${r.headSha.slice(0, 7)}` : ""} by Bridge.`, "", ...lines2, "", hiddenJson(PIECES_MARK, r)].join("\n");
|
|
4932
4992
|
}
|
|
@@ -4969,7 +5029,7 @@ function withNeededBy(body, n) {
|
|
|
4969
5029
|
|
|
4970
5030
|
${b}` : ""}`;
|
|
4971
5031
|
}
|
|
4972
|
-
var
|
|
5032
|
+
var cap2 = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
4973
5033
|
var forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
|
|
4974
5034
|
function libraryBranchPlan(piece, branch, opts = {}) {
|
|
4975
5035
|
const neededBy = {
|
|
@@ -4993,11 +5053,23 @@ function libraryBranchPlan(piece, branch, opts = {}) {
|
|
|
4993
5053
|
path: t.path,
|
|
4994
5054
|
type: t.type,
|
|
4995
5055
|
value: t.value,
|
|
4996
|
-
description: `${
|
|
5056
|
+
description: `${cap2(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
|
|
4997
5057
|
...file ? { file } : {}
|
|
4998
5058
|
}));
|
|
4999
5059
|
return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
|
|
5000
5060
|
}
|
|
5061
|
+
if (piece.kind === "variant" && piece.component && opts.mode === "suggest") {
|
|
5062
|
+
const request2 = {
|
|
5063
|
+
component: piece.component,
|
|
5064
|
+
kind: "new-variant",
|
|
5065
|
+
title: piece.title,
|
|
5066
|
+
summary: `${forLine(branch)}: ${piece.summary}${near}`,
|
|
5067
|
+
changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
|
|
5068
|
+
source: opts.source ?? "mcp",
|
|
5069
|
+
...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
|
|
5070
|
+
};
|
|
5071
|
+
return { kind: "request", title: piece.title, request: request2, neededBy };
|
|
5072
|
+
}
|
|
5001
5073
|
if (piece.kind === "variant" && piece.component) {
|
|
5002
5074
|
const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
|
|
5003
5075
|
const exploration = {
|
|
@@ -5014,7 +5086,7 @@ function libraryBranchPlan(piece, branch, opts = {}) {
|
|
|
5014
5086
|
].filter(Boolean).join(" ");
|
|
5015
5087
|
return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
|
|
5016
5088
|
}
|
|
5017
|
-
const name = piece.component ??
|
|
5089
|
+
const name = piece.component ?? cap2(piece.name);
|
|
5018
5090
|
const request = {
|
|
5019
5091
|
component: name,
|
|
5020
5092
|
kind: "new-component",
|
|
@@ -5037,15 +5109,29 @@ var STATUS_LABEL = {
|
|
|
5037
5109
|
requested: "Requested",
|
|
5038
5110
|
"in-progress": "In progress",
|
|
5039
5111
|
done: "Done",
|
|
5040
|
-
declined: "Declined"
|
|
5112
|
+
declined: "Declined",
|
|
5113
|
+
outdated: "Outdated"
|
|
5041
5114
|
};
|
|
5042
5115
|
var names2 = (people2) => people2.length <= 2 ? people2.map((p) => `@${p}`).join(" and ") : `@${people2[0]} and ${people2.length - 1} more`;
|
|
5116
|
+
var SUGGESTION_LINK_STATUS = {
|
|
5117
|
+
"needs-review": "in-review",
|
|
5118
|
+
"changes-requested": "changes",
|
|
5119
|
+
approved: "approved",
|
|
5120
|
+
applying: "in-progress",
|
|
5121
|
+
applied: "shipped",
|
|
5122
|
+
declined: "declined",
|
|
5123
|
+
outdated: "outdated"
|
|
5124
|
+
};
|
|
5043
5125
|
function linkStatus(x) {
|
|
5044
5126
|
const make = (key, people2 = [], verb = "reviews") => {
|
|
5045
5127
|
const label = STATUS_LABEL[key];
|
|
5046
5128
|
const tail = people2.length ? ` \xB7 ${names2(people2)} ${people2.length === 1 ? verb : verb.replace(/s$/, "")}` : "";
|
|
5047
5129
|
return { key, label, people: people2, line: `${label}${tail}` };
|
|
5048
5130
|
};
|
|
5131
|
+
if (x.kind === "suggestion") {
|
|
5132
|
+
const key = SUGGESTION_LINK_STATUS[x.status];
|
|
5133
|
+
return make(key, key === "in-progress" ? [...x.assignees ?? []] : [], "builds");
|
|
5134
|
+
}
|
|
5049
5135
|
if (x.kind === "request") {
|
|
5050
5136
|
if (x.state === "closed") return make(x.stateReason === "not_planned" ? "declined" : "done");
|
|
5051
5137
|
return x.implemented ? make("in-progress", [...x.assignees ?? []], "builds") : make("requested", [...x.assignees ?? []], "builds");
|
|
@@ -5063,6 +5149,11 @@ function linkStatus(x) {
|
|
|
5063
5149
|
return make("in-review", waiting);
|
|
5064
5150
|
}
|
|
5065
5151
|
function libraryPreviewState(x) {
|
|
5152
|
+
if (x.kind === "suggestion") {
|
|
5153
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this suggestion.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
5154
|
+
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library applied the suggestion. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
5155
|
+
return { key: "nothing-yet", sentence: "Nothing to preview yet: owners review the suggestion, then a developer applies it. Your branch keeps its own value until then.", action: null };
|
|
5156
|
+
}
|
|
5066
5157
|
if (x.kind === "request") {
|
|
5067
5158
|
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this request.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
5068
5159
|
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library built it. Switch your branch to the published version, then ask Claude to use it.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
@@ -5114,129 +5205,1226 @@ function proposedTokenPath(nearest, name, category) {
|
|
|
5114
5205
|
return [...parts.slice(0, -1), slug2].join(".");
|
|
5115
5206
|
}
|
|
5116
5207
|
|
|
5117
|
-
// ../core/src/
|
|
5118
|
-
var
|
|
5119
|
-
var
|
|
5120
|
-
var
|
|
5121
|
-
var
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5208
|
+
// ../core/src/suggestions.ts
|
|
5209
|
+
var SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
5210
|
+
var SUGGESTION_LABEL = "bridge-suggestion";
|
|
5211
|
+
var SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
5212
|
+
var SUGGESTION_STATE_LABELS = {
|
|
5213
|
+
"needs-review": "suggestion: needs review",
|
|
5214
|
+
"changes-requested": "suggestion: changes requested",
|
|
5215
|
+
approved: "suggestion: approved",
|
|
5216
|
+
applying: "suggestion: applying"
|
|
5217
|
+
};
|
|
5218
|
+
var SUGGESTION_STATUS_LABEL = {
|
|
5219
|
+
"needs-review": "Needs review",
|
|
5220
|
+
"changes-requested": "Changes requested",
|
|
5221
|
+
approved: "Approved, ready to apply",
|
|
5222
|
+
applying: "Being applied",
|
|
5223
|
+
applied: "Applied",
|
|
5224
|
+
declined: "Declined",
|
|
5225
|
+
outdated: "Outdated"
|
|
5226
|
+
};
|
|
5227
|
+
var SUGGESTION_LIMITS = {
|
|
5228
|
+
body: 65536,
|
|
5229
|
+
/** The readable part of the body (tables are cut with "and N more"). */
|
|
5230
|
+
human: 12e3,
|
|
5231
|
+
/** The JSON block in the body. */
|
|
5232
|
+
inline: 48e3,
|
|
5233
|
+
/** The JSON block of one continuation comment. */
|
|
5234
|
+
part: 6e4,
|
|
5235
|
+
/** Continuation comments at most (about 480 KB of file contents). */
|
|
5236
|
+
maxParts: 8
|
|
5237
|
+
};
|
|
5238
|
+
var SuggestionError = class extends Error {
|
|
5239
|
+
constructor(code2, message) {
|
|
5240
|
+
super(message);
|
|
5241
|
+
this.code = code2;
|
|
5242
|
+
this.name = "SuggestionError";
|
|
5243
|
+
}
|
|
5244
|
+
code;
|
|
5245
|
+
};
|
|
5246
|
+
var SUGGESTION_TOO_LARGE = "Too large for a suggestion: split it, or ask a developer to open it directly.";
|
|
5247
|
+
var utf8 = (text) => new TextEncoder().encode(text);
|
|
5248
|
+
var hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
|
|
5249
|
+
function padded(bytes) {
|
|
5250
|
+
const len = bytes.length;
|
|
5251
|
+
const total = Math.ceil((len + 9) / 64) * 64;
|
|
5252
|
+
const buf = new Uint8Array(total);
|
|
5253
|
+
buf.set(bytes);
|
|
5254
|
+
buf[len] = 128;
|
|
5255
|
+
const view = new DataView(buf.buffer);
|
|
5256
|
+
view.setUint32(total - 8, Math.floor(len / 536870912), false);
|
|
5257
|
+
view.setUint32(total - 4, len * 8 >>> 0, false);
|
|
5258
|
+
return view;
|
|
5259
|
+
}
|
|
5260
|
+
var K256 = [
|
|
5261
|
+
1116352408,
|
|
5262
|
+
1899447441,
|
|
5263
|
+
3049323471,
|
|
5264
|
+
3921009573,
|
|
5265
|
+
961987163,
|
|
5266
|
+
1508970993,
|
|
5267
|
+
2453635748,
|
|
5268
|
+
2870763221,
|
|
5269
|
+
3624381080,
|
|
5270
|
+
310598401,
|
|
5271
|
+
607225278,
|
|
5272
|
+
1426881987,
|
|
5273
|
+
1925078388,
|
|
5274
|
+
2162078206,
|
|
5275
|
+
2614888103,
|
|
5276
|
+
3248222580,
|
|
5277
|
+
3835390401,
|
|
5278
|
+
4022224774,
|
|
5279
|
+
264347078,
|
|
5280
|
+
604807628,
|
|
5281
|
+
770255983,
|
|
5282
|
+
1249150122,
|
|
5283
|
+
1555081692,
|
|
5284
|
+
1996064986,
|
|
5285
|
+
2554220882,
|
|
5286
|
+
2821834349,
|
|
5287
|
+
2952996808,
|
|
5288
|
+
3210313671,
|
|
5289
|
+
3336571891,
|
|
5290
|
+
3584528711,
|
|
5291
|
+
113926993,
|
|
5292
|
+
338241895,
|
|
5293
|
+
666307205,
|
|
5294
|
+
773529912,
|
|
5295
|
+
1294757372,
|
|
5296
|
+
1396182291,
|
|
5297
|
+
1695183700,
|
|
5298
|
+
1986661051,
|
|
5299
|
+
2177026350,
|
|
5300
|
+
2456956037,
|
|
5301
|
+
2730485921,
|
|
5302
|
+
2820302411,
|
|
5303
|
+
3259730800,
|
|
5304
|
+
3345764771,
|
|
5305
|
+
3516065817,
|
|
5306
|
+
3600352804,
|
|
5307
|
+
4094571909,
|
|
5308
|
+
275423344,
|
|
5309
|
+
430227734,
|
|
5310
|
+
506948616,
|
|
5311
|
+
659060556,
|
|
5312
|
+
883997877,
|
|
5313
|
+
958139571,
|
|
5314
|
+
1322822218,
|
|
5315
|
+
1537002063,
|
|
5316
|
+
1747873779,
|
|
5317
|
+
1955562222,
|
|
5318
|
+
2024104815,
|
|
5319
|
+
2227730452,
|
|
5320
|
+
2361852424,
|
|
5321
|
+
2428436474,
|
|
5322
|
+
2756734187,
|
|
5323
|
+
3204031479,
|
|
5324
|
+
3329325298
|
|
5325
|
+
];
|
|
5326
|
+
function sha256Hex(text) {
|
|
5327
|
+
const view = padded(utf8(text));
|
|
5328
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
5329
|
+
const w = new Array(64);
|
|
5330
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
5331
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
5332
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
5333
|
+
for (let i = 16; i < 64; i++) {
|
|
5334
|
+
const a2 = w[i - 15];
|
|
5335
|
+
const b2 = w[i - 2];
|
|
5336
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
5337
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
5338
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
5339
|
+
}
|
|
5340
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
5341
|
+
for (let i = 0; i < 64; i++) {
|
|
5342
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
|
|
5343
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
5344
|
+
hh = g;
|
|
5345
|
+
g = f;
|
|
5346
|
+
f = e;
|
|
5347
|
+
e = d + t1 | 0;
|
|
5348
|
+
d = c;
|
|
5349
|
+
c = b;
|
|
5350
|
+
b = a;
|
|
5351
|
+
a = t1 + t2 | 0;
|
|
5352
|
+
}
|
|
5353
|
+
h[0] = h[0] + a | 0;
|
|
5354
|
+
h[1] = h[1] + b | 0;
|
|
5355
|
+
h[2] = h[2] + c | 0;
|
|
5356
|
+
h[3] = h[3] + d | 0;
|
|
5357
|
+
h[4] = h[4] + e | 0;
|
|
5358
|
+
h[5] = h[5] + f | 0;
|
|
5359
|
+
h[6] = h[6] + g | 0;
|
|
5360
|
+
h[7] = h[7] + hh | 0;
|
|
5361
|
+
}
|
|
5362
|
+
return hex(h);
|
|
5363
|
+
}
|
|
5364
|
+
function sha1Bytes(bytes) {
|
|
5365
|
+
const view = padded(bytes);
|
|
5366
|
+
const h = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
|
|
5367
|
+
const w = new Array(80);
|
|
5368
|
+
const rotl = (x, n) => x << n | x >>> 32 - n;
|
|
5369
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
5370
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
5371
|
+
for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
5372
|
+
let [a, b, c, d, e] = h;
|
|
5373
|
+
for (let i = 0; i < 80; i++) {
|
|
5374
|
+
const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
|
|
5375
|
+
const t = rotl(a, 5) + f + e + k + w[i] | 0;
|
|
5376
|
+
e = d;
|
|
5377
|
+
d = c;
|
|
5378
|
+
c = rotl(b, 30);
|
|
5379
|
+
b = a;
|
|
5380
|
+
a = t;
|
|
5381
|
+
}
|
|
5382
|
+
h[0] = h[0] + a | 0;
|
|
5383
|
+
h[1] = h[1] + b | 0;
|
|
5384
|
+
h[2] = h[2] + c | 0;
|
|
5385
|
+
h[3] = h[3] + d | 0;
|
|
5386
|
+
h[4] = h[4] + e | 0;
|
|
5387
|
+
}
|
|
5388
|
+
return hex(h);
|
|
5389
|
+
}
|
|
5390
|
+
function gitBlobSha(text) {
|
|
5391
|
+
const body = utf8(text);
|
|
5392
|
+
const head = utf8(`blob ${body.length}\0`);
|
|
5393
|
+
const all = new Uint8Array(head.length + body.length);
|
|
5394
|
+
all.set(head);
|
|
5395
|
+
all.set(body, head.length);
|
|
5396
|
+
return sha1Bytes(all);
|
|
5397
|
+
}
|
|
5398
|
+
function canonicalJson(value) {
|
|
5399
|
+
return JSON.stringify(sortKeys(value));
|
|
5400
|
+
}
|
|
5401
|
+
function sortKeys(v) {
|
|
5402
|
+
if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
|
|
5403
|
+
if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
|
|
5404
|
+
const out = {};
|
|
5405
|
+
for (const k of Object.keys(v).sort()) {
|
|
5406
|
+
const x = v[k];
|
|
5407
|
+
if (x !== void 0) out[k] = sortKeys(x);
|
|
5408
|
+
}
|
|
5409
|
+
return out;
|
|
5410
|
+
}
|
|
5411
|
+
return v;
|
|
5126
5412
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename));
|
|
5131
|
-
const dir = product.path ? `${cleanPath(product.path)}/` : "";
|
|
5132
|
-
const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
|
|
5133
|
-
const read = changed.slice(0, PIECES_FILE_LIMIT);
|
|
5134
|
-
const start = cmp.mergeBase ?? pull.baseRef;
|
|
5135
|
-
const files = await Promise.all(
|
|
5136
|
-
read.map(async (f) => {
|
|
5137
|
-
const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
|
|
5138
|
-
return { path: f.filename, before: before ?? null, after: after ?? null };
|
|
5139
|
-
})
|
|
5140
|
-
);
|
|
5141
|
-
return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
|
|
5413
|
+
function shaForm(p) {
|
|
5414
|
+
if (!p.files) return p;
|
|
5415
|
+
return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
|
|
5142
5416
|
}
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
|
|
5146
|
-
await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
|
|
5147
|
-
return record;
|
|
5417
|
+
function suggestionPayloadSha(p) {
|
|
5418
|
+
return sha256Hex(canonicalJson(shaForm(p)));
|
|
5148
5419
|
}
|
|
5149
|
-
var
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5420
|
+
var KIND_SUBJECT = {
|
|
5421
|
+
"token-change": "Tokens",
|
|
5422
|
+
docs: "Docs",
|
|
5423
|
+
"component-spec": "Component",
|
|
5424
|
+
brand: "Brand",
|
|
5425
|
+
revert: "Revert",
|
|
5426
|
+
config: "Settings"
|
|
5427
|
+
};
|
|
5428
|
+
function suggestionSubject(p) {
|
|
5429
|
+
if (p.kind === "component-spec" && p.componentSpec?.component) return p.componentSpec.component;
|
|
5430
|
+
if (p.kind === "docs" && p.docs?.length) {
|
|
5431
|
+
const names3 = [...new Set(p.docs.map((d) => d.component))];
|
|
5432
|
+
return names3.length === 1 ? names3[0] : KIND_SUBJECT.docs;
|
|
5160
5433
|
}
|
|
5434
|
+
return KIND_SUBJECT[p.kind];
|
|
5161
5435
|
}
|
|
5162
|
-
function
|
|
5163
|
-
const
|
|
5164
|
-
|
|
5165
|
-
let author = pull.author;
|
|
5166
|
-
if (name) {
|
|
5167
|
-
try {
|
|
5168
|
-
const e = JSON.parse(name[1]);
|
|
5169
|
-
if (e.name) title = e.name;
|
|
5170
|
-
if (e.author) author = e.author;
|
|
5171
|
-
} catch {
|
|
5172
|
-
}
|
|
5173
|
-
}
|
|
5174
|
-
return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {} };
|
|
5436
|
+
function suggestionIssueTitle(p) {
|
|
5437
|
+
const t = p.title.trim().replace(/\.$/, "");
|
|
5438
|
+
return `${suggestionSubject(p)}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
|
|
5175
5439
|
}
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
const
|
|
5194
|
-
|
|
5195
|
-
} else if (plan.kind === "exploration") {
|
|
5196
|
-
const r = await startExplorationOnGitHub(gh, { ...plan.exploration, ...input.openedBy ? { author: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} }, { base: input.base, ...input.exploration, labels: [NEEDED_BY_LABEL] });
|
|
5197
|
-
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
5198
|
-
} else {
|
|
5199
|
-
const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
|
|
5200
|
-
const r = await gh.createIssue(issue);
|
|
5201
|
-
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at, ...who };
|
|
5440
|
+
function suggestionLabels(p, opts = {}) {
|
|
5441
|
+
return [
|
|
5442
|
+
.../* @__PURE__ */ new Set([
|
|
5443
|
+
SUGGESTION_LABEL,
|
|
5444
|
+
`suggestion: ${p.kind}`,
|
|
5445
|
+
`source: ${p.source}`,
|
|
5446
|
+
`tier: ${p.tier.tier}`,
|
|
5447
|
+
SUGGESTION_STATE_LABELS[opts.state ?? "needs-review"],
|
|
5448
|
+
...p.links?.neededBy ? [NEEDED_BY_LABEL] : [],
|
|
5449
|
+
...p.links?.figma || p.source === "figma" ? [FROM_FIGMA_LABEL] : [],
|
|
5450
|
+
...p.links?.exploration ? [EXPLORATION_LABEL] : [],
|
|
5451
|
+
...opts.labels ?? []
|
|
5452
|
+
])
|
|
5453
|
+
];
|
|
5454
|
+
}
|
|
5455
|
+
function suggestionKindFromLabels(labels) {
|
|
5456
|
+
for (const l of labels) {
|
|
5457
|
+
const m = /^suggestion: (.+)$/.exec(l);
|
|
5458
|
+
if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
|
|
5202
5459
|
}
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5460
|
+
return void 0;
|
|
5461
|
+
}
|
|
5462
|
+
function suggestionStateFromLabels(labels) {
|
|
5463
|
+
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
5464
|
+
}
|
|
5465
|
+
function suggestionStatus(i) {
|
|
5466
|
+
const prs = i.linkedPrs ?? [];
|
|
5467
|
+
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
5468
|
+
if (prs.some((p) => p.state === "open") || i.applied) return "applying";
|
|
5469
|
+
if (i.outdated) return "outdated";
|
|
5470
|
+
return i.review ?? "needs-review";
|
|
5471
|
+
}
|
|
5472
|
+
function suggestionStateLabel(status) {
|
|
5473
|
+
return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
|
|
5474
|
+
}
|
|
5475
|
+
var bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
|
|
5476
|
+
var MARK3 = "bridge-suggestion";
|
|
5477
|
+
var PART_SCHEMA = "bridge.suggestion-part/1";
|
|
5478
|
+
var cell3 = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
5479
|
+
var safeText = (s) => s.replace(/<!--/g, "<!--");
|
|
5480
|
+
var sourceLabel2 = (s) => s === "figma" ? "Figma" : s === "studio" ? "Bridge Studio" : s === "cli" ? "the CLI" : "Claude (MCP)";
|
|
5481
|
+
var show2 = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
5482
|
+
function table(head, rows, max) {
|
|
5483
|
+
if (!rows.length) return [];
|
|
5484
|
+
const shown = rows.slice(0, max);
|
|
5485
|
+
return [
|
|
5486
|
+
`| ${head.join(" | ")} |`,
|
|
5487
|
+
`| ${head.map(() => "---").join(" | ")} |`,
|
|
5488
|
+
...shown.map((r) => `| ${r.map((c) => cell3(c)).join(" | ")} |`),
|
|
5489
|
+
...rows.length > max ? ["", `and ${rows.length - max} more`] : []
|
|
5490
|
+
];
|
|
5491
|
+
}
|
|
5492
|
+
function changesSection(p, max) {
|
|
5493
|
+
const out = [];
|
|
5494
|
+
if (p.tokens?.changes.length) {
|
|
5495
|
+
const rows = p.tokens.changes.map((c) => [
|
|
5496
|
+
`\`${c.path}\`${c.kind === "renamed" && c.from ? ` (was \`${c.from}\`)` : c.kind === "added" ? " (new)" : c.kind === "removed" ? " (removed)" : ""}`,
|
|
5497
|
+
c.beforeCss ?? show2(c.before),
|
|
5498
|
+
c.afterCss ?? show2(c.after)
|
|
5499
|
+
]);
|
|
5500
|
+
out.push(...table(["Token", "Now", "Suggested"], rows, max), "");
|
|
5501
|
+
} else if (p.tokens?.edits.length) {
|
|
5502
|
+
const rows = p.tokens.edits.map((e) => [`\`${e.path}\``, e.op === "rename" ? `rename to \`${e.newPath ?? ""}\`` : e.op, show2(e.value)]);
|
|
5503
|
+
out.push(...table(["Token", "Edit", "Value"], rows, max), "");
|
|
5215
5504
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
5222
|
-
warnings.push(`Your branch does not use the preview yet: ${err.message}`);
|
|
5223
|
-
return null;
|
|
5224
|
-
});
|
|
5505
|
+
if (p.docs?.length) out.push(...table(["Component", "Docs file"], p.docs.map((d) => [d.component, `\`${d.path}\``]), max), "");
|
|
5506
|
+
if (p.componentSpec) out.push(`**Component spec:** ${p.componentSpec.component} (\`${p.componentSpec.specPath}\`)`, "");
|
|
5507
|
+
if (p.files?.length) {
|
|
5508
|
+
const rows = p.files.map((f) => [`\`${f.path}\``, f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed"]);
|
|
5509
|
+
out.push(...table(["File", "Change"], rows, max), "");
|
|
5225
5510
|
}
|
|
5226
|
-
return
|
|
5511
|
+
return out;
|
|
5227
5512
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5513
|
+
function checksSection(p, max) {
|
|
5514
|
+
const c = p.checks;
|
|
5515
|
+
const failing = c.contrast.filter((x) => !x.pass);
|
|
5516
|
+
const rows = [...failing, ...c.contrast.filter((x) => x.pass)].map((x) => [x.pair, x.mode ?? "", x.before.toFixed(2), x.after.toFixed(2), String(x.min), x.pass ? "pass" : "**fails**"]);
|
|
5517
|
+
return [
|
|
5518
|
+
"## Checks when suggested",
|
|
5519
|
+
"",
|
|
5520
|
+
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
5521
|
+
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
5522
|
+
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one approval" : "design and tech approval"}`,
|
|
5523
|
+
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
5524
|
+
"",
|
|
5525
|
+
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
5526
|
+
];
|
|
5527
|
+
}
|
|
5528
|
+
function linksSection(p) {
|
|
5529
|
+
const l = p.links ?? {};
|
|
5530
|
+
const out = [];
|
|
5531
|
+
if (l.neededBy) out.push(`- Needed by ${l.neededBy.productName}: [#${l.neededBy.pr} ${cell3(l.neededBy.title)}](${l.neededBy.url})`);
|
|
5532
|
+
if (l.figma?.url) out.push(`- Design: [Open in Figma](${l.figma.url})`);
|
|
5533
|
+
if (l.exploration) out.push(`- Exploration: \`${l.exploration}\``);
|
|
5534
|
+
if (l.supersedes) out.push(`- Replaces suggestion #${l.supersedes}`);
|
|
5535
|
+
for (const v of p.previews ?? []) out.push(`- ${cell3(v.label)}: ${v.url}`);
|
|
5536
|
+
return out.length ? ["## Previews and links", "", ...out, ""] : [];
|
|
5537
|
+
}
|
|
5538
|
+
function humanPart(p, details, max, repo) {
|
|
5539
|
+
return [
|
|
5540
|
+
`## Suggested change: ${safeText(p.title.trim())}`,
|
|
5541
|
+
"",
|
|
5542
|
+
safeText(p.summary.trim()),
|
|
5543
|
+
"",
|
|
5544
|
+
...changesSection(p, max),
|
|
5545
|
+
...details?.trim() ? [safeText(details.trim()), ""] : [],
|
|
5546
|
+
...checksSection(p, max),
|
|
5547
|
+
...linksSection(p),
|
|
5548
|
+
"## How to apply",
|
|
5549
|
+
"",
|
|
5550
|
+
"- CLI: `bridge apply <this issue number>` (add `--dry-run` to check it first, `--patch` for a patch file)",
|
|
5551
|
+
'- Claude Code with the Bridge MCP server: ask "Apply Bridge suggestion #<this issue number>" (tool `bridge_apply_suggestion`).',
|
|
5552
|
+
` One-time install: \`${bridgeMcpInstallCommand(repo)}\``,
|
|
5553
|
+
"",
|
|
5554
|
+
"The pull request that applies it must say `Closes #<this issue number>`, so this suggestion closes when it merges.",
|
|
5555
|
+
"",
|
|
5556
|
+
`<sub>Suggested from ${sourceLabel2(p.source)}${p.suggestedBy ? ` by @${p.suggestedBy.replace(/^@/, "")}` : ""} with Bridge.</sub>`
|
|
5557
|
+
].join("\n");
|
|
5558
|
+
}
|
|
5559
|
+
function fitHuman(p, details, repo) {
|
|
5560
|
+
for (const max of [50, 20, 8, 3]) {
|
|
5561
|
+
const h2 = humanPart(p, details, max, repo);
|
|
5562
|
+
if (h2.length <= SUGGESTION_LIMITS.human) return h2;
|
|
5563
|
+
}
|
|
5564
|
+
const h = humanPart(p, void 0, 3, repo);
|
|
5565
|
+
if (h.length <= SUGGESTION_LIMITS.human) return h;
|
|
5566
|
+
const note = "\n\n_The description is cut here: the full change is in the machine-readable block below._";
|
|
5567
|
+
return h.slice(0, SUGGESTION_LIMITS.human - note.length) + note;
|
|
5568
|
+
}
|
|
5569
|
+
var pretty = (v) => JSON.stringify(sortKeys(v), null, 2);
|
|
5570
|
+
var fit = (v) => {
|
|
5571
|
+
const p = pretty(v);
|
|
5572
|
+
return p.length <= SUGGESTION_LIMITS.inline ? p : canonicalJson(v);
|
|
5573
|
+
};
|
|
5574
|
+
var fenced = (json) => ["```json", json, "```"].join("\n");
|
|
5575
|
+
function payloadBlock(json, sha, parts) {
|
|
5576
|
+
return [
|
|
5577
|
+
"<details><summary>Machine-readable change (Bridge suggestion v1)</summary>",
|
|
5578
|
+
"",
|
|
5579
|
+
`<!-- ${MARK3}:v1 sha256=${sha}${parts ? ` parts=${parts}` : ""} -->`,
|
|
5580
|
+
fenced(json),
|
|
5581
|
+
"",
|
|
5582
|
+
"</details>"
|
|
5583
|
+
].join("\n");
|
|
5584
|
+
}
|
|
5585
|
+
function partComment(i, n, json) {
|
|
5586
|
+
return [
|
|
5587
|
+
`<details><summary>Machine-readable change, part ${i} of ${n} (Bridge suggestion v1)</summary>`,
|
|
5588
|
+
"",
|
|
5589
|
+
`<!-- ${MARK3}:v1 part ${i}/${n} sha256=${sha256Hex(json)} -->`,
|
|
5590
|
+
fenced(json),
|
|
5591
|
+
"",
|
|
5592
|
+
"</details>"
|
|
5593
|
+
].join("\n");
|
|
5594
|
+
}
|
|
5595
|
+
function escapedEnd(s, from, budget) {
|
|
5596
|
+
let used = 0;
|
|
5597
|
+
let i = from;
|
|
5598
|
+
while (i < s.length) {
|
|
5599
|
+
const c = s.charCodeAt(i);
|
|
5600
|
+
let w = 1;
|
|
5601
|
+
let len = 1;
|
|
5602
|
+
if (c === 34 || c === 92 || c === 8 || c === 12 || c === 10 || c === 13 || c === 9) len = 2;
|
|
5603
|
+
else if (c < 32) len = 6;
|
|
5604
|
+
else if (c >= 55296 && c <= 56319) {
|
|
5605
|
+
const d = s.charCodeAt(i + 1);
|
|
5606
|
+
if (d >= 56320 && d <= 57343) {
|
|
5607
|
+
w = 2;
|
|
5608
|
+
len = 2;
|
|
5609
|
+
} else len = 6;
|
|
5610
|
+
} else if (c >= 56320 && c <= 57343) len = 6;
|
|
5611
|
+
if (used + len > budget) break;
|
|
5612
|
+
used += len;
|
|
5613
|
+
i += w;
|
|
5614
|
+
}
|
|
5615
|
+
return i;
|
|
5616
|
+
}
|
|
5617
|
+
function composeSuggestionIssue(payload, opts = {}) {
|
|
5618
|
+
const problems = suggestionShapeProblems(payload);
|
|
5619
|
+
if (problems.length) throw new SuggestionError("invalid", problems.join(" "));
|
|
5620
|
+
if (payload.checks.blocked) throw new SuggestionError("invalid", `This change cannot be suggested: ${payload.checks.blocked}`);
|
|
5621
|
+
const full = shaForm(payload);
|
|
5622
|
+
const sha = suggestionPayloadSha(full);
|
|
5623
|
+
const human = fitHuman(full, opts.details, opts.repo);
|
|
5624
|
+
const base = { title: suggestionIssueTitle(full), labels: suggestionLabels(full, { state: opts.state, labels: opts.labels }), assignees: opts.assignees ?? [], sha };
|
|
5625
|
+
const inline = fit(full);
|
|
5626
|
+
if (inline.length <= SUGGESTION_LIMITS.inline) {
|
|
5627
|
+
return { ...base, body: `${human}
|
|
5628
|
+
|
|
5629
|
+
${payloadBlock(inline, sha, 0)}
|
|
5630
|
+
`, parts: [] };
|
|
5631
|
+
}
|
|
5632
|
+
const moved = (full.files ?? []).map((f, i) => ({ f, i })).filter(({ f }) => typeof f.content === "string" && f.content.length > 0);
|
|
5633
|
+
const parts = [];
|
|
5634
|
+
const firstPart = /* @__PURE__ */ new Map();
|
|
5635
|
+
const partJson = (chunks, i, n2) => pretty({ schema: PART_SCHEMA, part: i, of: n2, files: chunks });
|
|
5636
|
+
const emptyOverhead = partJson([], 9, 9).length;
|
|
5637
|
+
let cur = [];
|
|
5638
|
+
let room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
5639
|
+
for (const { f, i } of moved) {
|
|
5640
|
+
const text = f.content;
|
|
5641
|
+
let at = 0;
|
|
5642
|
+
while (at < text.length) {
|
|
5643
|
+
const entryOverhead = partJson([{ i, path: f.path, text: "" }], 9, 9).length - emptyOverhead + 8;
|
|
5644
|
+
if (room - entryOverhead < 256) {
|
|
5645
|
+
parts.push(cur);
|
|
5646
|
+
cur = [];
|
|
5647
|
+
room = SUGGESTION_LIMITS.part - emptyOverhead - 64;
|
|
5648
|
+
}
|
|
5649
|
+
const end = escapedEnd(text, at, room - entryOverhead);
|
|
5650
|
+
if (end === at) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5651
|
+
if (!firstPart.has(i)) firstPart.set(i, parts.length + 1);
|
|
5652
|
+
const chunk = { i, path: f.path, text: text.slice(at, end) };
|
|
5653
|
+
cur.push(chunk);
|
|
5654
|
+
room -= partJson([chunk], 9, 9).length - emptyOverhead + 8;
|
|
5655
|
+
at = end;
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
if (cur.length) parts.push(cur);
|
|
5659
|
+
if (parts.length > SUGGESTION_LIMITS.maxParts) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5660
|
+
const skeleton = {
|
|
5661
|
+
...full,
|
|
5662
|
+
files: (full.files ?? []).map((f, i) => {
|
|
5663
|
+
if (!firstPart.has(i)) return f;
|
|
5664
|
+
const { content: _content, ...rest } = f;
|
|
5665
|
+
return { ...rest, part: firstPart.get(i) };
|
|
5666
|
+
})
|
|
5667
|
+
};
|
|
5668
|
+
const skeletonJson = fit(skeleton);
|
|
5669
|
+
if (skeletonJson.length > SUGGESTION_LIMITS.inline) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5670
|
+
const n = parts.length;
|
|
5671
|
+
const partBodies = parts.map((chunks, k) => {
|
|
5672
|
+
const json = partJson(chunks, k + 1, n);
|
|
5673
|
+
if (json.length > SUGGESTION_LIMITS.part) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5674
|
+
return partComment(k + 1, n, json);
|
|
5675
|
+
});
|
|
5676
|
+
const body = `${human}
|
|
5677
|
+
|
|
5678
|
+
${payloadBlock(skeletonJson, sha, n)}
|
|
5679
|
+
`;
|
|
5680
|
+
if (body.length > SUGGESTION_LIMITS.body || partBodies.some((b) => b.length > SUGGESTION_LIMITS.body)) throw new SuggestionError("too-large", SUGGESTION_TOO_LARGE);
|
|
5681
|
+
return { ...base, body, parts: partBodies };
|
|
5682
|
+
}
|
|
5683
|
+
function fencedAfter(text, from) {
|
|
5684
|
+
const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
|
|
5685
|
+
return m?.[1];
|
|
5686
|
+
}
|
|
5687
|
+
var fail = (reason, message) => ({ ok: false, reason, message });
|
|
5688
|
+
function parseSuggestionIssue(body, opts = {}) {
|
|
5689
|
+
const text = body ?? "";
|
|
5690
|
+
const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK3}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
|
|
5691
|
+
const marker = markers[markers.length - 1];
|
|
5692
|
+
if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
|
|
5693
|
+
if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
|
|
5694
|
+
const json = fencedAfter(text, marker.index + marker[0].length);
|
|
5695
|
+
if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
|
|
5696
|
+
let raw;
|
|
5697
|
+
try {
|
|
5698
|
+
raw = JSON.parse(json);
|
|
5699
|
+
} catch {
|
|
5700
|
+
return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
|
|
5701
|
+
}
|
|
5702
|
+
const schema = raw?.schema;
|
|
5703
|
+
if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
|
|
5704
|
+
const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
|
|
5705
|
+
return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
|
|
5706
|
+
}
|
|
5707
|
+
const problems = suggestionShapeProblems(raw);
|
|
5708
|
+
if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
|
|
5709
|
+
const payload = raw;
|
|
5710
|
+
const markedSha = marker[2];
|
|
5711
|
+
const expected = marker[3] ? Number(marker[3]) : 0;
|
|
5712
|
+
let edited = false;
|
|
5713
|
+
if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
|
|
5714
|
+
const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
|
|
5715
|
+
const found = /* @__PURE__ */ new Map();
|
|
5716
|
+
const author = opts.author?.toLowerCase();
|
|
5717
|
+
for (const c of opts.comments ?? []) {
|
|
5718
|
+
if (author && c.author && c.author.toLowerCase() !== author) continue;
|
|
5719
|
+
const m = new RegExp(`^<!-- ${MARK3}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
|
|
5720
|
+
if (!m) continue;
|
|
5721
|
+
const i = Number(m[1]);
|
|
5722
|
+
if (found.has(i)) continue;
|
|
5723
|
+
const pj = fencedAfter(c.body, m.index + m[0].length);
|
|
5724
|
+
if (pj === void 0) continue;
|
|
5725
|
+
if (sha256Hex(pj) !== m[3]) edited = true;
|
|
5726
|
+
try {
|
|
5727
|
+
const part = JSON.parse(pj);
|
|
5728
|
+
if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
|
|
5729
|
+
found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
|
|
5730
|
+
} catch {
|
|
5731
|
+
continue;
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
for (let i = 1; i <= want; i++) {
|
|
5735
|
+
if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
|
|
5736
|
+
}
|
|
5737
|
+
const files = (payload.files ?? []).map((f, idx) => {
|
|
5738
|
+
if (f.part === void 0) return f;
|
|
5739
|
+
let content = "";
|
|
5740
|
+
for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
|
|
5741
|
+
const { part: _part, ...rest } = f;
|
|
5742
|
+
return { ...rest, content };
|
|
5743
|
+
});
|
|
5744
|
+
payload.files = files;
|
|
5745
|
+
}
|
|
5746
|
+
const sha = suggestionPayloadSha(payload);
|
|
5747
|
+
return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
|
|
5748
|
+
}
|
|
5749
|
+
function suggestionShapeProblems(raw) {
|
|
5750
|
+
const out = [];
|
|
5751
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
|
|
5752
|
+
const p = raw;
|
|
5753
|
+
if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
|
|
5754
|
+
if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
|
|
5755
|
+
if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
|
|
5756
|
+
if (typeof p.summary !== "string") out.push('"summary" must be text.');
|
|
5757
|
+
if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
|
|
5758
|
+
if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
|
|
5759
|
+
const base = p.base;
|
|
5760
|
+
if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
|
|
5761
|
+
const tier = p.tier;
|
|
5762
|
+
if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
|
|
5763
|
+
const checks = p.checks;
|
|
5764
|
+
if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
|
|
5765
|
+
out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
|
|
5766
|
+
const tokens = p.tokens;
|
|
5767
|
+
if (tokens !== void 0 && (!tokens || !Array.isArray(tokens.edits) || !Array.isArray(tokens.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
5768
|
+
if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
|
|
5769
|
+
const spec = p.componentSpec;
|
|
5770
|
+
if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
|
|
5771
|
+
if (p.files !== void 0) {
|
|
5772
|
+
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
5773
|
+
else {
|
|
5774
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5775
|
+
for (const f of p.files) {
|
|
5776
|
+
const ok2 = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
|
|
5777
|
+
if (!ok2) {
|
|
5778
|
+
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
5779
|
+
break;
|
|
5780
|
+
}
|
|
5781
|
+
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
5782
|
+
seen.add(f.path);
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
return out;
|
|
5787
|
+
}
|
|
5788
|
+
function unsafePathProblem(path) {
|
|
5789
|
+
if (typeof path !== "string" || !path.length) return "A path is empty.";
|
|
5790
|
+
if (/[\0-\x1f\x7f]/.test(path)) return `"${path.replace(/[\0-\x1f\x7f]/g, "?")}" has control characters.`;
|
|
5791
|
+
if (path.includes("\\")) return `"${path}" uses backslashes: paths use forward slashes.`;
|
|
5792
|
+
if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || path.startsWith("~")) return `"${path}" is absolute: paths are relative to the repository root.`;
|
|
5793
|
+
const segs = path.split("/");
|
|
5794
|
+
if (segs.some((s) => s === "" || s === "." || s === "..")) return `"${path}" has an empty, "." or ".." part: paths stay inside the repository.`;
|
|
5795
|
+
if (segs.some((s) => s.toLowerCase() === ".git")) return `"${path}" is inside .git.`;
|
|
5796
|
+
return null;
|
|
5797
|
+
}
|
|
5798
|
+
var under = (path, root) => {
|
|
5799
|
+
const r = root.replace(/\/+$/, "");
|
|
5800
|
+
return !r || r === "." || path === r || path.startsWith(`${r}/`);
|
|
5801
|
+
};
|
|
5802
|
+
function isGuardrailPath(path, guardrails) {
|
|
5803
|
+
return guardrails.some((g) => g.endsWith("/") ? path.startsWith(g) || path === g.slice(0, -1) : path === g);
|
|
5804
|
+
}
|
|
5805
|
+
function suggestionAllowedRoots(kind, config) {
|
|
5806
|
+
const tokenRoot2 = tokensPath(config) || ".";
|
|
5807
|
+
const changesets = config.changesets?.dir ? [config.changesets.dir] : [];
|
|
5808
|
+
switch (kind) {
|
|
5809
|
+
case "token-change":
|
|
5810
|
+
return [tokenRoot2, ...changesets];
|
|
5811
|
+
case "docs":
|
|
5812
|
+
return [...config.components?.dir ? [config.components.dir] : [], ...changesets];
|
|
5813
|
+
case "component-spec":
|
|
5814
|
+
return [".bridge/figma"];
|
|
5815
|
+
case "brand":
|
|
5816
|
+
return [tokenRoot2, ...changesets, CONFIG_FILE];
|
|
5817
|
+
case "config":
|
|
5818
|
+
return [CONFIG_FILE];
|
|
5819
|
+
case "revert":
|
|
5820
|
+
return void 0;
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
function suggestionPathProblems(p, config) {
|
|
5824
|
+
const out = [];
|
|
5825
|
+
const roots = suggestionAllowedRoots(p.kind, config);
|
|
5826
|
+
const guardrails = config.governance.guardrails;
|
|
5827
|
+
const check = (path) => {
|
|
5828
|
+
const bad = unsafePathProblem(path);
|
|
5829
|
+
if (bad) return out.push(bad);
|
|
5830
|
+
if (roots && !roots.some((r) => under(path, r))) return out.push(`"${path}" is outside what a ${p.kind} suggestion may change (${roots.join(", ")}).`);
|
|
5831
|
+
const configAllowed = path === CONFIG_FILE && (p.kind === "config" || p.kind === "brand");
|
|
5832
|
+
if (!configAllowed && isGuardrailPath(path, guardrails)) return out.push(`"${path}" is a guardrail file: change it in a pull request, not a suggestion.`);
|
|
5833
|
+
return 0;
|
|
5834
|
+
};
|
|
5835
|
+
for (const f of p.files ?? []) check(f.path);
|
|
5836
|
+
for (const d of p.docs ?? []) check(d.path);
|
|
5837
|
+
if (p.componentSpec) check(p.componentSpec.specPath);
|
|
5838
|
+
for (const e of p.tokens?.edits ?? []) {
|
|
5839
|
+
if (e.file === void 0) continue;
|
|
5840
|
+
const bad = unsafePathProblem(e.file);
|
|
5841
|
+
if (bad) out.push(bad);
|
|
5842
|
+
}
|
|
5843
|
+
return [...new Set(out)];
|
|
5844
|
+
}
|
|
5845
|
+
|
|
5846
|
+
// ../core/src/suggestions-github.ts
|
|
5847
|
+
async function createSuggestionOnGitHub(gh, payload, opts = {}) {
|
|
5848
|
+
const issue = composeSuggestionIssue(payload, { repo: gh.fullName, ...opts });
|
|
5849
|
+
const created = await gh.createIssue({ title: issue.title, body: issue.body, labels: issue.labels, assignees: issue.assignees });
|
|
5850
|
+
for (const [i, part] of issue.parts.entries()) {
|
|
5851
|
+
try {
|
|
5852
|
+
await gh.createIssueComment(created.number, part);
|
|
5853
|
+
} catch (err) {
|
|
5854
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
5855
|
+
throw new GitHubError(err instanceof GitHubError ? err.status : 500, `Suggestion #${created.number} is incomplete: part ${i + 1} of ${issue.parts.length} could not be posted (${why}). Send it again, then close #${created.number}.`);
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
const warnings = [];
|
|
5859
|
+
const missing = issue.labels.filter((l) => !created.labels.includes(l));
|
|
5860
|
+
if (created.labels.length && missing.length) warnings.push(`GitHub did not keep these labels on #${created.number}: ${missing.join(", ")}.`);
|
|
5861
|
+
return { number: created.number, url: created.url, sha: issue.sha, parts: issue.parts.length, warnings };
|
|
5862
|
+
}
|
|
5863
|
+
function linkedPullsOf(n, pulls) {
|
|
5864
|
+
return pulls.filter((p) => closingIssueNumbers(p.body).includes(n)).map((p) => ({ number: p.number, url: p.url, state: p.state === "open" ? "open" : "closed", merged: p.state === "merged" }));
|
|
5865
|
+
}
|
|
5866
|
+
async function listSuggestionsOnGitHub(gh, opts = {}) {
|
|
5867
|
+
const issues = await gh.listIssues({ label: SUGGESTION_LABEL, state: opts.state ?? "open", perPage: opts.perPage ?? 50 });
|
|
5868
|
+
const pulls = opts.pulls ?? (issues.length ? await gh.listPullRequests("all").catch(() => []) : []);
|
|
5869
|
+
return issues.map((i) => listItem(i, pulls));
|
|
5870
|
+
}
|
|
5871
|
+
function listItem(i, pulls) {
|
|
5872
|
+
const linkedPrs = linkedPullsOf(i.number, pulls);
|
|
5873
|
+
const cached = suggestionStateFromLabels(i.labels);
|
|
5874
|
+
const t = i.labels.map((l) => /^tier: ([123])$/.exec(l)?.[1]).find(Boolean);
|
|
5875
|
+
return {
|
|
5876
|
+
number: i.number,
|
|
5877
|
+
title: i.title,
|
|
5878
|
+
url: i.url,
|
|
5879
|
+
author: i.author,
|
|
5880
|
+
createdAt: i.createdAt,
|
|
5881
|
+
state: i.state,
|
|
5882
|
+
stateReason: i.stateReason,
|
|
5883
|
+
labels: i.labels,
|
|
5884
|
+
kind: suggestionKindFromLabels(i.labels),
|
|
5885
|
+
tier: t ? Number(t) : void 0,
|
|
5886
|
+
linkedPrs,
|
|
5887
|
+
status: suggestionStatus({ state: i.state, stateReason: i.stateReason, linkedPrs, applied: cached === "applying", review: cached === "applying" ? void 0 : cached })
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
5891
|
+
const [issue, comments, pulls] = await Promise.all([gh.getIssue(n), gh.listIssueComments(n), opts.pulls ? Promise.resolve(opts.pulls) : gh.listPullRequests("all").catch(() => [])]);
|
|
5892
|
+
const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
|
|
5893
|
+
const linkedPrs = linkedPullsOf(n, pulls);
|
|
5894
|
+
const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
|
|
5895
|
+
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
|
|
5896
|
+
const status = suggestionStatus({
|
|
5897
|
+
state: issue.state,
|
|
5898
|
+
stateReason: issue.stateReason,
|
|
5899
|
+
linkedPrs,
|
|
5900
|
+
applied: parsed.ok && applied.some((a) => a.payloadSha === parsed.sha),
|
|
5901
|
+
outdated: opts.outdated,
|
|
5902
|
+
review: parsed.ok && approvals ? suggestionReviewState(parsed.payload.tier.tier, approvals) : void 0
|
|
5903
|
+
});
|
|
5904
|
+
return { issue, parsed, comments, approvals, linkedPrs, applied, status };
|
|
5905
|
+
}
|
|
5906
|
+
async function syncSuggestionStateLabel(gh, n, labels, status) {
|
|
5907
|
+
const want = suggestionStateLabel(status);
|
|
5908
|
+
let ok2 = true;
|
|
5909
|
+
for (const l of Object.values(SUGGESTION_STATE_LABELS)) {
|
|
5910
|
+
if (l === want || !labels.includes(l)) continue;
|
|
5911
|
+
try {
|
|
5912
|
+
await gh.removeLabel(n, l);
|
|
5913
|
+
} catch {
|
|
5914
|
+
ok2 = false;
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
if (want && !labels.includes(want)) ok2 = await gh.addLabels(n, [want]) && ok2;
|
|
5918
|
+
return ok2 ? null : `The state label on #${n} could not be updated; the status shown in Bridge is still right.`;
|
|
5919
|
+
}
|
|
5920
|
+
async function reviewSuggestionOnGitHub(gh, n, input, gov) {
|
|
5921
|
+
if (input.event === "request-changes" && !input.message?.trim()) throw new GitHubError(422, "Say what should change, in a sentence or two: the author reads it.");
|
|
5922
|
+
const before = await getSuggestionOnGitHub(gh, n, gov);
|
|
5923
|
+
if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be reviewed: ${before.parsed.message}`);
|
|
5924
|
+
if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed. Reopen it on GitHub to review it again.`);
|
|
5925
|
+
const sha = before.parsed.sha;
|
|
5926
|
+
if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and review the new version.`);
|
|
5927
|
+
const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
|
|
5928
|
+
const body = composeSuggestionReviewComment({ event: input.event, role: input.role, payloadSha: sha, at: (input.at ?? /* @__PURE__ */ new Date()).toISOString(), by: by || void 0, message: input.message });
|
|
5929
|
+
const posted = await gh.createIssueComment(n, body);
|
|
5930
|
+
const after = await getSuggestionOnGitHub(gh, n, gov);
|
|
5931
|
+
const status = after.status;
|
|
5932
|
+
const who = (after.comments.find((c) => c.id === posted.id)?.author ?? by).toLowerCase();
|
|
5933
|
+
const counts = input.event === "comment" || [...after.approvals?.approvers ?? [], ...after.approvals?.changesRequested ?? []].some((l) => l.toLowerCase() === who);
|
|
5934
|
+
const warnings = [];
|
|
5935
|
+
if (!counts) warnings.push("Your review is posted but does not count: only the owners listed in bridge.config.json approve suggestions.");
|
|
5936
|
+
if (before.parsed.edited) warnings.push(`#${n} was edited by hand after Bridge wrote it: check the machine-readable block before applying it.`);
|
|
5937
|
+
const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, status);
|
|
5938
|
+
if (labelWarning) warnings.push(labelWarning);
|
|
5939
|
+
return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
|
|
5940
|
+
}
|
|
5941
|
+
async function linkSuggestionPrOnGitHub(gh, n, link) {
|
|
5942
|
+
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.");
|
|
5943
|
+
const issue = await gh.getIssue(n);
|
|
5944
|
+
await gh.addComment(n, composeSuggestionAppliedComment({ by: link.by, branch: link.branch, commit: link.commit, pr: link.pr, payloadSha: link.payloadSha, forced: link.forced, at: (link.at ?? /* @__PURE__ */ new Date()).toISOString() }));
|
|
5945
|
+
const warning = await syncSuggestionStateLabel(gh, n, issue.labels, "applying");
|
|
5946
|
+
return { warnings: warning ? [warning] : [] };
|
|
5947
|
+
}
|
|
5948
|
+
async function declineSuggestionOnGitHub(gh, n, reason, by) {
|
|
5949
|
+
const issue = await gh.getIssue(n);
|
|
5950
|
+
await declineRequestOnGitHub(gh, n, reason, by);
|
|
5951
|
+
const warning = await syncSuggestionStateLabel(gh, n, issue.labels, "declined");
|
|
5952
|
+
return { warnings: warning ? [warning] : [] };
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5955
|
+
// ../core/src/docs-proposal.ts
|
|
5956
|
+
var DOCS_EDITOR_ORIGIN = {
|
|
5957
|
+
where: "in the Bridge docs editor",
|
|
5958
|
+
footer: "Written by a person in the Bridge docs editor. No AI involved in this text.",
|
|
5959
|
+
label: "source: bridge-web"
|
|
5960
|
+
};
|
|
5961
|
+
var DOCS_MCP_ORIGIN = {
|
|
5962
|
+
where: "with Claude through the Bridge MCP server",
|
|
5963
|
+
footer: 'Proposed with the Bridge MCP server on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
5964
|
+
label: "source: mcp"
|
|
5965
|
+
};
|
|
5966
|
+
var EM_DASH = /\u2014/;
|
|
5967
|
+
function validateDocs(d) {
|
|
5968
|
+
const out = [];
|
|
5969
|
+
if (!d.summary.trim()) out.push({ field: "summary", message: "Add a summary: one or two sentences on what it is for." });
|
|
5970
|
+
const lists = ["whenToUse", "whenNotToUse", "accessibility", "contentGuidelines"];
|
|
5971
|
+
for (const k of lists) (d[k] ?? []).forEach((x, i) => !x.trim() && out.push({ field: `${k}.${i}`, message: "Fill or remove the empty item." }));
|
|
5972
|
+
for (const k of ["dos", "donts"]) d[k].forEach((g, i) => (!g.title.trim() || !g.text.trim()) && out.push({ field: `${k}.${i}`, message: "Give every item a title and a sentence." }));
|
|
5973
|
+
const prose = JSON.stringify([d.summary, d.whenToUse, d.whenNotToUse, d.dos, d.donts, d.accessibility, d.contentGuidelines ?? [], d.anatomy ?? {}]);
|
|
5974
|
+
if (EM_DASH.test(prose)) out.push({ field: "voice", message: "Replace the em dashes with a colon, a comma or two sentences." });
|
|
5975
|
+
return out;
|
|
5976
|
+
}
|
|
5977
|
+
function cleanDocs(d) {
|
|
5978
|
+
const list = (xs) => (xs ?? []).map((x) => x.trim()).filter(Boolean);
|
|
5979
|
+
const guide = (xs) => xs.map((g) => ({ ...g, title: g.title.trim(), text: g.text.trim() })).filter((g) => g.title || g.text);
|
|
5980
|
+
return {
|
|
5981
|
+
...d,
|
|
5982
|
+
summary: d.summary.trim(),
|
|
5983
|
+
whenToUse: list(d.whenToUse),
|
|
5984
|
+
whenNotToUse: list(d.whenNotToUse),
|
|
5985
|
+
dos: guide(d.dos),
|
|
5986
|
+
donts: guide(d.donts),
|
|
5987
|
+
accessibility: list(d.accessibility),
|
|
5988
|
+
contentGuidelines: list(d.contentGuidelines),
|
|
5989
|
+
anatomy: Object.fromEntries(Object.entries(d.anatomy ?? {}).filter(([k, v]) => k.trim() && v.trim()))
|
|
5990
|
+
};
|
|
5991
|
+
}
|
|
5992
|
+
var docsFileText = (d, original) => patchDocsJson(original, cleanDocs(d));
|
|
5993
|
+
var slugDate = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
|
|
5994
|
+
var docsSlug = (nameOrSlug) => nameOrSlug.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 40);
|
|
5995
|
+
function defaultDocsFooter(edits, origin) {
|
|
5996
|
+
if (origin.label !== DOCS_EDITOR_ORIGIN.label) return origin.footer;
|
|
5997
|
+
const claude = edits.some((e) => {
|
|
5998
|
+
try {
|
|
5999
|
+
return JSON.parse(e.after).draftedBy === "claude";
|
|
6000
|
+
} catch {
|
|
6001
|
+
return false;
|
|
6002
|
+
}
|
|
6003
|
+
});
|
|
6004
|
+
return claude ? `Drafted by Claude AI (the docs say "draftedBy": "claude"), sent by a person ${origin.where}.` : origin.footer;
|
|
6005
|
+
}
|
|
6006
|
+
function withAuthorLine(body, line) {
|
|
6007
|
+
const i = body.lastIndexOf("\n---\n");
|
|
6008
|
+
return i < 0 ? `${body.trimEnd()}
|
|
6009
|
+
|
|
6010
|
+
---
|
|
6011
|
+
${line}` : `${body.slice(0, i + 5)}${line}`;
|
|
6012
|
+
}
|
|
6013
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6014
|
+
function isDocsProposalBranch(branch, component) {
|
|
6015
|
+
const slugs = /* @__PURE__ */ new Set([docsSlug(component.slug ?? component.name), docsSlug(component.name), docsSlug(component.name.toLowerCase())]);
|
|
6016
|
+
return [...slugs].some((s) => new RegExp(`^docs/(?:[A-Z][A-Z0-9]*-\\d+-)?${escapeRe(s)}-\\d{8}-[a-z0-9]+$`).test(branch));
|
|
6017
|
+
}
|
|
6018
|
+
function findOpenDocsProposal(pulls, component, base) {
|
|
6019
|
+
const hit = pulls.filter((p) => p.state === "open" && isDocsProposalBranch(p.headRef, component) && (!base || !p.baseRef || p.baseRef === base)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
6020
|
+
return hit ? { number: hit.number, url: hit.url, branch: hit.headRef, author: hit.author, title: hit.title, updatedAt: hit.updatedAt } : null;
|
|
6021
|
+
}
|
|
6022
|
+
async function findOpenDocsProposalOnGitHub(gh, component, base) {
|
|
6023
|
+
return findOpenDocsProposal(await gh.listPullRequests("open"), component, base);
|
|
6024
|
+
}
|
|
6025
|
+
async function continueDocsProposalOnGitHub(gh, open, edit, opts = {}) {
|
|
6026
|
+
const message = opts.message ?? `docs(${docsSlug(edit.component)}): edit usage guidance in the Bridge docs editor`;
|
|
6027
|
+
const cur = await gh.getFile(edit.path, open.branch);
|
|
6028
|
+
if (cur?.text === edit.after) return "";
|
|
6029
|
+
const sha = await gh.putFile({ path: edit.path, content: edit.after, branch: open.branch, message, sha: cur?.sha });
|
|
6030
|
+
if (opts.authorLine) {
|
|
6031
|
+
const line = opts.authorLine;
|
|
6032
|
+
await gh.getPullRequest(open.number).then((pr) => gh.updatePullRequest(open.number, { body: withAuthorLine(pr.body, line) })).catch(() => void 0);
|
|
6033
|
+
}
|
|
6034
|
+
return sha;
|
|
6035
|
+
}
|
|
6036
|
+
function docsProposal(edits, opts) {
|
|
6037
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
6038
|
+
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
6039
|
+
const names3 = edits.map((e) => e.component);
|
|
6040
|
+
const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
|
|
6041
|
+
const title = opts.title?.trim() || `docs(${names3.length === 1 ? edits[0].slug : "components"}): usage guidance for ${list}`;
|
|
6042
|
+
const slug2 = docsSlug(names3.length === 1 ? edits[0].slug : `${edits[0].slug}-and-${names3.length - 1}-more`);
|
|
6043
|
+
const branch = `docs/${slug2}-${slugDate(opts.date)}-${opts.date.getTime().toString(36).slice(-4)}`;
|
|
6044
|
+
const csPath = `${opts.changesetDir ?? ".changeset"}/docs-${slug2}-${opts.date.getTime().toString(36)}.md`;
|
|
6045
|
+
const pkg = opts.packageName ?? "@bridge-ds/react";
|
|
6046
|
+
const changeset = `---
|
|
6047
|
+
"${pkg}": patch
|
|
6048
|
+
---
|
|
6049
|
+
|
|
6050
|
+
Usage guidance for ${list}.
|
|
6051
|
+
`;
|
|
6052
|
+
const files = [...edits.map((e) => ({ path: e.path, content: e.after })), { path: csPath, content: changeset }];
|
|
6053
|
+
const risk = classifyRisk({ files: files.map((f) => ({ path: f.path, status: "modified" })) });
|
|
6054
|
+
const rows = edits.map((e) => {
|
|
6055
|
+
let state = "edited";
|
|
6056
|
+
try {
|
|
6057
|
+
const d = JSON.parse(e.after);
|
|
6058
|
+
const gaps = missingProse(d);
|
|
6059
|
+
state = gaps.length ? `still missing: ${gaps.join(", ")}` : "every section filled";
|
|
6060
|
+
} catch {
|
|
6061
|
+
state = "not valid JSON";
|
|
6062
|
+
}
|
|
6063
|
+
return `- \`${e.path}\` (${e.before === null ? "new" : "changed"}, ${state})`;
|
|
6064
|
+
});
|
|
6065
|
+
const body = [
|
|
6066
|
+
`## Docs change`,
|
|
6067
|
+
"",
|
|
6068
|
+
`Usage guidance for ${list}, written ${origin.where}.`,
|
|
6069
|
+
"",
|
|
6070
|
+
...rows,
|
|
6071
|
+
"",
|
|
6072
|
+
`**Risk:** ${risk.label}. Docs only: the published components do not change. Changeset: patch.`,
|
|
6073
|
+
"",
|
|
6074
|
+
opts.notes?.trim() ? `## Notes
|
|
6075
|
+
|
|
6076
|
+
${opts.notes.trim()}
|
|
6077
|
+
` : "",
|
|
6078
|
+
"---",
|
|
6079
|
+
opts.authorLine?.trim() || defaultDocsFooter(edits, origin)
|
|
6080
|
+
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
6081
|
+
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
6082
|
+
}
|
|
6083
|
+
var docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : "studio";
|
|
6084
|
+
function docsSuggestionPayload(edits, opts) {
|
|
6085
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
6086
|
+
const date = opts.date ?? /* @__PURE__ */ new Date();
|
|
6087
|
+
const p = docsProposal(edits, { title: opts.title, date, origin, packageName: opts.packageName, changesetDir: opts.changesetDir });
|
|
6088
|
+
const names3 = edits.map((e) => e.component);
|
|
6089
|
+
const list = names3.length === 1 ? names3[0] : `${names3.slice(0, -1).join(", ")} and ${names3[names3.length - 1]}`;
|
|
6090
|
+
const docs = edits.map((e) => {
|
|
6091
|
+
let after;
|
|
6092
|
+
try {
|
|
6093
|
+
after = JSON.parse(e.after);
|
|
6094
|
+
} catch {
|
|
6095
|
+
throw new Error(`${e.path} is not valid JSON: fix it before sending.`);
|
|
6096
|
+
}
|
|
6097
|
+
return { component: e.component, path: e.path, after: cleanDocs(after) };
|
|
6098
|
+
});
|
|
6099
|
+
const docPaths = new Set(edits.map((e) => e.path));
|
|
6100
|
+
const files = p.files.filter((f) => !docPaths.has(f.path)).map((f) => ({ path: f.path, beforeSha: null, afterSha: gitBlobSha(f.content), content: f.content }));
|
|
6101
|
+
const by = opts.suggestedBy?.replace(/^@/, "");
|
|
6102
|
+
return {
|
|
6103
|
+
schema: SUGGESTION_SCHEMA,
|
|
6104
|
+
kind: "docs",
|
|
6105
|
+
title: opts.title?.trim() || (names3.length === 1 ? "Usage guidance" : `Usage guidance for ${list}`),
|
|
6106
|
+
summary: [`Usage guidance for ${list}, written ${origin.where}.`, opts.notes?.trim() ?? ""].filter(Boolean).join("\n\n"),
|
|
6107
|
+
source: opts.source ?? docsSource(origin),
|
|
6108
|
+
...by ? { suggestedBy: by } : {},
|
|
6109
|
+
createdAt: date.toISOString(),
|
|
6110
|
+
base: opts.base,
|
|
6111
|
+
tier: { tier: p.risk.tier, bump: "patch" },
|
|
6112
|
+
docs,
|
|
6113
|
+
files,
|
|
6114
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 }
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
async function suggestDocsOnGitHub(gh, edits, opts) {
|
|
6118
|
+
if (!edits.length) throw new Error("No docs edits in the draft.");
|
|
6119
|
+
const sha = opts.baseSha ?? await gh.getBranchSha(opts.base);
|
|
6120
|
+
if (!sha) throw new Error(`Branch ${opts.base} not found.`);
|
|
6121
|
+
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
6122
|
+
const payload = docsSuggestionPayload(edits, { base: { ref: opts.base, sha }, title: opts.title, notes: opts.notes, date: opts.date, origin, source: opts.source, suggestedBy: opts.suggestedBy });
|
|
6123
|
+
const p = docsProposal(edits, { date: opts.date ?? /* @__PURE__ */ new Date(), origin });
|
|
6124
|
+
const rows = p.body.split("\n").filter((l) => l.startsWith("- `"));
|
|
6125
|
+
const details = [...rows, "", opts.authorLine?.trim() || defaultDocsFooter(edits, origin)].join("\n");
|
|
6126
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels: ["docs", ...opts.extraLabels ?? []], details });
|
|
6127
|
+
return { status: "suggested", number: created.number, url: created.url, title: payload.title, sha: created.sha, warnings: created.warnings };
|
|
6128
|
+
}
|
|
6129
|
+
async function proposeDocsOnGitHub(gh, edits, opts) {
|
|
6130
|
+
if (opts.mode === "suggest") return suggestDocsOnGitHub(gh, edits, opts);
|
|
6131
|
+
if (opts.continueOpen !== false && edits.length === 1) {
|
|
6132
|
+
const e = edits[0];
|
|
6133
|
+
const open = await findOpenDocsProposalOnGitHub(gh, { name: e.component, slug: e.slug }, opts.base).catch(() => null);
|
|
6134
|
+
if (open) {
|
|
6135
|
+
const sha = await continueDocsProposalOnGitHub(gh, open, e, { authorLine: opts.authorLine?.trim() || defaultDocsFooter(edits, opts.origin ?? DOCS_EDITOR_ORIGIN) });
|
|
6136
|
+
return { status: "continued", number: open.number, url: open.url, branch: open.branch, title: open.title, continued: true, unchanged: !sha };
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
const p = docsProposal(edits, { title: opts.title, notes: opts.notes, date: opts.date ?? /* @__PURE__ */ new Date(), origin: opts.origin, authorLine: opts.authorLine });
|
|
6140
|
+
const prTitle = titleWithJiraKey(p.title, opts.jira ?? null);
|
|
6141
|
+
const branch = branchWithJiraKey(p.branch, opts.jira ?? null);
|
|
6142
|
+
const mainSha = await gh.getBranchSha(opts.base);
|
|
6143
|
+
if (!mainSha) throw new Error(`Branch ${opts.base} not found.`);
|
|
6144
|
+
const tree = await gh.createTree(await gh.getCommitTree(mainSha), p.files.map((f) => ({ path: f.path, content: f.content })));
|
|
6145
|
+
const commit = await gh.createCommit(`${prTitle}
|
|
6146
|
+
|
|
6147
|
+
${opts.commitNote ?? "Proposed with the Bridge docs editor."}`, tree, [mainSha]);
|
|
6148
|
+
await gh.createBranch(branch, commit);
|
|
6149
|
+
const pr = await gh.createPullRequest({ title: prTitle, head: branch, base: opts.base, body: p.body, draft: false });
|
|
6150
|
+
await gh.addLabels(pr.number, [...p.labels, ...opts.extraLabels ?? []]).catch(() => false);
|
|
6151
|
+
return { status: "opened", number: pr.number, url: pr.url, branch, title: prTitle, continued: false, unchanged: false };
|
|
6152
|
+
}
|
|
6153
|
+
async function commitFileOnGitHub(gh, input) {
|
|
6154
|
+
const cur = await gh.getFile(input.path, input.branch);
|
|
6155
|
+
if (!cur) throw new Error(`${input.path} is not on ${input.branch}. Check the component name and the branch.`);
|
|
6156
|
+
const content = input.next(cur.text);
|
|
6157
|
+
if (content === cur.text) return "";
|
|
6158
|
+
return gh.putFile({ path: input.path, content, branch: input.branch, message: input.message, sha: cur.sha });
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
// ../core/src/share-branch.ts
|
|
6162
|
+
var packageDir = (name) => `packages/${name.replace(/^@[^/]+\//, "")}/`;
|
|
6163
|
+
function touchesPackages(files, packages) {
|
|
6164
|
+
const dirs = packages.map(packageDir);
|
|
6165
|
+
return files.some((f) => dirs.some((d) => f.startsWith(d)));
|
|
6166
|
+
}
|
|
6167
|
+
function shareState(x) {
|
|
6168
|
+
if (!touchesPackages(x.files, x.packages)) return { kind: "no-packages" };
|
|
6169
|
+
const c = readPreviewComment(x.comment);
|
|
6170
|
+
if (x.lifecycle === "merged" || x.lifecycle === "closed") {
|
|
6171
|
+
const kind = x.lifecycle === "merged" ? "shipped" : "closed";
|
|
6172
|
+
if (!c || !c.command || c.source === "none") return { kind, stillWorks: null, command: null };
|
|
6173
|
+
return { kind, stillWorks: c.source === "pkg.pr.new" ? true : c.source === "release" ? false : null, command: c.command };
|
|
6174
|
+
}
|
|
6175
|
+
if (!c) return { kind: "building" };
|
|
6176
|
+
if (c.sha7 && !x.headSha.startsWith(c.sha7)) return { kind: "building" };
|
|
6177
|
+
if (!c.command || c.source === "none") return { kind: "failed" };
|
|
6178
|
+
return { kind: "ready", command: c.command, source: c.source };
|
|
6179
|
+
}
|
|
6180
|
+
var stableCommand = (packages) => `npm i ${packages.map((p) => `${p}@latest`).join(" ")}`;
|
|
6181
|
+
function stateSentence(s) {
|
|
6182
|
+
switch (s.kind) {
|
|
6183
|
+
case "ready":
|
|
6184
|
+
return null;
|
|
6185
|
+
case "building":
|
|
6186
|
+
return "The preview version is being prepared. It is usually ready a couple of minutes after each update to the branch.";
|
|
6187
|
+
case "failed":
|
|
6188
|
+
return "The preview version could not be prepared for the latest update. Update the branch to try again, or ask a developer to run it again on GitHub.";
|
|
6189
|
+
case "no-packages":
|
|
6190
|
+
return "This branch does not touch the library tokens or components, so there is no preview version to install.";
|
|
6191
|
+
case "shipped":
|
|
6192
|
+
return s.stillWorks === true ? "This branch was merged. Its preview version still installs, and products get it with the next library release." : s.stillWorks === false ? "This branch was merged, so its preview version was removed. Products get it with the next library release." : "This branch was merged. Products get it with the next library release.";
|
|
6193
|
+
case "closed":
|
|
6194
|
+
return s.stillWorks === true ? "This branch was closed without merging. Its preview version still installs, but it will not reach the library." : s.stillWorks === false ? "This branch was closed without merging, so its preview version was removed." : "This branch was closed without merging.";
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
var quote = (t) => `"${t.trim().replace(/"/g, "'")}"`;
|
|
6198
|
+
function shareMessage(x) {
|
|
6199
|
+
const back = stableCommand(x.packages);
|
|
6200
|
+
const s = x.state;
|
|
6201
|
+
if (s.kind === "ready") {
|
|
6202
|
+
const lasts = s.source === "pkg.pr.new" ? "This preview stays available. Each update to the change makes a new one: ask me for the latest command." : "This preview is replaced on each update to the change and removed when it ships or closes: ask me for the latest command if the install fails.";
|
|
6203
|
+
return {
|
|
6204
|
+
subject: `Try the design system change ${quote(x.title)} before it ships`,
|
|
6205
|
+
body: [
|
|
6206
|
+
"Hi,",
|
|
6207
|
+
"",
|
|
6208
|
+
`Could you try the design system change ${quote(x.title)} in our product before it ships?`,
|
|
6209
|
+
...x.tryUrl ? ["", `See it in the demo product first: ${x.tryUrl}`] : [],
|
|
6210
|
+
"",
|
|
6211
|
+
"Install the preview version in a staging branch (not production):",
|
|
6212
|
+
s.command,
|
|
6213
|
+
"",
|
|
6214
|
+
lasts,
|
|
6215
|
+
"",
|
|
6216
|
+
"To go back to the stable version:",
|
|
6217
|
+
back,
|
|
6218
|
+
"",
|
|
6219
|
+
`Details for developers: ${x.url}`
|
|
6220
|
+
].join("\n")
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
if ((s.kind === "shipped" || s.kind === "closed") && s.command) {
|
|
6224
|
+
const what = s.kind === "shipped" ? `The design system change ${quote(x.title)} has shipped. You get it with the next library release.` : `The design system change ${quote(x.title)} was closed without shipping.`;
|
|
6225
|
+
const preview = s.stillWorks === false ? "Its preview version was removed, so reinstalling it fails." : s.stillWorks === true ? "Its preview version still installs, but it is not the released version." : "";
|
|
6226
|
+
return {
|
|
6227
|
+
subject: `Design system change ${quote(x.title)} ${s.kind === "shipped" ? "has shipped" : "was closed"}`,
|
|
6228
|
+
body: ["Hi,", "", [what, preview].filter(Boolean).join(" "), "", "If you installed its preview, go back to the stable version:", back, "", `Details for developers: ${x.url}`].join("\n")
|
|
6229
|
+
};
|
|
6230
|
+
}
|
|
6231
|
+
return null;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
// ../core/src/try-in-product.ts
|
|
6235
|
+
function tryInProductUrl(preview, pr, screen) {
|
|
6236
|
+
const url = new URL(preview);
|
|
6237
|
+
url.pathname = "/";
|
|
6238
|
+
url.search = "";
|
|
6239
|
+
url.searchParams.set("bridge", String(pr));
|
|
6240
|
+
if (screen) {
|
|
6241
|
+
url.searchParams.set("tenant", screen.tenant);
|
|
6242
|
+
url.searchParams.set("theme", screen.theme);
|
|
6243
|
+
}
|
|
6244
|
+
url.hash = screen ? screen.route : "";
|
|
6245
|
+
return url.href;
|
|
6246
|
+
}
|
|
6247
|
+
|
|
6248
|
+
// ../core/src/combine-branches.ts
|
|
6249
|
+
var MIN_TOGETHER = 2;
|
|
6250
|
+
var MAX_TOGETHER = 4;
|
|
6251
|
+
function onlyTokens(files, tokensRoot) {
|
|
6252
|
+
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
6253
|
+
return files.length > 0 && files.every((f) => f.startsWith(root) && f.endsWith(".tokens.json") || /contrast\.checks\.json$/.test(f) || f.startsWith(".changeset/"));
|
|
6254
|
+
}
|
|
6255
|
+
function tokenFilesOf(files, tokensRoot) {
|
|
6256
|
+
const root = `${tokensRoot.replace(/^\/+|\/+$/g, "")}/`;
|
|
6257
|
+
return files.filter((f) => f.startsWith(root) && f.endsWith(".tokens.json")).map((f) => f.slice(root.length));
|
|
6258
|
+
}
|
|
6259
|
+
var changeList = (numbers) => numbers.join(",");
|
|
6260
|
+
function tryTogetherUrl(admin, numbers, picks) {
|
|
6261
|
+
const url = new URL(admin);
|
|
6262
|
+
url.pathname = "/";
|
|
6263
|
+
url.hash = "";
|
|
6264
|
+
const use = encodePicks(picks);
|
|
6265
|
+
url.search = `?bridge=${changeList(numbers)}${use ? `&bridge-use=${encodeURIComponent(use)}` : ""}`;
|
|
6266
|
+
return url.href;
|
|
6267
|
+
}
|
|
6268
|
+
var COMBINE_COMPARE_LIMIT = 300;
|
|
6269
|
+
async function readCombineBranch(gh, pull, opts) {
|
|
6270
|
+
const compare = await gh.compareDetailed(opts.base, pull.headSha);
|
|
6271
|
+
const files = compare.files;
|
|
6272
|
+
const names3 = files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
6273
|
+
const start = compare.mergeBase;
|
|
6274
|
+
if (!start || files.length >= COMBINE_COMPARE_LIMIT || !onlyTokens(names3, opts.tokensRoot)) return { number: pull.number, title: pull.title, tokens: false, edits: null };
|
|
6275
|
+
const root = opts.tokensRoot.replace(/^\/+|\/+$/g, "");
|
|
6276
|
+
const tokenFiles = files.filter((f) => tokenFilesOf([f.filename], root).length);
|
|
6277
|
+
const source = { number: pull.number, base: {}, head: {}, files: tokenFiles.map((f) => tokenFilesOf([f.filename], root)[0]) };
|
|
6278
|
+
await Promise.all(
|
|
6279
|
+
tokenFiles.map(async (f) => {
|
|
6280
|
+
const rel = tokenFilesOf([f.filename], root)[0];
|
|
6281
|
+
source.base[rel] = f.status === "added" ? void 0 : await gh.readFile(f.previous_filename ?? f.filename, start);
|
|
6282
|
+
source.head[rel] = f.status === "removed" ? void 0 : await gh.readFile(f.filename, pull.headSha);
|
|
6283
|
+
})
|
|
6284
|
+
);
|
|
6285
|
+
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
6286
|
+
}
|
|
6287
|
+
|
|
6288
|
+
// ../core/src/prod-branch-github.ts
|
|
6289
|
+
var PRODUCT_FILE = /\.(css|scss|sass|less|tsx|jsx|ts|js|mjs|cjs|mts|cts|astro|vue|svelte)$/;
|
|
6290
|
+
var SKIP_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$/;
|
|
6291
|
+
var PIECES_FILE_LIMIT = 40;
|
|
6292
|
+
var cleanPath = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
6293
|
+
function productOf(products, pull, files) {
|
|
6294
|
+
const product = productOfBranch(products, { labels: pull.labels, files });
|
|
6295
|
+
if (product.key === defaultProduct(products).key) throw new GitHubError(422, `This branch is a library branch (${product.name}), not a product branch: missing pieces are checked on a branch started for a product.`);
|
|
6296
|
+
return product;
|
|
6297
|
+
}
|
|
6298
|
+
async function readProductBranchOnGitHub(gh, pr, opts) {
|
|
6299
|
+
const pull = await gh.getPullRequest(pr);
|
|
6300
|
+
const cmp = await gh.compareDetailed(pull.baseRef, pull.headSha);
|
|
6301
|
+
const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename));
|
|
6302
|
+
const dir = product.path ? `${cleanPath(product.path)}/` : "";
|
|
6303
|
+
const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
|
|
6304
|
+
const read = changed.slice(0, PIECES_FILE_LIMIT);
|
|
6305
|
+
const start = cmp.mergeBase ?? pull.baseRef;
|
|
6306
|
+
const files = await Promise.all(
|
|
6307
|
+
read.map(async (f) => {
|
|
6308
|
+
const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
|
|
6309
|
+
return { path: f.filename, before: before ?? null, after: after ?? null };
|
|
6310
|
+
})
|
|
6311
|
+
);
|
|
6312
|
+
return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
|
|
6313
|
+
}
|
|
6314
|
+
async function recordPiecesOnGitHub(gh, pr, check) {
|
|
6315
|
+
const pull = await gh.getPullRequest(pr);
|
|
6316
|
+
const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
|
|
6317
|
+
await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
|
|
6318
|
+
return record;
|
|
6319
|
+
}
|
|
6320
|
+
var productWriter = (gh, branch) => isExplorationBranch(branch) ? gh.withBranchPrefix(EXPLORATION_PREFIX) : gh;
|
|
6321
|
+
async function useLibraryPreviewOnGitHub(gh, input) {
|
|
6322
|
+
const message = `chore(bridge): use the preview of ${input.libraryBranch} (#${input.libraryNumber})
|
|
6323
|
+
|
|
6324
|
+
The product branch takes the library branch before it ships. Switch to the published version once it does.`;
|
|
6325
|
+
try {
|
|
6326
|
+
const r = await productWriter(gh, input.productBranch).mergeIntoBranch(input.productBranch, input.libraryBranch, message);
|
|
6327
|
+
return { ...r, said: r.status === "merged" ? "Your branch now uses the preview of the library branch." : "Your branch already has the latest preview." };
|
|
6328
|
+
} catch (err) {
|
|
6329
|
+
if (err instanceof GitHubError && err.status === 409) return { status: "conflict", sha: null, said: `Your branch and the library branch change the same lines. A developer merges ${input.libraryBranch} into ${input.productBranch} by hand.` };
|
|
6330
|
+
throw err;
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
function productBranchRef(pull, product) {
|
|
6334
|
+
const name = /<!-- bridge-exploration (\{[\s\S]*?\}) -->/.exec(pull.body ?? "");
|
|
6335
|
+
let title = pull.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, "");
|
|
6336
|
+
let author = pull.author;
|
|
6337
|
+
if (name) {
|
|
6338
|
+
try {
|
|
6339
|
+
const e = JSON.parse(name[1]);
|
|
6340
|
+
if (e.name) title = e.name;
|
|
6341
|
+
if (e.author) author = e.author;
|
|
6342
|
+
} catch {
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {} };
|
|
6346
|
+
}
|
|
6347
|
+
async function openLibraryBranchOnGitHub(gh, input) {
|
|
6348
|
+
const pull = await gh.getPullRequest(input.pr);
|
|
6349
|
+
const record = parsePiecesRecord(pull.body);
|
|
6350
|
+
const piece = record?.pieces.find((p) => p.id === input.piece);
|
|
6351
|
+
if (!record || !piece) throw new GitHubError(404, record ? `No piece "${input.piece}" on #${input.pr}. Pieces: ${record.pieces.map((p) => p.id).join(", ") || "none"}.` : `#${input.pr} was not checked yet: find its missing pieces first (bridge_missing_pieces with record, or \`bridge pieces ${input.pr} --record\`).`);
|
|
6352
|
+
const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []));
|
|
6353
|
+
const mode = input.mode ?? "direct";
|
|
6354
|
+
const plan = libraryBranchPlan(piece, productBranchRef(pull, product), { source: input.source, requestedBy: input.openedBy, mode });
|
|
6355
|
+
const existing = record.links.find((l) => l.piece === piece.id);
|
|
6356
|
+
if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
|
|
6357
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null, preview: null, warnings: [] };
|
|
6358
|
+
if (!gh.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a library change needs a GitHub token (issues: write)." : "Opening a library branch needs a GitHub token (contents, pull requests and issues: write).");
|
|
6359
|
+
const warnings = [];
|
|
6360
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
6361
|
+
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} };
|
|
6362
|
+
let link;
|
|
6363
|
+
if (plan.kind === "token-change") {
|
|
6364
|
+
if (!input.writeTokens) throw new GitHubError(422, "This piece is a token: open it where the token files are read (Bridge Studio, bridge_open_library_branch or `bridge pieces open`).");
|
|
6365
|
+
const r = await input.writeTokens(plan, { mode });
|
|
6366
|
+
link = r.status === "suggested" ? { piece: piece.id, kind: "suggestion", number: r.number, url: r.url, title: plan.title, at, ...who } : { piece: piece.id, kind: "token-change", number: r.number, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
6367
|
+
} else if (plan.kind === "exploration") {
|
|
6368
|
+
const r = await startExplorationOnGitHub(gh, { ...plan.exploration, ...input.openedBy ? { author: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} }, { base: input.base, ...input.exploration, labels: [NEEDED_BY_LABEL] });
|
|
6369
|
+
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at, ...who };
|
|
6370
|
+
} else {
|
|
6371
|
+
const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
|
|
6372
|
+
const r = await gh.createIssue(issue);
|
|
6373
|
+
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at, ...who };
|
|
6374
|
+
}
|
|
6375
|
+
try {
|
|
6376
|
+
if (link.kind === "request" || link.kind === "suggestion") {
|
|
6377
|
+
const issue = await gh.getIssue(link.number);
|
|
6378
|
+
await gh.updateIssue(link.number, { body: withNeededBy(issue.body, plan.neededBy) });
|
|
6379
|
+
if (link.kind === "suggestion" && !issue.labels.includes(NEEDED_BY_LABEL)) await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
6380
|
+
} else {
|
|
6381
|
+
const lib = await gh.getPullRequest(link.number);
|
|
6382
|
+
await gh.updatePullRequest(link.number, { body: withNeededBy(lib.body, plan.neededBy) });
|
|
6383
|
+
await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
6384
|
+
}
|
|
6385
|
+
} catch (err) {
|
|
6386
|
+
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
6387
|
+
warnings.push(`The library side does not say who needs it yet (${err.message}). Add "Needed by #${input.pr}" to #${link.number} on GitHub.`);
|
|
6388
|
+
}
|
|
6389
|
+
try {
|
|
6390
|
+
const fresh = await gh.getPullRequest(input.pr);
|
|
6391
|
+
const next = withLink(parsePiecesRecord(fresh.body) ?? record, link);
|
|
6392
|
+
await gh.updatePullRequest(input.pr, { body: withPiecesRecord(fresh.body, next) });
|
|
6393
|
+
} catch (err) {
|
|
6394
|
+
if (mode !== "suggest") throw err;
|
|
6395
|
+
warnings.push(`#${input.pr} does not list #${link.number} yet (${err.message}). Add "Library: #${link.number}" to #${input.pr} on GitHub, so this piece is not sent twice.`);
|
|
6396
|
+
}
|
|
6397
|
+
let preview = null;
|
|
6398
|
+
if ((input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
|
|
6399
|
+
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
6400
|
+
warnings.push(`Your branch does not use the preview yet: ${err.message}`);
|
|
6401
|
+
return null;
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
return { status: link.kind === "suggestion" ? "suggested" : "opened", plan, link, preview, warnings };
|
|
6405
|
+
}
|
|
6406
|
+
var isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
|
|
6407
|
+
async function linkedLibraryBranchOnGitHub(gh, link, product, piece) {
|
|
6408
|
+
const nearest = piece?.nearest?.label;
|
|
6409
|
+
if (link.kind === "suggestion") {
|
|
6410
|
+
const issue = await gh.getIssue(link.number);
|
|
6411
|
+
const cached = suggestionStateFromLabels(issue.labels);
|
|
6412
|
+
const s = suggestionStatus({ state: issue.state, stateReason: issue.stateReason, applied: cached === "applying", review: cached === "applying" ? void 0 : cached });
|
|
6413
|
+
const status2 = linkStatus({ kind: "suggestion", status: s });
|
|
6414
|
+
const lifecycle2 = s === "applied" ? "merged" : issue.state === "closed" ? "closed" : "open";
|
|
6415
|
+
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
6416
|
+
const preview2 = libraryPreviewState({ kind: "suggestion", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
6417
|
+
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
6418
|
+
}
|
|
6419
|
+
if (link.kind === "request") {
|
|
6420
|
+
const issue = await gh.getIssue(link.number);
|
|
6421
|
+
const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
|
|
6422
|
+
const lifecycle2 = issue.state === "closed" ? issue.stateReason === "not_planned" ? "closed" : "merged" : "open";
|
|
6423
|
+
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
6424
|
+
const preview2 = libraryPreviewState({ kind: "request", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
6425
|
+
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
6426
|
+
}
|
|
6427
|
+
const lib = await gh.getPullRequest(link.number);
|
|
5240
6428
|
const reviews = await gh.listReviews(link.number).catch(() => []);
|
|
5241
6429
|
const status = linkStatus({ kind: link.kind, state: lib.state, draft: lib.draft, labels: lib.labels, requestedReviewers: lib.requestedReviewers, reviews: reviews.map((r) => ({ user: r.user, state: r.state })) });
|
|
5242
6430
|
const lifecycle = lib.state === "merged" ? "merged" : lib.state === "open" ? "open" : "closed";
|
|
@@ -5310,21 +6498,436 @@ function planTokenProposal(input) {
|
|
|
5310
6498
|
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
5311
6499
|
];
|
|
5312
6500
|
const blocked = newErrors.length || newFailures.length ? [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") : null;
|
|
5313
|
-
return { title: input.title, summary: summary2, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug: slug2, blocked };
|
|
6501
|
+
return { title: input.title, summary: summary2, edits: input.edits, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug: slug2, blocked };
|
|
6502
|
+
}
|
|
6503
|
+
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
6504
|
+
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
6505
|
+
const jira = opts.jira ?? null;
|
|
6506
|
+
let branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}`, jira);
|
|
6507
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}-${i}`, jira);
|
|
6508
|
+
const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
|
|
6509
|
+
|
|
6510
|
+
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
6511
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
6512
|
+
const draft = plan.tier.tier >= 2;
|
|
6513
|
+
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
6514
|
+
await gh.addLabels(pr.number, labels);
|
|
6515
|
+
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
6516
|
+
}
|
|
6517
|
+
var tokenLabels = (plan, before = [], after = []) => [
|
|
6518
|
+
.../* @__PURE__ */ new Set([...before, `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...after])
|
|
6519
|
+
];
|
|
6520
|
+
function tokenSuggestionPayload(plan, ctx) {
|
|
6521
|
+
const tokenFiles = new Set(plan.touched);
|
|
6522
|
+
const files = plan.files.filter((f) => !tokenFiles.has(f.path)).map((f) => ({ path: f.path, beforeSha: null, afterSha: f.content === null ? null : gitBlobSha(f.content), content: f.content }));
|
|
6523
|
+
return {
|
|
6524
|
+
schema: SUGGESTION_SCHEMA,
|
|
6525
|
+
kind: "token-change",
|
|
6526
|
+
title: plan.title,
|
|
6527
|
+
summary: plan.summary,
|
|
6528
|
+
source: ctx.source,
|
|
6529
|
+
...ctx.suggestedBy ? { suggestedBy: ctx.suggestedBy.replace(/^@/, "") } : {},
|
|
6530
|
+
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6531
|
+
base: ctx.base,
|
|
6532
|
+
tier: { tier: plan.tier.tier, bump: plan.tier.bump },
|
|
6533
|
+
tokens: {
|
|
6534
|
+
edits: plan.edits,
|
|
6535
|
+
changes: plan.changes.map((c) => ({ kind: c.kind, path: c.path, from: c.from, file: c.file, before: c.before, after: c.after, beforeCss: c.beforeCss, afterCss: c.afterCss }))
|
|
6536
|
+
},
|
|
6537
|
+
...files.length ? { files } : {},
|
|
6538
|
+
checks: {
|
|
6539
|
+
newErrors: plan.newErrors.length,
|
|
6540
|
+
contrast: plan.contrast.map((d) => ({ pair: `${d.fg} on ${d.bg}`, mode: d.modes || void 0, before: d.before ?? 0, after: d.after ?? 0, min: d.min, pass: d.passAfter })),
|
|
6541
|
+
blocked: plan.blocked,
|
|
6542
|
+
impact: plan.impact.total
|
|
6543
|
+
},
|
|
6544
|
+
...ctx.links ? { links: ctx.links } : {},
|
|
6545
|
+
...ctx.previews?.length ? { previews: ctx.previews } : {}
|
|
6546
|
+
};
|
|
6547
|
+
}
|
|
6548
|
+
async function suggestTokenProposalOnGitHub(gh, plan, opts) {
|
|
6549
|
+
if (plan.blocked) throw new Error(`Not suggested: ${plan.blocked}. Bridge never suggests a change that breaks the gate.`);
|
|
6550
|
+
const sha = opts.baseSha ?? await gh.getBranchSha(opts.base);
|
|
6551
|
+
if (!sha) throw new Error(`Branch ${opts.base} not found. Check the default branch in bridge.config.json.`);
|
|
6552
|
+
const links = opts.neededBy || opts.links ? { ...opts.links, ...opts.neededBy ? { neededBy: opts.neededBy } : {} } : void 0;
|
|
6553
|
+
const payload = tokenSuggestionPayload(plan, { base: { ref: opts.base, sha }, source: opts.source, suggestedBy: opts.suggestedBy, links, previews: opts.previews });
|
|
6554
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
6555
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels, assignees: opts.assignees, details: plan.body });
|
|
6556
|
+
return { status: "suggested", number: created.number, url: created.url, sha: created.sha, labels, warnings: created.warnings };
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6559
|
+
// ../core/src/figma-push-github.ts
|
|
6560
|
+
var pushSource = (d) => ["figma", "studio", "cli", "mcp"].includes(d.source) ? d.source : "figma";
|
|
6561
|
+
var specTier = (d) => ({ tier: 2, bump: d.kind === "new-component" || d.kind === "new-variant" || d.kind === "prop-change" ? "minor" : "patch" });
|
|
6562
|
+
function componentSpecSuggestionPayload(d, plan, ctx) {
|
|
6563
|
+
const specPath = figmaSpecPath(d);
|
|
6564
|
+
const spec = JSON.parse(formatFigmaSpec(d, plan));
|
|
6565
|
+
const brief = figmaDraftBrief({ ...d, tokenEdits: plan.edits, reasons: plan.reasons }, { suggestion: true, specPath, componentsDir: ctx.componentsDir });
|
|
6566
|
+
const by = (ctx.suggestedBy ?? d.requestedBy)?.replace(/^@/, "");
|
|
6567
|
+
const figma = d.figma?.fileKey || d.figma?.nodeId || d.figma?.url ? { ...d.figma.fileKey ? { fileKey: d.figma.fileKey } : {}, ...d.figma.nodeId ? { nodeId: d.figma.nodeId } : {}, ...d.figma.url ? { url: d.figma.url } : {} } : void 0;
|
|
6568
|
+
return {
|
|
6569
|
+
schema: SUGGESTION_SCHEMA,
|
|
6570
|
+
kind: "component-spec",
|
|
6571
|
+
title: d.title,
|
|
6572
|
+
summary: d.summary.trim() || d.title,
|
|
6573
|
+
source: pushSource(d),
|
|
6574
|
+
...by ? { suggestedBy: by } : {},
|
|
6575
|
+
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6576
|
+
base: ctx.base,
|
|
6577
|
+
tier: specTier(d),
|
|
6578
|
+
componentSpec: { component: d.component, specPath, spec, brief },
|
|
6579
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 },
|
|
6580
|
+
...figma ? { links: { figma } } : {}
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
async function openComponentPushOnGitHub(gh, input) {
|
|
6584
|
+
const d = input.diff;
|
|
6585
|
+
if (!input.plan && !input.tokens) throw new Error("Route the change first: pass the token facts or the plan.");
|
|
6586
|
+
const plan = input.plan ?? planComponentPush(d, input.tokens);
|
|
6587
|
+
if (plan.route === "manual") return { status: "not-sent", plan, link: null };
|
|
6588
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null };
|
|
6589
|
+
const mode = input.mode ?? "direct";
|
|
6590
|
+
if (!gh.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a component change needs a GitHub token (issues: write)." : "Sending a component change needs a GitHub token (contents and pull requests: write).");
|
|
6591
|
+
const origin = {
|
|
6592
|
+
component: d.component,
|
|
6593
|
+
figmaName: d.figmaName,
|
|
6594
|
+
route: plan.route,
|
|
6595
|
+
kind: d.kind,
|
|
6596
|
+
...d.figma?.nodeId ? { nodeId: d.figma.nodeId } : {},
|
|
6597
|
+
...d.figma?.fileKey ? { fileKey: d.figma.fileKey } : {},
|
|
6598
|
+
...d.figma?.url ? { url: d.figma.url } : {},
|
|
6599
|
+
...d.requestedBy ? { requestedBy: d.requestedBy } : {}
|
|
6600
|
+
};
|
|
6601
|
+
if (plan.route === "token-change") {
|
|
6602
|
+
if (!input.writeTokens) throw new GitHubError(422, "This change is a token change: send it where the token files are read (the DS Bridge plugin, bridge_push_figma_component or `bridge figma push`).");
|
|
6603
|
+
const figma = { ...origin.fileKey ? { fileKey: origin.fileKey } : {}, ...origin.nodeId ? { nodeId: origin.nodeId } : {}, ...origin.url ? { url: origin.url } : {} };
|
|
6604
|
+
const r = await input.writeTokens({ edits: plan.edits, title: figmaBranchTitle(d), summary: d.summary.trim() || d.title, bodyExtra: [figmaOriginSection(origin)], mode, figma });
|
|
6605
|
+
await gh.addLabels(r.number, [FROM_FIGMA_LABEL]);
|
|
6606
|
+
if (r.status === "suggested") return { status: "suggested", plan, link: null, suggestion: { route: "token-change", number: r.number, url: r.url } };
|
|
6607
|
+
return { status: "opened", plan, link: { route: "token-change", number: r.number, url: r.url, branch: r.branch } };
|
|
6608
|
+
}
|
|
6609
|
+
if (mode === "suggest") {
|
|
6610
|
+
const sha = input.baseSha ?? await gh.getBranchSha(input.base);
|
|
6611
|
+
if (!sha) throw new GitHubError(404, `Branch ${input.base} not found. Check the default branch in bridge.config.json.`);
|
|
6612
|
+
const payload = componentSpecSuggestionPayload(d, plan, { base: { ref: input.base, sha }, suggestedBy: input.suggestedBy });
|
|
6613
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels: [...input.labels ?? [], WAITING_FOR_CLAUDE_LABEL] });
|
|
6614
|
+
return { status: "suggested", plan, link: null, suggestion: { route: "claude-draft", number: created.number, url: created.url } };
|
|
6615
|
+
}
|
|
6616
|
+
const spec = figmaSpecPath(d);
|
|
6617
|
+
const o = { ...origin, spec, status: "spec" };
|
|
6618
|
+
let branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}`;
|
|
6619
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}-${i}`;
|
|
6620
|
+
await gh.commitToNewBranch({
|
|
6621
|
+
base: input.base,
|
|
6622
|
+
branch,
|
|
6623
|
+
message: `chore(figma): spec for ${figmaBranchTitle(d)}
|
|
6624
|
+
|
|
6625
|
+
Sent from Figma with ${input.tool ?? "the DS Bridge Figma plugin"}. Claude AI drafts the code on this branch; a person reviews it.`,
|
|
6626
|
+
files: [{ path: spec, content: formatFigmaSpec(d, plan) }]
|
|
6627
|
+
});
|
|
6628
|
+
const pr = await gh.createPullRequest({ base: input.base, head: branch, title: figmaBranchTitle(d), body: figmaDraftBody(d, plan, o), draft: true });
|
|
6629
|
+
await gh.addLabels(pr.number, [.../* @__PURE__ */ new Set([...input.labels ?? [], FROM_FIGMA_LABEL, REQUEST_LABEL, WAITING_FOR_CLAUDE_LABEL])]);
|
|
6630
|
+
return { status: "opened", plan, link: { route: "claude-draft", number: pr.number, url: pr.url, branch } };
|
|
6631
|
+
}
|
|
6632
|
+
async function listFigmaChangesOnGitHub(gh, opts = {}) {
|
|
6633
|
+
const pulls = await gh.listPullRequests(opts.state ?? "open");
|
|
6634
|
+
const out = [];
|
|
6635
|
+
for (const p of pulls) {
|
|
6636
|
+
if (!p.labels.includes(FROM_FIGMA_LABEL)) continue;
|
|
6637
|
+
const origin = parseFigmaOrigin(p.body);
|
|
6638
|
+
if (!origin) continue;
|
|
6639
|
+
out.push({ number: p.number, title: p.title, url: p.url, branch: p.headRef, origin, status: figmaPushStatus({ state: p.state, draft: p.draft, labels: p.labels, route: origin.route }) });
|
|
6640
|
+
}
|
|
6641
|
+
return out;
|
|
6642
|
+
}
|
|
6643
|
+
async function figmaChangeOnGitHub(gh, n, opts = {}) {
|
|
6644
|
+
const pull = await gh.getPullRequest(n);
|
|
6645
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
6646
|
+
if (!origin) throw new GitHubError(404, `#${n} was not sent from Figma (no "From Figma" block in its description).`);
|
|
6647
|
+
const status = figmaPushStatus({ state: pull.state, draft: pull.draft, labels: pull.labels, route: origin.route });
|
|
6648
|
+
if (origin.route === "token-change") return { pull, origin, status, spec: void 0, brief: void 0 };
|
|
6649
|
+
const text = origin.spec ? await gh.getFileText(origin.spec, pull.headSha).catch(() => void 0) : void 0;
|
|
6650
|
+
const spec = parseFigmaSpec(text);
|
|
6651
|
+
if (!spec) throw new GitHubError(404, `The spec ${origin.spec ?? FIGMA_SPEC_DIR} is missing from ${pull.headRef}. Send the component from Figma again.`);
|
|
6652
|
+
const brief = figmaDraftBrief(spec, { pr: n, branch: pull.headRef, specPath: origin.spec, componentsDir: opts.componentsDir });
|
|
6653
|
+
return { pull, origin, status, spec, brief };
|
|
6654
|
+
}
|
|
6655
|
+
async function markFigmaChangeDraftedOnGitHub(gh, n) {
|
|
6656
|
+
if (!gh.canWrite) throw new GitHubError(401, "Marking a branch drafted needs a GitHub token (pull requests: write).");
|
|
6657
|
+
const pull = await gh.getPullRequest(n);
|
|
6658
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
6659
|
+
if (!origin || origin.route !== "claude-draft") throw new GitHubError(422, `#${n} is not a Claude draft branch from Figma.`);
|
|
6660
|
+
if (pull.state !== "open") throw new GitHubError(422, `#${n} is ${pull.state}: only an open branch is marked drafted.`);
|
|
6661
|
+
const files = (await gh.listPullRequestFiles(n)).map((f) => f.filename).filter((f) => !f.startsWith(`${FIGMA_SPEC_DIR}/`));
|
|
6662
|
+
if (!files.length) throw new GitHubError(422, `${pull.headRef} only carries the spec: write the component code there first, push it, then mark it drafted.`);
|
|
6663
|
+
await gh.updatePullRequest(n, { body: withFigmaOrigin(pull.body, { ...origin, status: "drafted" }) });
|
|
6664
|
+
await gh.removeLabel(n, WAITING_FOR_CLAUDE_LABEL).catch(() => void 0);
|
|
6665
|
+
await gh.addLabels(n, [DRAFTED_BY_CLAUDE_LABEL]);
|
|
6666
|
+
const labels = [...pull.labels.filter((l) => l !== WAITING_FOR_CLAUDE_LABEL), DRAFTED_BY_CLAUDE_LABEL];
|
|
6667
|
+
return { status: figmaPushStatus({ state: pull.state, labels, route: origin.route }), files };
|
|
6668
|
+
}
|
|
6669
|
+
|
|
6670
|
+
// ../core/src/suggestion-apply.ts
|
|
6671
|
+
var SUGGESTION_APPLY_EXIT = {
|
|
6672
|
+
ready: 0,
|
|
6673
|
+
"already-applied": 0,
|
|
6674
|
+
conflict: 3,
|
|
6675
|
+
"not-approved": 1,
|
|
6676
|
+
blocked: 1,
|
|
6677
|
+
refused: 1
|
|
6678
|
+
};
|
|
6679
|
+
var same2 = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
6680
|
+
var show3 = (v) => v === void 0 ? "nothing" : typeof v === "string" ? v : JSON.stringify(v);
|
|
6681
|
+
function tokenRoot(config) {
|
|
6682
|
+
const r = config.tokens.root.replace(/\/+$/, "");
|
|
6683
|
+
return r === "." ? "" : r;
|
|
6684
|
+
}
|
|
6685
|
+
function tokenFileOf(config, path) {
|
|
6686
|
+
const root = tokenRoot(config);
|
|
6687
|
+
if (root && !path.startsWith(`${root}/`)) return null;
|
|
6688
|
+
if (!selectTokenFiles(config, [path]).length) return null;
|
|
6689
|
+
return root ? path.slice(root.length + 1) : path;
|
|
6690
|
+
}
|
|
6691
|
+
function planTokenEdit(set, e, changes) {
|
|
6692
|
+
const base = { type: "token", path: e.path, edit: e, ...e.file ? { file: e.file } : {} };
|
|
6693
|
+
const defsAt = (path) => set.defs.filter((d) => d.path === path && (!e.file || d.file === e.file));
|
|
6694
|
+
const recorded = (file, path = e.path) => changes.find((c) => c.path === path && c.file === file && (e.op === "rename" ? c.kind === "renamed" : true));
|
|
6695
|
+
const conflict = (detail, extra = {}) => ({ op: { ...base, state: "conflict", detail, ...extra }, notes: [] });
|
|
6696
|
+
const done = (detail) => ({ op: { ...base, state: "already-applied", detail }, notes: [] });
|
|
6697
|
+
let result;
|
|
6698
|
+
try {
|
|
6699
|
+
result = applyTokenEdits(set, [e]);
|
|
6700
|
+
} catch (err) {
|
|
6701
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6702
|
+
switch (e.op) {
|
|
6703
|
+
case "rename": {
|
|
6704
|
+
const hasOld = defsAt(e.path).length > 0;
|
|
6705
|
+
const hasNew = set.defs.some((d) => d.path === e.newPath);
|
|
6706
|
+
if (!hasOld && hasNew) return done(`Already renamed to ${e.newPath}.`);
|
|
6707
|
+
return conflict(hasOld && hasNew ? `Both ${e.path} and ${e.newPath} exist now: rename it by hand.` : `${e.path} no longer exists: ${why}`);
|
|
6708
|
+
}
|
|
6709
|
+
case "remove":
|
|
6710
|
+
if (!defsAt(e.path).length) return done("Already removed.");
|
|
6711
|
+
return conflict(why);
|
|
6712
|
+
case "add": {
|
|
6713
|
+
const existing = defsAt(e.path);
|
|
6714
|
+
if (!existing.length) return conflict(why);
|
|
6715
|
+
const want = (file) => {
|
|
6716
|
+
const c = recorded(file);
|
|
6717
|
+
if (c && c.after !== void 0) return c.after;
|
|
6718
|
+
return coerceValue(e.value, e.type ?? existing.find((d2) => d2.file === file)?.type);
|
|
6719
|
+
};
|
|
6720
|
+
const all = existing.every((d2) => same2(d2.value, want(d2.file)));
|
|
6721
|
+
if (all) return done("Already added with the suggested value.");
|
|
6722
|
+
const d = existing.find((x) => !same2(x.value, want(x.file)));
|
|
6723
|
+
return conflict(`Someone added ${e.path} with another value since.`, { file: d.file, after: want(d.file), now: d.value });
|
|
6724
|
+
}
|
|
6725
|
+
default:
|
|
6726
|
+
return conflict(defsAt(e.path).length ? why : `${e.path} no longer exists, so its value cannot be set.`);
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
if (!result.touched.length) return done("Already has the suggested value.");
|
|
6730
|
+
if (e.op === "set" || e.op === "remove" || e.op === "rename") {
|
|
6731
|
+
for (const d of defsAt(e.path)) {
|
|
6732
|
+
const c = recorded(d.file, e.op === "rename" ? e.newPath ?? e.path : e.path);
|
|
6733
|
+
if (!c) continue;
|
|
6734
|
+
const okNow = same2(d.value, c.before) || e.op === "set" && same2(d.value, c.after);
|
|
6735
|
+
if (!okNow) return conflict(`${e.path} changed since it was suggested (now ${show3(d.value)}, suggested from ${show3(c.before)}).`, { file: d.file, before: c.before, after: c.after, now: d.value });
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
const verb = e.op === "set" ? "Set" : e.op === "add" ? "Add" : e.op === "remove" ? "Remove" : `Rename to ${e.newPath ?? ""}`;
|
|
6739
|
+
return { op: { ...base, state: "apply", detail: `${verb} in ${result.touched.join(", ")}.` }, files: result.files, notes: result.notes };
|
|
6740
|
+
}
|
|
6741
|
+
async function planFile(ws, baseSha, f) {
|
|
6742
|
+
const base = { type: "file", path: f.path };
|
|
6743
|
+
const cur = await ws.read(f.path);
|
|
6744
|
+
const curSha = cur?.sha ?? null;
|
|
6745
|
+
const theirs = f.afterSha === null ? null : f.content ?? null;
|
|
6746
|
+
if (curSha === f.afterSha) return { op: { ...base, state: "already-applied", detail: f.afterSha === null ? "Already deleted." : "Already has the suggested content." } };
|
|
6747
|
+
if (curSha === f.beforeSha) return { op: { ...base, state: "apply", detail: f.afterSha === null ? "Delete it." : f.beforeSha === null ? "Create it." : "Replace it with the suggested content." }, content: theirs };
|
|
6748
|
+
if (f.beforeSha === null) return { op: { ...base, state: "conflict", detail: "The suggestion creates this file, but a different one exists now." } };
|
|
6749
|
+
if (!cur) return { op: { ...base, state: "conflict", detail: "The suggestion changes this file, but it was deleted since." } };
|
|
6750
|
+
if (theirs === null) return { op: { ...base, state: "conflict", detail: "The suggestion deletes this file, but it changed since." } };
|
|
6751
|
+
const original = await ws.readAt(baseSha, f.path);
|
|
6752
|
+
if (original === null || gitBlobSha(original) !== f.beforeSha || !ws.merge) {
|
|
6753
|
+
return { op: { ...base, state: "conflict", detail: original === null ? "The file changed since and its version at the suggestion base cannot be read, so it cannot be merged." : "The file changed since and cannot be merged here." } };
|
|
6754
|
+
}
|
|
6755
|
+
const merged = await ws.merge({ path: f.path, ours: cur.text, base: original, theirs });
|
|
6756
|
+
if (!merged.clean) return { op: { ...base, state: "conflict", detail: `The file changed since and the merge has ${merged.conflicts} conflict(s).`, hunks: merged.conflicts } };
|
|
6757
|
+
if (merged.text === cur.text) return { op: { ...base, state: "already-applied", detail: "Already has the suggested lines." } };
|
|
6758
|
+
return { op: { ...base, state: "apply-merged", detail: "The file changed since: merged with the suggested lines, no conflict." }, content: merged.text };
|
|
6759
|
+
}
|
|
6760
|
+
async function planSuggestionApply(input) {
|
|
6761
|
+
const { parsed, config, workspace: ws } = input;
|
|
6762
|
+
const payload = parsed.payload;
|
|
6763
|
+
const notes = [];
|
|
6764
|
+
const out = (outcome, message, rest2 = {}) => ({
|
|
6765
|
+
outcome,
|
|
6766
|
+
exitCode: SUGGESTION_APPLY_EXIT[outcome],
|
|
6767
|
+
message,
|
|
6768
|
+
payloadSha: parsed.sha,
|
|
6769
|
+
payload,
|
|
6770
|
+
gate: null,
|
|
6771
|
+
ops: [],
|
|
6772
|
+
writes: [],
|
|
6773
|
+
checks: null,
|
|
6774
|
+
problems: [],
|
|
6775
|
+
notes,
|
|
6776
|
+
...payload.componentSpec ? { brief: payload.componentSpec.brief } : {},
|
|
6777
|
+
...rest2
|
|
6778
|
+
});
|
|
6779
|
+
const problems = suggestionPathProblems(payload, config);
|
|
6780
|
+
if (payload.checks.blocked) problems.push(`It was suggested with a failing check (${payload.checks.blocked}): ask the author to fix it and send it again.`);
|
|
6781
|
+
for (const f of payload.files ?? []) {
|
|
6782
|
+
if (f.afterSha !== null && typeof f.content !== "string") problems.push(`"${f.path}" has no content in the suggestion: it is incomplete, ask the author to send it again.`);
|
|
6783
|
+
else if (f.afterSha !== null && gitBlobSha(f.content) !== f.afterSha) problems.push(`"${f.path}" does not match the content it was suggested with: someone edited it by hand.`);
|
|
6784
|
+
}
|
|
6785
|
+
if (problems.length) return out("refused", "Not applied: the suggestion asks for something Bridge never writes.", { problems: [...new Set(problems)] });
|
|
6786
|
+
if (parsed.edited && !input.acceptEdited) {
|
|
6787
|
+
return out("refused", 'Not applied: the suggestion was edited by hand since Bridge wrote it. Read the change, then apply it with "accept edited" if it is right.', {
|
|
6788
|
+
problems: ["The payload no longer matches the sha Bridge recorded."]
|
|
6789
|
+
});
|
|
6790
|
+
}
|
|
6791
|
+
const root = tokenRoot(config);
|
|
6792
|
+
const tokenFiles = selectTokenFiles(config, await ws.list());
|
|
6793
|
+
const targets = [
|
|
6794
|
+
...(payload.files ?? []).map((f) => f.path),
|
|
6795
|
+
...(payload.docs ?? []).map((d) => d.path),
|
|
6796
|
+
...payload.componentSpec ? [payload.componentSpec.specPath] : [],
|
|
6797
|
+
...payload.tokens?.edits.length ? tokenFiles.map((f) => root ? `${root}/${f}` : f) : []
|
|
6798
|
+
];
|
|
6799
|
+
if (ws.isSymlink) {
|
|
6800
|
+
const links = [];
|
|
6801
|
+
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.`);
|
|
6802
|
+
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
6803
|
+
}
|
|
6804
|
+
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null };
|
|
6805
|
+
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
|
|
6806
|
+
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
6807
|
+
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
6808
|
+
const approved = review === "approved";
|
|
6809
|
+
const gate = { review, missing, approvals, forced: !approved && Boolean(input.forceUnapproved), edited: parsed.edited };
|
|
6810
|
+
const ops = [];
|
|
6811
|
+
const writes = /* @__PURE__ */ new Map();
|
|
6812
|
+
const current = /* @__PURE__ */ new Map();
|
|
6813
|
+
for (const f of tokenFiles) {
|
|
6814
|
+
const r = await ws.read(root ? `${root}/${f}` : f);
|
|
6815
|
+
if (r) current.set(f, r.text);
|
|
6816
|
+
}
|
|
6817
|
+
const before = new LayeredTokenSet(config, current);
|
|
6818
|
+
let set = before;
|
|
6819
|
+
const appliedEdits = [];
|
|
6820
|
+
for (const e of payload.tokens?.edits ?? []) {
|
|
6821
|
+
const r = planTokenEdit(set, e, payload.tokens.changes);
|
|
6822
|
+
ops.push(r.op);
|
|
6823
|
+
notes.push(...r.notes);
|
|
6824
|
+
if (r.op.state === "apply" && r.files) {
|
|
6825
|
+
set = new LayeredTokenSet(config, r.files);
|
|
6826
|
+
appliedEdits.push(e);
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
for (const [f, text] of set.files) if (text !== before.files.get(f)) writes.set(set.repoPath(f), text);
|
|
6830
|
+
for (const f of payload.files ?? []) {
|
|
6831
|
+
if (writes.has(f.path)) {
|
|
6832
|
+
notes.push(`${f.path}: the token edits write this file, its copy in the suggestion is not used.`);
|
|
6833
|
+
continue;
|
|
6834
|
+
}
|
|
6835
|
+
const r = await planFile(ws, payload.base.sha, f);
|
|
6836
|
+
ops.push(r.op);
|
|
6837
|
+
if (r.op.state === "apply" || r.op.state === "apply-merged") writes.set(f.path, r.content ?? null);
|
|
6838
|
+
}
|
|
6839
|
+
for (const d of payload.docs ?? []) {
|
|
6840
|
+
const base = { type: "docs", path: d.path };
|
|
6841
|
+
const docs = d.after;
|
|
6842
|
+
let text;
|
|
6843
|
+
try {
|
|
6844
|
+
const issues = validateDocs(docs);
|
|
6845
|
+
if (issues.length) {
|
|
6846
|
+
problems.push(`${d.path}: ${issues.map((i) => i.message).join(" ")}`);
|
|
6847
|
+
continue;
|
|
6848
|
+
}
|
|
6849
|
+
const cur = await ws.read(d.path);
|
|
6850
|
+
text = docsFileText(docs, cur?.text ?? null);
|
|
6851
|
+
if (cur && text === cur.text) {
|
|
6852
|
+
ops.push({ ...base, state: "already-applied", detail: "The docs already say this." });
|
|
6853
|
+
continue;
|
|
6854
|
+
}
|
|
6855
|
+
ops.push({ ...base, state: "apply", detail: cur ? "Update the changed fields; other fields stay as they are now." : "Create the docs file." });
|
|
6856
|
+
writes.set(d.path, text);
|
|
6857
|
+
} catch (err) {
|
|
6858
|
+
problems.push(`${d.path}: the suggested docs are not a docs.json Bridge can read (${err instanceof Error ? err.message : String(err)}).`);
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
if (payload.componentSpec) {
|
|
6862
|
+
const s = payload.componentSpec;
|
|
6863
|
+
const text = `${JSON.stringify(s.spec, null, 2)}
|
|
6864
|
+
`;
|
|
6865
|
+
const cur = await ws.read(s.specPath);
|
|
6866
|
+
if (cur && cur.text === text) ops.push({ type: "component-spec", path: s.specPath, state: "already-applied", detail: "The spec file is already there." });
|
|
6867
|
+
else {
|
|
6868
|
+
ops.push({ type: "component-spec", path: s.specPath, state: "apply", detail: `Write the spec; the ${s.component} code itself is written from the brief.` });
|
|
6869
|
+
writes.set(s.specPath, text);
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
const afterFiles = new Map(set.files);
|
|
6873
|
+
for (const [p, content] of writes) {
|
|
6874
|
+
const tf = tokenFileOf(config, p);
|
|
6875
|
+
if (!tf) continue;
|
|
6876
|
+
if (content === null) afterFiles.delete(tf);
|
|
6877
|
+
else afterFiles.set(tf, content);
|
|
6878
|
+
}
|
|
6879
|
+
const after = new LayeredTokenSet(config, afterFiles);
|
|
6880
|
+
const tokenChanges = diffLayered(before, after);
|
|
6881
|
+
const issueKey4 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
6882
|
+
const prevErrors = new Set(validateLayered(before).map(issueKey4));
|
|
6883
|
+
const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey4(i))) : [];
|
|
6884
|
+
let pairs = [];
|
|
6885
|
+
if (config.contrast && tokenChanges.length) {
|
|
6886
|
+
try {
|
|
6887
|
+
pairs = parseContrastChecks((await ws.read(config.contrast.path))?.text);
|
|
6888
|
+
} catch {
|
|
6889
|
+
notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
const modes = config.contrast?.modes;
|
|
6893
|
+
const contrast = pairs.length ? contrastDelta(runLayeredContrast(before, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
6894
|
+
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
6895
|
+
const otherFiles = [...writes.keys()].filter((p) => !tokenFileOf(config, p));
|
|
6896
|
+
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 });
|
|
6897
|
+
const tierIncreased = writes.size > 0 && tier.tier > payload.tier.tier;
|
|
6898
|
+
const checks = { tierWhenSuggested: payload.tier.tier, tier, tierIncreased, newErrors, newFailures, contrast, tokenChanges };
|
|
6899
|
+
const rest = { gate, ops, checks, problems };
|
|
6900
|
+
if (problems.length) return out("refused", "Not applied: part of the suggestion is not valid.", rest);
|
|
6901
|
+
const conflicts = ops.filter((o) => o.state === "conflict");
|
|
6902
|
+
if (conflicts.length) {
|
|
6903
|
+
return out("conflict", `Not applied: ${conflicts.length} change(s) conflict with the code now. Ask the author to refresh the suggestion on today's code, or apply those parts by hand.`, rest);
|
|
6904
|
+
}
|
|
6905
|
+
if (!writes.size) return out("already-applied", "Already applied: the code has every change of this suggestion.", rest);
|
|
6906
|
+
if (newErrors.length || newFailures.length) {
|
|
6907
|
+
const why = [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ");
|
|
6908
|
+
return out("blocked", `Not applied: on today's code it would add ${why}. Ask the author to refresh the suggestion.`, rest);
|
|
6909
|
+
}
|
|
6910
|
+
if (tierIncreased && !input.forceUnapproved) {
|
|
6911
|
+
return out("blocked", `Not applied: on today's code it is a tier ${tier.tier} change, it was approved as tier ${payload.tier.tier}. It needs a new approval.`, rest);
|
|
6912
|
+
}
|
|
6913
|
+
if (!approved && !input.forceUnapproved) {
|
|
6914
|
+
const who = review === "changes-requested" ? `changes requested by ${approvals.changesRequested.map((l) => `@${l}`).join(", ")}` : `missing ${missing.map((m) => m === "approval" ? "an approval" : `${m} approval`).join(" and ")}`;
|
|
6915
|
+
return out("not-approved", `Not applied: it is not approved yet (${who}). Ask the owners to review it on the issue.`, rest);
|
|
6916
|
+
}
|
|
6917
|
+
const forced = gate.forced || tierIncreased && Boolean(input.forceUnapproved);
|
|
6918
|
+
return out("ready", `Ready: ${writes.size} file(s) to write${forced ? ", without every approval" : ""}.`, {
|
|
6919
|
+
...rest,
|
|
6920
|
+
gate: { ...gate, forced },
|
|
6921
|
+
writes: [...writes].map(([path, content]) => ({ path, content })).sort((a, b) => a.path.localeCompare(b.path))
|
|
6922
|
+
});
|
|
5314
6923
|
}
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
const
|
|
5321
|
-
|
|
5322
|
-
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
5323
|
-
const labels = [.../* @__PURE__ */ new Set([...opts.labels ?? [], `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...opts.labelsAfter ?? []])];
|
|
5324
|
-
const draft = plan.tier.tier >= 2;
|
|
5325
|
-
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
5326
|
-
await gh.addLabels(pr.number, labels);
|
|
5327
|
-
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
6924
|
+
function suggestionAppliedComment(plan, where2) {
|
|
6925
|
+
return composeSuggestionAppliedComment({ ...where2, payloadSha: plan.payloadSha, ...plan.gate?.forced ? { forced: true } : {} });
|
|
6926
|
+
}
|
|
6927
|
+
function suggestionApplyTable(plan) {
|
|
6928
|
+
const label = { "already-applied": "already applied", apply: "apply", "apply-merged": "apply (merged)", conflict: "**conflict**" };
|
|
6929
|
+
const cell4 = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
6930
|
+
return ["| Change | Result | Detail |", "| --- | --- | --- |", ...plan.ops.map((o) => `| \`${cell4(o.path)}\` | ${label[o.state]} | ${cell4(o.detail)} |`)].join("\n");
|
|
5328
6931
|
}
|
|
5329
6932
|
|
|
5330
6933
|
// ../core/src/node/git-history.ts
|
|
@@ -5333,8 +6936,8 @@ var RS = "";
|
|
|
5333
6936
|
function localGitHistoryBackend(root, config) {
|
|
5334
6937
|
const trees = /* @__PURE__ */ new Map();
|
|
5335
6938
|
const blobs = /* @__PURE__ */ new Map();
|
|
5336
|
-
const
|
|
5337
|
-
const prefix =
|
|
6939
|
+
const tokenRoot2 = config.tokens.root.replace(/\/$/, "");
|
|
6940
|
+
const prefix = tokenRoot2 === "." || !tokenRoot2 ? "" : `${tokenRoot2}/`;
|
|
5338
6941
|
const lsTree = (sha, path) => {
|
|
5339
6942
|
const k = `${sha}:${path}`;
|
|
5340
6943
|
let t = trees.get(k);
|
|
@@ -5371,7 +6974,7 @@ function localGitHistoryBackend(root, config) {
|
|
|
5371
6974
|
});
|
|
5372
6975
|
},
|
|
5373
6976
|
async tokenFiles(sha) {
|
|
5374
|
-
const entries = await lsTree(sha,
|
|
6977
|
+
const entries = await lsTree(sha, tokenRoot2 === "." ? "" : tokenRoot2);
|
|
5375
6978
|
const picked = new Set(selectTokenFiles(config, entries.map((e) => e.path)));
|
|
5376
6979
|
const wanted = entries.filter((e) => picked.has(prefix ? e.path.slice(prefix.length) : e.path));
|
|
5377
6980
|
return new Map(await Promise.all(wanted.map(async (e) => [prefix ? e.path.slice(prefix.length) : e.path, await blob(e.sha)])));
|
|
@@ -6001,14 +7604,14 @@ function scanClassString(text) {
|
|
|
6001
7604
|
const out = [];
|
|
6002
7605
|
for (const m of text.matchAll(/[^\s"'`]+/g)) {
|
|
6003
7606
|
const raw = m[0];
|
|
6004
|
-
let
|
|
7607
|
+
let cut2 = -1;
|
|
6005
7608
|
let depth = 0;
|
|
6006
7609
|
for (let i = 0; i < raw.length; i++) {
|
|
6007
7610
|
if (raw[i] === "[") depth++;
|
|
6008
7611
|
else if (raw[i] === "]") depth--;
|
|
6009
|
-
else if (raw[i] === ":" && depth === 0)
|
|
7612
|
+
else if (raw[i] === ":" && depth === 0) cut2 = i;
|
|
6010
7613
|
}
|
|
6011
|
-
const cls = raw.slice(
|
|
7614
|
+
const cls = raw.slice(cut2 + 1).replace(/^!/, "");
|
|
6012
7615
|
const index = m.index + raw.length - cls.length;
|
|
6013
7616
|
const arb = /^-?([a-z][a-z-]*?)-\[(.+)\]$/.exec(cls);
|
|
6014
7617
|
if (arb) {
|
|
@@ -6182,8 +7785,8 @@ function tailwindFindings(hits, offset, pos, ds) {
|
|
|
6182
7785
|
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
6183
7786
|
if (cat === "color" && !isColor) continue;
|
|
6184
7787
|
const suggestion = ds.suggest(cat, v);
|
|
6185
|
-
const
|
|
6186
|
-
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...
|
|
7788
|
+
const cap3 = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
7789
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap3 ? { suggestion: cap3 } : {} });
|
|
6187
7790
|
}
|
|
6188
7791
|
return { findings, themeRefs };
|
|
6189
7792
|
}
|
|
@@ -6496,16 +8099,16 @@ function applyAllowlist(entries, findings, components) {
|
|
|
6496
8099
|
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
6497
8100
|
}
|
|
6498
8101
|
function parseAllowlist(json) {
|
|
6499
|
-
const
|
|
8102
|
+
const fail3 = (msg) => {
|
|
6500
8103
|
throw new Error(`allowlist: ${msg}`);
|
|
6501
8104
|
};
|
|
6502
|
-
if (!json || typeof json !== "object" || !Array.isArray(json.entries))
|
|
8105
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail3('expected { "entries": [...] }');
|
|
6503
8106
|
return json.entries.map((e, i) => {
|
|
6504
8107
|
const x = e;
|
|
6505
|
-
if (typeof x.file !== "string" || !x.file)
|
|
6506
|
-
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1)
|
|
6507
|
-
if (typeof x.reason !== "string" || !x.reason.trim())
|
|
6508
|
-
if (x.what !== void 0 && typeof x.what !== "string")
|
|
8108
|
+
if (typeof x.file !== "string" || !x.file) fail3(`entry ${i} needs "file"`);
|
|
8109
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail3(`entry ${i} (${x.file}) needs a "line" number`);
|
|
8110
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail3(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
8111
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail3(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
6509
8112
|
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
6510
8113
|
});
|
|
6511
8114
|
}
|
|
@@ -7191,9 +8794,9 @@ var DesignSystemIndex = class {
|
|
|
7191
8794
|
return generic ? this.make(generic, 1, 0.3) : void 0;
|
|
7192
8795
|
}
|
|
7193
8796
|
case "shadow": {
|
|
7194
|
-
const
|
|
8797
|
+
const norm4 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
7195
8798
|
const cands = this.entries(["shadow"]);
|
|
7196
|
-
const exact = cands.find((e) =>
|
|
8799
|
+
const exact = cands.find((e) => norm4(e.css) === norm4(value));
|
|
7197
8800
|
if (exact) return this.make(exact, 0, 1);
|
|
7198
8801
|
const t = shadowNums(value);
|
|
7199
8802
|
const scored = cands.map((e) => {
|
|
@@ -7260,6 +8863,194 @@ function loadAllowlist(path) {
|
|
|
7260
8863
|
return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
|
|
7261
8864
|
}
|
|
7262
8865
|
|
|
8866
|
+
// ../core/src/node/suggestion-apply-node.ts
|
|
8867
|
+
import { execFile } from "node:child_process";
|
|
8868
|
+
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8869
|
+
import { tmpdir } from "node:os";
|
|
8870
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
8871
|
+
function gitCode(root, args) {
|
|
8872
|
+
return new Promise((resolve4) => {
|
|
8873
|
+
execFile("git", args, { cwd: root, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
8874
|
+
const code2 = err ? typeof err.code === "number" ? err.code : -1 : 0;
|
|
8875
|
+
resolve4({ code: code2, stdout, stderr });
|
|
8876
|
+
});
|
|
8877
|
+
});
|
|
8878
|
+
}
|
|
8879
|
+
async function treeEntry(root, ref, path) {
|
|
8880
|
+
const r = await gitCode(root, ["ls-tree", ref, "--", path]);
|
|
8881
|
+
const m = /^(\d+) (\w+) ([0-9a-f]+)\t/.exec(r.stdout);
|
|
8882
|
+
return r.code === 0 && m ? { mode: m[1], type: m[2], sha: m[3] } : null;
|
|
8883
|
+
}
|
|
8884
|
+
var prefixes = (path) => path.split("/").map((_, i, segs) => segs.slice(0, i + 1).join("/"));
|
|
8885
|
+
async function mergeText(root, input) {
|
|
8886
|
+
const dir = await mkdtemp(join2(tmpdir(), "bridge-merge-"));
|
|
8887
|
+
try {
|
|
8888
|
+
const [o, b, t] = ["current", "base", "suggested"].map((n) => join2(dir, n));
|
|
8889
|
+
await Promise.all([writeFile(o, input.ours), writeFile(b, input.base), writeFile(t, input.theirs)]);
|
|
8890
|
+
const r = await gitCode(root, ["merge-file", "-p", "-L", "current", "-L", "base", "-L", "suggested", o, b, t]);
|
|
8891
|
+
if (r.code < 0 || r.code > 127) throw new Error(`git merge-file failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
8892
|
+
return { clean: r.code === 0, text: r.stdout, conflicts: r.code };
|
|
8893
|
+
} finally {
|
|
8894
|
+
await rm(dir, { recursive: true, force: true });
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8897
|
+
async function fileDiff(path, before, after) {
|
|
8898
|
+
if (before === after) return "";
|
|
8899
|
+
const dir = await mkdtemp(join2(tmpdir(), "bridge-diff-"));
|
|
8900
|
+
try {
|
|
8901
|
+
const side = async (name, text) => {
|
|
8902
|
+
if (text === null) return "/dev/null";
|
|
8903
|
+
const p = join2(dir, name, path);
|
|
8904
|
+
await mkdir(dirname2(p), { recursive: true });
|
|
8905
|
+
await writeFile(p, text);
|
|
8906
|
+
return `${name}/${path}`;
|
|
8907
|
+
};
|
|
8908
|
+
const a = await side("a", before);
|
|
8909
|
+
const b = await side("b", after);
|
|
8910
|
+
const r = await new Promise((resolve4) => execFile("git", ["diff", "--no-index", "--no-color", "--src-prefix=", "--dst-prefix=", "--", a, b], { cwd: dir, maxBuffer: 32 * 1024 * 1024 }, (_e, stdout) => resolve4({ stdout })));
|
|
8911
|
+
return r.stdout;
|
|
8912
|
+
} finally {
|
|
8913
|
+
await rm(dir, { recursive: true, force: true });
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
function gitWorkspace(root, opts = {}) {
|
|
8917
|
+
const { ref } = opts;
|
|
8918
|
+
let fetched = false;
|
|
8919
|
+
return {
|
|
8920
|
+
async read(path) {
|
|
8921
|
+
if (ref) {
|
|
8922
|
+
const e = await treeEntry(root, ref, path);
|
|
8923
|
+
if (!e || e.type !== "blob" || e.mode === "120000") return null;
|
|
8924
|
+
return { text: await git(root, ["cat-file", "blob", e.sha]), sha: e.sha };
|
|
8925
|
+
}
|
|
8926
|
+
try {
|
|
8927
|
+
const st = await lstat(join2(root, path));
|
|
8928
|
+
if (!st.isFile()) return null;
|
|
8929
|
+
const text = await readFile(join2(root, path), "utf8");
|
|
8930
|
+
return { text, sha: gitBlobSha(text) };
|
|
8931
|
+
} catch {
|
|
8932
|
+
return null;
|
|
8933
|
+
}
|
|
8934
|
+
},
|
|
8935
|
+
async readAt(at, path) {
|
|
8936
|
+
if ((await gitCode(root, ["cat-file", "-e", `${at}^{commit}`])).code !== 0 && !fetched) {
|
|
8937
|
+
fetched = true;
|
|
8938
|
+
await gitCode(root, ["fetch", "--no-tags", "--quiet", "origin", at]);
|
|
8939
|
+
}
|
|
8940
|
+
const e = await treeEntry(root, at, path);
|
|
8941
|
+
if (e?.type === "blob") return git(root, ["cat-file", "blob", e.sha]);
|
|
8942
|
+
return opts.readBase ? opts.readBase(path, at) : null;
|
|
8943
|
+
},
|
|
8944
|
+
async list() {
|
|
8945
|
+
if (ref) return listFilesAt(root, ref);
|
|
8946
|
+
const out = await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
8947
|
+
const all = [...new Set(out.split("\0").filter(Boolean))];
|
|
8948
|
+
const present = await Promise.all(all.map((p) => lstat(join2(root, p)).then((s) => s.isFile(), () => false)));
|
|
8949
|
+
return all.filter((_, i) => present[i]);
|
|
8950
|
+
},
|
|
8951
|
+
async isSymlink(path) {
|
|
8952
|
+
for (const p of prefixes(path)) {
|
|
8953
|
+
if (ref) {
|
|
8954
|
+
const e = await treeEntry(root, ref, p);
|
|
8955
|
+
if (!e) return false;
|
|
8956
|
+
if (e.mode === "120000") return true;
|
|
8957
|
+
} else {
|
|
8958
|
+
try {
|
|
8959
|
+
if ((await lstat(join2(root, p))).isSymbolicLink()) return true;
|
|
8960
|
+
} catch {
|
|
8961
|
+
return false;
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
return false;
|
|
8966
|
+
},
|
|
8967
|
+
merge: (input) => mergeText(root, input)
|
|
8968
|
+
};
|
|
8969
|
+
}
|
|
8970
|
+
var COMMIT_TYPE = { "token-change": "feat(tokens)", docs: "docs", "component-spec": "feat", brand: "feat(tokens)", revert: "revert", config: "chore(config)" };
|
|
8971
|
+
function commitMessage(plan, n) {
|
|
8972
|
+
const p = plan.payload;
|
|
8973
|
+
const type = `${COMMIT_TYPE[p.kind] ?? "chore"}${p.tier.bump === "major" ? "!" : ""}`;
|
|
8974
|
+
return [`${type}: ${p.title.trim()}`, "", p.summary.trim(), "", `Applies Bridge suggestion #${n} (version ${plan.payloadSha.slice(0, 7)}).`].filter((l, i, a) => !(l === "" && a[i - 1] === "")).join("\n");
|
|
8975
|
+
}
|
|
8976
|
+
async function readConfigAt(ws) {
|
|
8977
|
+
const text = (await ws.read(CONFIG_FILE))?.text;
|
|
8978
|
+
if (!text) throw new Error(`No ${CONFIG_FILE} here: run \`bridge init\` first, or apply from the repository root.`);
|
|
8979
|
+
return loadConfig(text);
|
|
8980
|
+
}
|
|
8981
|
+
async function applySuggestionLocally(input) {
|
|
8982
|
+
const mode = input.mode ?? "branch";
|
|
8983
|
+
const base = input.base ?? "HEAD";
|
|
8984
|
+
const { root } = input;
|
|
8985
|
+
const wsOpts = { readBase: input.readBase };
|
|
8986
|
+
const ws = gitWorkspace(root, mode === "write" ? wsOpts : { ...wsOpts, ref: base });
|
|
8987
|
+
const config = input.config ?? await readConfigAt(ws);
|
|
8988
|
+
const planFor = (workspace) => planSuggestionApply({ ...input, config, workspace });
|
|
8989
|
+
const plan = await planFor(ws);
|
|
8990
|
+
const at = (input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
8991
|
+
const result = (status, rest = {}) => ({ status, exitCode: plan.exitCode, message: plan.message, plan, ...rest });
|
|
8992
|
+
if (plan.outcome !== "ready") return result(plan.outcome);
|
|
8993
|
+
const readNow = async (path) => (await ws.read(path))?.text ?? null;
|
|
8994
|
+
const diffOf = async () => (await Promise.all(plan.writes.map(async (w) => fileDiff(w.path, await readNow(w.path), w.content)))).join("");
|
|
8995
|
+
if (input.dryRun) return result("dry-run", { message: `Dry run: ${plan.message.replace(/^Ready: /, "")} Nothing written.`, diff: await diffOf() });
|
|
8996
|
+
if (mode === "patch") {
|
|
8997
|
+
const { sha } = await commitTree(root, { base, message: commitMessage(plan, input.number), files: plan.writes });
|
|
8998
|
+
const patch = await git(root, ["format-patch", "-1", "--stdout", sha]);
|
|
8999
|
+
return result("patch", { message: "Patch ready: apply it with `git am`.", patch, commit: sha });
|
|
9000
|
+
}
|
|
9001
|
+
if (mode === "write") {
|
|
9002
|
+
const paths = plan.writes.map((w) => w.path);
|
|
9003
|
+
const dirty = (await git(root, ["status", "--porcelain", "-z", "--", ...paths])).split("\0").filter((l) => l.length > 3).map((l) => l.slice(3));
|
|
9004
|
+
if (dirty.length) {
|
|
9005
|
+
return result("refused", {
|
|
9006
|
+
exitCode: 1,
|
|
9007
|
+
message: `Not written: these files have uncommitted changes: ${dirty.join(", ")}. Commit or stash them, or apply on a branch instead.`
|
|
9008
|
+
});
|
|
9009
|
+
}
|
|
9010
|
+
const diff = await diffOf();
|
|
9011
|
+
for (const w of plan.writes) {
|
|
9012
|
+
const full = join2(root, w.path);
|
|
9013
|
+
if (w.content === null) await rm(full, { force: true });
|
|
9014
|
+
else {
|
|
9015
|
+
await mkdir(dirname2(full), { recursive: true });
|
|
9016
|
+
await writeFile(full, w.content);
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
return result("written", {
|
|
9020
|
+
message: `Written: ${paths.length} file(s) in the working tree. Review them, commit, and say "Closes #${input.number}" in the pull request.`,
|
|
9021
|
+
written: paths,
|
|
9022
|
+
diff,
|
|
9023
|
+
appliedComment: suggestionAppliedComment(plan, { by: input.by, at })
|
|
9024
|
+
});
|
|
9025
|
+
}
|
|
9026
|
+
const prefix = config.governance.branchPrefix;
|
|
9027
|
+
const wanted = input.branch ?? `${prefix}suggestion-${input.number}-${slugify(plan.payload.title, 40)}`;
|
|
9028
|
+
let branch = wanted;
|
|
9029
|
+
for (let i = 2; await branchExists(root, branch); i++) {
|
|
9030
|
+
const there = await planFor(gitWorkspace(root, { ...wsOpts, ref: branch }));
|
|
9031
|
+
if (there.outcome === "already-applied") {
|
|
9032
|
+
const commit = (await git(root, ["rev-parse", `refs/heads/${branch}`])).trim();
|
|
9033
|
+
return result("branch-exists", {
|
|
9034
|
+
exitCode: 0,
|
|
9035
|
+
message: `Already on ${branch}: that branch has every change of this suggestion. Push it and open a pull request that says "Closes #${input.number}".`,
|
|
9036
|
+
branch,
|
|
9037
|
+
commit,
|
|
9038
|
+
appliedComment: suggestionAppliedComment(plan, { by: input.by, branch, commit, at })
|
|
9039
|
+
});
|
|
9040
|
+
}
|
|
9041
|
+
branch = `${wanted}-${i}`;
|
|
9042
|
+
}
|
|
9043
|
+
const protectedBranches = [.../* @__PURE__ */ new Set([...protectedBranchesOf(config), baseBranchOf(config)])];
|
|
9044
|
+
const res = await commitToNewLocalBranch(root, { base, branch, message: commitMessage(plan, input.number), files: plan.writes, protectedBranches, branchPrefix: prefix });
|
|
9045
|
+
return result("branch-created", {
|
|
9046
|
+
message: `Applied on ${branch}, your working tree is untouched. Push it and open a pull request that says "Closes #${input.number}".`,
|
|
9047
|
+
branch,
|
|
9048
|
+
commit: res.sha,
|
|
9049
|
+
diff: res.diff,
|
|
9050
|
+
appliedComment: suggestionAppliedComment(plan, { by: input.by, branch, commit: res.sha, at })
|
|
9051
|
+
});
|
|
9052
|
+
}
|
|
9053
|
+
|
|
7263
9054
|
// src/config.ts
|
|
7264
9055
|
var SCHEMA_URL = "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json";
|
|
7265
9056
|
var TIER_NAMES = {
|
|
@@ -7419,10 +9210,10 @@ var core = {
|
|
|
7419
9210
|
};
|
|
7420
9211
|
|
|
7421
9212
|
// src/source.ts
|
|
7422
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
7423
|
-
import { join as
|
|
9213
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
9214
|
+
import { join as join3 } from "node:path";
|
|
7424
9215
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", "storybook-static", ".turbo", ".next", ".astro"]);
|
|
7425
|
-
var
|
|
9216
|
+
var under2 = (prefix) => (f) => !prefix || prefix === "." || f === prefix || f.startsWith(`${prefix.replace(/\/$/, "")}/`);
|
|
7426
9217
|
var LocalSource = class {
|
|
7427
9218
|
constructor(root) {
|
|
7428
9219
|
this.root = root;
|
|
@@ -7438,7 +9229,7 @@ var LocalSource = class {
|
|
|
7438
9229
|
const walk2 = async (rel) => {
|
|
7439
9230
|
let entries;
|
|
7440
9231
|
try {
|
|
7441
|
-
entries = await readdir(
|
|
9232
|
+
entries = await readdir(join3(this.root, rel), { withFileTypes: true });
|
|
7442
9233
|
} catch {
|
|
7443
9234
|
return;
|
|
7444
9235
|
}
|
|
@@ -7454,7 +9245,7 @@ var LocalSource = class {
|
|
|
7454
9245
|
}
|
|
7455
9246
|
async read(path) {
|
|
7456
9247
|
try {
|
|
7457
|
-
return await
|
|
9248
|
+
return await readFile2(join3(this.root, path), "utf8");
|
|
7458
9249
|
} catch {
|
|
7459
9250
|
return null;
|
|
7460
9251
|
}
|
|
@@ -7474,7 +9265,7 @@ var GitRefSource = class {
|
|
|
7474
9265
|
}
|
|
7475
9266
|
async list(prefix = "") {
|
|
7476
9267
|
this.files ??= listFilesAt(this.root, this.ref);
|
|
7477
|
-
return (await this.files).filter(
|
|
9268
|
+
return (await this.files).filter(under2(prefix));
|
|
7478
9269
|
}
|
|
7479
9270
|
read(path) {
|
|
7480
9271
|
return showFile(this.root, this.ref, path);
|
|
@@ -7495,7 +9286,7 @@ var GitHubSource = class {
|
|
|
7495
9286
|
}
|
|
7496
9287
|
async list(prefix = "") {
|
|
7497
9288
|
this.tree ??= this.client.listFiles(this.ref);
|
|
7498
|
-
return (await this.tree).filter(
|
|
9289
|
+
return (await this.tree).filter(under2(prefix));
|
|
7499
9290
|
}
|
|
7500
9291
|
read(path) {
|
|
7501
9292
|
let p = this.cache.get(path);
|
|
@@ -7517,7 +9308,7 @@ function client(repo, o, rules) {
|
|
|
7517
9308
|
...o.rawUrl ? { rawUrl: o.rawUrl } : {},
|
|
7518
9309
|
protectedBranches: rules.protectedBranches,
|
|
7519
9310
|
branchPrefix: rules.branchPrefix,
|
|
7520
|
-
tokenHint: "set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN) in the environment of the Bridge MCP server
|
|
9311
|
+
tokenHint: "set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN) in the environment of the Bridge MCP server: read access to this repository is enough to read, suggestions and requests need Issues: read and write, and direct changes (branches and pull requests) need Contents and Pull requests: read and write"
|
|
7521
9312
|
});
|
|
7522
9313
|
}
|
|
7523
9314
|
var BridgeContext = class _BridgeContext {
|
|
@@ -7939,8 +9730,8 @@ __export(ops_exports, {
|
|
|
7939
9730
|
visualImpact: () => visualImpact
|
|
7940
9731
|
});
|
|
7941
9732
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
7942
|
-
import { mkdir as
|
|
7943
|
-
import { dirname as
|
|
9733
|
+
import { mkdir as mkdir3, writeFile as writeFile3, access } from "node:fs/promises";
|
|
9734
|
+
import { dirname as dirname4, isAbsolute, join as join5, resolve as resolve3 } from "node:path";
|
|
7944
9735
|
|
|
7945
9736
|
// ../core/src/audit/missing-pieces.ts
|
|
7946
9737
|
var lines = (text) => text.split(/\r?\n/);
|
|
@@ -8012,7 +9803,7 @@ function aspectOf(values) {
|
|
|
8012
9803
|
for (const a of ["tone", "shape", "size"]) if (values.some((v) => ASPECT[v.category] === a)) return a;
|
|
8013
9804
|
return "style";
|
|
8014
9805
|
}
|
|
8015
|
-
var
|
|
9806
|
+
var norm3 = (f) => f.category === "color" ? f.value.toLowerCase() : f.value.replace(/\s+/g, "");
|
|
8016
9807
|
function nearestOf(s) {
|
|
8017
9808
|
if (!s) return void 0;
|
|
8018
9809
|
return { token: s.token, cssVar: s.cssVar, value: s.value, level: s.level, label: tokenLabel(s.token) };
|
|
@@ -8099,7 +9890,7 @@ function missingPieces(input) {
|
|
|
8099
9890
|
variants.set(key2, v);
|
|
8100
9891
|
continue;
|
|
8101
9892
|
}
|
|
8102
|
-
const key = `${finding.category}|${
|
|
9893
|
+
const key = `${finding.category}|${norm3(finding)}`;
|
|
8103
9894
|
const t = tokens.get(key) ?? { category: finding.category, value: finding.value, name, hits: [] };
|
|
8104
9895
|
if (!t.name && name) t.name = name;
|
|
8105
9896
|
t.hits.push(hit);
|
|
@@ -8117,13 +9908,13 @@ function missingPieces(input) {
|
|
|
8117
9908
|
foreign.set(c.name, e);
|
|
8118
9909
|
}
|
|
8119
9910
|
}
|
|
8120
|
-
const
|
|
9911
|
+
const where2 = (hits) => hits.slice(0, 5).map((h) => ({ file: h.file, line: h.line }));
|
|
8121
9912
|
const pieces = [];
|
|
8122
9913
|
for (const v of variants.values()) {
|
|
8123
9914
|
const values = [];
|
|
8124
9915
|
for (const h of v.hits) {
|
|
8125
9916
|
const pv = { category: h.finding.category, ...h.finding.property ? { property: h.finding.property } : {}, value: h.finding.value };
|
|
8126
|
-
if (!values.some((x) => x.category === pv.category && x.property === pv.property &&
|
|
9917
|
+
if (!values.some((x) => x.category === pv.category && x.property === pv.property && norm3(x) === norm3(pv))) values.push(pv);
|
|
8127
9918
|
}
|
|
8128
9919
|
const aspect = aspectOf(values);
|
|
8129
9920
|
const bg = v.hits.find((h) => /background/.test(h.finding.property ?? ""))?.finding.suggestion;
|
|
@@ -8139,7 +9930,7 @@ function missingPieces(input) {
|
|
|
8139
9930
|
aspect,
|
|
8140
9931
|
values,
|
|
8141
9932
|
...nearest ? { nearest } : {},
|
|
8142
|
-
where:
|
|
9933
|
+
where: where2(v.hits)
|
|
8143
9934
|
});
|
|
8144
9935
|
}
|
|
8145
9936
|
for (const [name, e] of foreign) {
|
|
@@ -8151,7 +9942,7 @@ function missingPieces(input) {
|
|
|
8151
9942
|
name,
|
|
8152
9943
|
component: name.split(".").pop(),
|
|
8153
9944
|
values: [],
|
|
8154
|
-
where:
|
|
9945
|
+
where: where2(e.hits)
|
|
8155
9946
|
});
|
|
8156
9947
|
}
|
|
8157
9948
|
for (const t of tokens.values()) {
|
|
@@ -8168,7 +9959,7 @@ function missingPieces(input) {
|
|
|
8168
9959
|
name: pname,
|
|
8169
9960
|
values: [{ category: t.category, ...t.hits[0].finding.property ? { property: t.hits[0].finding.property } : {}, value: t.value }],
|
|
8170
9961
|
...nearest ? { nearest } : {},
|
|
8171
|
-
where:
|
|
9962
|
+
where: where2(t.hits),
|
|
8172
9963
|
token: { path, type: TOKEN_TYPE[t.category] ?? "dimension", value: t.value, files: tokenFiles }
|
|
8173
9964
|
});
|
|
8174
9965
|
}
|
|
@@ -8319,8 +10110,8 @@ __export(explorations_exports, {
|
|
|
8319
10110
|
toExplorationBranch: () => toExplorationBranch,
|
|
8320
10111
|
writeToExploration: () => writeToExploration
|
|
8321
10112
|
});
|
|
8322
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
8323
|
-
import { dirname as
|
|
10113
|
+
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
10114
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
8324
10115
|
|
|
8325
10116
|
// src/branches.ts
|
|
8326
10117
|
var branches_exports = {};
|
|
@@ -8476,19 +10267,19 @@ async function whatNeedsMe(ctx, a = {}) {
|
|
|
8476
10267
|
const pulls = await gh.listPullRequests("open");
|
|
8477
10268
|
const queue = reviewQueue(pulls.map((p) => ({ ...p, state: p.state, head: { ref: p.headRef }, user: { login: p.author }, labels: p.labels.map((name) => ({ name })) }))).slice(0, a.limit ?? 20);
|
|
8478
10269
|
const who = await people(gh, config);
|
|
8479
|
-
const
|
|
10270
|
+
const same3 = (x, y) => Boolean(y && x.toLowerCase() === y.toLowerCase());
|
|
8480
10271
|
const rows = await Promise.all(
|
|
8481
10272
|
queue.map(async (p) => {
|
|
8482
10273
|
const f = await branchFacts(ctx, gh, p.number, viewer).catch(() => null);
|
|
8483
|
-
if (!f) return { pr: p.number, title: p.title, url: p.url, author: p.author, status: "unknown", statusLabel: STATUS_WORDS.unknown, yours:
|
|
8484
|
-
const mine =
|
|
8485
|
-
const inDesign = viewer ? who.lists.design.some((x) =>
|
|
8486
|
-
const inTech = viewer ? who.lists.tech.some((x) =>
|
|
10274
|
+
if (!f) return { pr: p.number, title: p.title, url: p.url, author: p.author, status: "unknown", statusLabel: STATUS_WORDS.unknown, yours: same3(p.author, viewer), needsYou: null };
|
|
10275
|
+
const mine = same3(p.author, viewer);
|
|
10276
|
+
const inDesign = viewer ? who.lists.design.some((x) => same3(x, viewer)) : false;
|
|
10277
|
+
const inTech = viewer ? who.lists.tech.some((x) => same3(x, viewer)) : false;
|
|
8487
10278
|
let needsYou = null;
|
|
8488
10279
|
if (viewer && !mine) {
|
|
8489
10280
|
if (f.status === "needs-design" && inDesign) needsYou = "Your design approval";
|
|
8490
10281
|
else if (f.status === "needs-tech" && inTech) needsYou = "Your tech approval";
|
|
8491
|
-
else if (f.status === "needs-approval" && !f.approvals.approvers.some((x) =>
|
|
10282
|
+
else if (f.status === "needs-approval" && !f.approvals.approvers.some((x) => same3(x, viewer))) needsYou = "Your approval";
|
|
8492
10283
|
else if (f.status === "ready" && f.gate.canShip) needsYou = "Ready for you to merge";
|
|
8493
10284
|
}
|
|
8494
10285
|
if (viewer && mine) {
|
|
@@ -8945,11 +10736,11 @@ async function writeToExploration(ctx, branch, input) {
|
|
|
8945
10736
|
};
|
|
8946
10737
|
}
|
|
8947
10738
|
for (const f of input.files) {
|
|
8948
|
-
const abs =
|
|
8949
|
-
if (f.content === null) await
|
|
10739
|
+
const abs = join4(ctx.root, f.path);
|
|
10740
|
+
if (f.content === null) await rm2(abs, { force: true });
|
|
8950
10741
|
else {
|
|
8951
|
-
await
|
|
8952
|
-
await
|
|
10742
|
+
await mkdir2(dirname3(abs), { recursive: true });
|
|
10743
|
+
await writeFile2(abs, f.content);
|
|
8953
10744
|
}
|
|
8954
10745
|
}
|
|
8955
10746
|
const paths = input.files.map((f) => f.path);
|
|
@@ -8960,6 +10751,379 @@ async function writeToExploration(ctx, branch, input) {
|
|
|
8960
10751
|
};
|
|
8961
10752
|
}
|
|
8962
10753
|
|
|
10754
|
+
// src/suggestions.ts
|
|
10755
|
+
var suggestions_exports = {};
|
|
10756
|
+
__export(suggestions_exports, {
|
|
10757
|
+
SUGGEST_RIGHTS: () => SUGGEST_RIGHTS,
|
|
10758
|
+
appliedNext: () => appliedNext,
|
|
10759
|
+
applySuggestionOp: () => applySuggestionOp,
|
|
10760
|
+
changesFor: () => changesFor,
|
|
10761
|
+
createSuggestionOp: () => createSuggestionOp,
|
|
10762
|
+
fileSuggestion: () => fileSuggestion,
|
|
10763
|
+
filesSuggestionPayload: () => filesSuggestionPayload,
|
|
10764
|
+
getSuggestionOp: () => getSuggestionOp,
|
|
10765
|
+
githubWorkspace: () => githubWorkspace,
|
|
10766
|
+
linkSuggestionOp: () => linkSuggestionOp,
|
|
10767
|
+
listSuggestionsOp: () => listSuggestionsOp,
|
|
10768
|
+
noSuggestionAccess: () => noSuggestionAccess,
|
|
10769
|
+
reviewSuggestionOp: () => reviewSuggestionOp,
|
|
10770
|
+
suggestionBase: () => suggestionBase,
|
|
10771
|
+
suggestionGovernance: () => suggestionGovernance,
|
|
10772
|
+
suggestionNumber: () => suggestionNumber
|
|
10773
|
+
});
|
|
10774
|
+
var SUGGEST_RIGHTS = "Issues: read and write (Contents: read is enough)";
|
|
10775
|
+
async function changesFor(ctx, explicit) {
|
|
10776
|
+
if (explicit === "suggest" || explicit === "direct") return explicit;
|
|
10777
|
+
const { config } = await ctx.config();
|
|
10778
|
+
return changesMode(config);
|
|
10779
|
+
}
|
|
10780
|
+
var appliedNext = (n) => `The owners review it on the issue (bridge_review_suggestion, or Bridge Studio). Once approved, a developer applies it: \`bridge apply ${n}\` in the repo, or in Claude Code with the Bridge MCP server, "Apply Bridge suggestion #${n}" (bridge_apply_suggestion).`;
|
|
10781
|
+
function noSuggestionAccess(ctx, gh) {
|
|
10782
|
+
const local = ctx.mode === "local" && ctx.isGit ? ', or pass changes "direct" (the CLI: --direct) to commit it on a new local branch instead' : "";
|
|
10783
|
+
if (!gh) return `Nothing sent: a suggestion is a GitHub issue and no GitHub repo is known. Set "repo" in ${CONFIG_FILE} or add an origin remote${local}.`;
|
|
10784
|
+
return `Nothing sent: a suggestion is a GitHub issue, which needs a GitHub token with ${SUGGEST_RIGHTS} on ${gh.fullName}. Set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN)${local}.`;
|
|
10785
|
+
}
|
|
10786
|
+
async function suggestionBase(ctx, gh, config) {
|
|
10787
|
+
const ref = ctx.ref ?? baseBranchOf(config);
|
|
10788
|
+
if (ctx.mode === "local" && ctx.isGit && ctx.root) {
|
|
10789
|
+
const sha2 = (await git(ctx.root, ["rev-parse", "HEAD"]).catch(() => "")).trim();
|
|
10790
|
+
if (/^[0-9a-f]{40}$/.test(sha2)) return { ref: baseBranchOf(config), sha: sha2 };
|
|
10791
|
+
}
|
|
10792
|
+
const sha = await gh.getBranchSha(ref);
|
|
10793
|
+
if (!sha) throw new Error(`Branch ${ref} not found on ${gh.fullName}. Check the default branch in ${CONFIG_FILE}.`);
|
|
10794
|
+
return { ref, sha };
|
|
10795
|
+
}
|
|
10796
|
+
async function filesSuggestionPayload(ctx, input) {
|
|
10797
|
+
const files = await Promise.all(
|
|
10798
|
+
input.files.map(async (f) => {
|
|
10799
|
+
const now = await ctx.source.read(f.path);
|
|
10800
|
+
return { path: f.path, beforeSha: now === null ? null : gitBlobSha(now), afterSha: f.content === null ? null : gitBlobSha(f.content), content: f.content };
|
|
10801
|
+
})
|
|
10802
|
+
);
|
|
10803
|
+
return {
|
|
10804
|
+
schema: SUGGESTION_SCHEMA,
|
|
10805
|
+
kind: input.kind,
|
|
10806
|
+
title: input.title,
|
|
10807
|
+
summary: input.summary.trim() || input.title,
|
|
10808
|
+
source: input.source,
|
|
10809
|
+
...input.suggestedBy ? { suggestedBy: input.suggestedBy.replace(/^@/, "") } : {},
|
|
10810
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10811
|
+
base: input.base,
|
|
10812
|
+
tier: input.tier,
|
|
10813
|
+
files,
|
|
10814
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0, ...input.checks }
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
async function fileSuggestion(ctx, build, opts = {}) {
|
|
10818
|
+
const { config } = await ctx.config();
|
|
10819
|
+
const gh = await ctx.github();
|
|
10820
|
+
if (opts.dryRun) {
|
|
10821
|
+
const base2 = gh ? await suggestionBase(ctx, gh, config).catch(() => ({ ref: baseBranchOf(config), sha: "0000000" })) : { ref: baseBranchOf(config), sha: "0000000" };
|
|
10822
|
+
const payload2 = await build(base2, null);
|
|
10823
|
+
const issue = composeSuggestionIssue(payload2, { labels: [...config.governance.labels, ...opts.labels ?? []], details: opts.details, repo: gh?.fullName });
|
|
10824
|
+
return { status: "dry-run", suggestion: { title: issue.title, labels: issue.labels, body: issue.body, parts: issue.parts.length } };
|
|
10825
|
+
}
|
|
10826
|
+
if (!gh || !ctx.canWriteRemote) return { status: "preview", message: noSuggestionAccess(ctx, gh) };
|
|
10827
|
+
const base = await suggestionBase(ctx, gh, config);
|
|
10828
|
+
const viewer = await viewerOf(ctx, gh);
|
|
10829
|
+
const payload = await build(base, viewer);
|
|
10830
|
+
const problems = suggestionPathProblems(payload, config);
|
|
10831
|
+
if (problems.length) throw new Error(`Not suggested: ${problems.join(" ")}`);
|
|
10832
|
+
const labels = [.../* @__PURE__ */ new Set([...config.governance.labels, ...opts.labels ?? []])];
|
|
10833
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels, details: opts.details, assignees: config.integrations?.requests?.assignees ?? [] });
|
|
10834
|
+
return { status: "suggested", number: created.number, url: created.url, sha: created.sha, labels, warnings: created.warnings, next: appliedNext(created.number) };
|
|
10835
|
+
}
|
|
10836
|
+
function suggestionNumber(input) {
|
|
10837
|
+
if (typeof input === "number" && Number.isInteger(input) && input > 0) return input;
|
|
10838
|
+
const m = /^#?(\d+)$/.exec(String(input).trim()) ?? /\/issues\/(\d+)/.exec(String(input));
|
|
10839
|
+
if (!m) throw new Error(`"${input}" is not a suggestion number. Pass the issue number (42 or "#42") or its link.`);
|
|
10840
|
+
return Number(m[1]);
|
|
10841
|
+
}
|
|
10842
|
+
async function suggestionGovernance(ctx, gh) {
|
|
10843
|
+
const { config: here } = await ctx.config();
|
|
10844
|
+
let config = here;
|
|
10845
|
+
try {
|
|
10846
|
+
const text = await gh.readFile(CONFIG_FILE, baseBranchOf(here));
|
|
10847
|
+
if (text) config = loadConfig(text);
|
|
10848
|
+
} catch {
|
|
10849
|
+
}
|
|
10850
|
+
const { owners, rights } = config.governance;
|
|
10851
|
+
const hasTeams = [...owners.design, ...owners.tech].some((o) => o.includes("/"));
|
|
10852
|
+
const teams = hasTeams ? await new TeamDirectory(gh).resolve(owners).catch(() => void 0) : void 0;
|
|
10853
|
+
return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {} } };
|
|
10854
|
+
}
|
|
10855
|
+
var OPEN_STATUSES = ["needs-review", "changes-requested", "approved", "applying", "outdated"];
|
|
10856
|
+
async function listSuggestionsOp(ctx, a = {}) {
|
|
10857
|
+
const gh = await reader(ctx);
|
|
10858
|
+
const status = a.status ?? "all";
|
|
10859
|
+
const state = a.state ?? (status === "all" ? "open" : OPEN_STATUSES.includes(status) ? "open" : "closed");
|
|
10860
|
+
let rows = await listSuggestionsOnGitHub(gh, { state, perPage: 100 });
|
|
10861
|
+
if (status !== "all") rows = rows.filter((r) => r.status === status);
|
|
10862
|
+
if (a.kind) rows = rows.filter((r) => r.kind === a.kind);
|
|
10863
|
+
const total = rows.length;
|
|
10864
|
+
rows = rows.slice(0, a.limit ?? 100);
|
|
10865
|
+
const approved = rows.filter((r) => r.status === "approved");
|
|
10866
|
+
return {
|
|
10867
|
+
repo: gh.fullName,
|
|
10868
|
+
label: SUGGESTION_LABEL,
|
|
10869
|
+
state,
|
|
10870
|
+
status,
|
|
10871
|
+
total,
|
|
10872
|
+
suggestions: rows.map((r) => ({
|
|
10873
|
+
number: r.number,
|
|
10874
|
+
title: r.title,
|
|
10875
|
+
kind: r.kind ?? null,
|
|
10876
|
+
tier: r.tier ?? null,
|
|
10877
|
+
status: r.status,
|
|
10878
|
+
said: SUGGESTION_STATUS_LABEL[r.status],
|
|
10879
|
+
author: r.author,
|
|
10880
|
+
url: r.url,
|
|
10881
|
+
linkedPrs: r.linkedPrs
|
|
10882
|
+
})),
|
|
10883
|
+
next: approved.length ? `Ready to apply: ${approved.map((r) => `#${r.number}`).join(", ")}. Apply one with bridge_apply_suggestion { number } (dry run first), or \`bridge apply <n>\`.` : rows.length ? "Read one with bridge_get_suggestion { number }: its change, checks and who still has to approve it. Statuses come from labels here; bridge_get_suggestion counts the approvals." : "No suggestion matches."
|
|
10884
|
+
};
|
|
10885
|
+
}
|
|
10886
|
+
var roleText = (m) => m === "approval" ? "one approval" : `${m} approval`;
|
|
10887
|
+
async function getSuggestionOp(ctx, a) {
|
|
10888
|
+
const n = suggestionNumber(a.number);
|
|
10889
|
+
const gh = await reader(ctx);
|
|
10890
|
+
const { gov } = await suggestionGovernance(ctx, gh);
|
|
10891
|
+
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
10892
|
+
const base = { number: n, url: d.issue.url, title: d.issue.title, state: d.issue.state, status: d.status, said: SUGGESTION_STATUS_LABEL[d.status], author: d.issue.author, labels: d.issue.labels, linkedPrs: d.linkedPrs };
|
|
10893
|
+
if (!d.parsed.ok) {
|
|
10894
|
+
if (!d.issue.labels.includes(SUGGESTION_LABEL) && d.parsed.reason === "no-block") throw new Error(`Issue #${n} is not a Bridge suggestion: it has no "${SUGGESTION_LABEL}" label and no suggestion block.`);
|
|
10895
|
+
return { ...base, problem: d.parsed.message, next: "Ask the author to send it again from Bridge." };
|
|
10896
|
+
}
|
|
10897
|
+
const p = d.parsed.payload;
|
|
10898
|
+
const approvals = d.approvals;
|
|
10899
|
+
const missing = pendingApprovals(p.tier.tier, approvals);
|
|
10900
|
+
return {
|
|
10901
|
+
...base,
|
|
10902
|
+
kind: p.kind,
|
|
10903
|
+
summary: p.summary,
|
|
10904
|
+
source: p.source,
|
|
10905
|
+
...p.suggestedBy ? { suggestedBy: p.suggestedBy } : {},
|
|
10906
|
+
createdAt: p.createdAt,
|
|
10907
|
+
base: p.base,
|
|
10908
|
+
tier: p.tier.tier,
|
|
10909
|
+
bump: p.tier.bump,
|
|
10910
|
+
version: d.parsed.sha.slice(0, 7),
|
|
10911
|
+
payloadSha: d.parsed.sha,
|
|
10912
|
+
...d.parsed.edited ? { edited: "The machine-readable block was edited by hand after Bridge wrote it. Applying it needs acceptEdited (the CLI: --accept-edited) once you checked it." } : {},
|
|
10913
|
+
change: {
|
|
10914
|
+
...p.tokens ? { tokens: p.tokens.changes.length ? p.tokens.changes.map((c) => ({ kind: c.kind, path: c.path, ...c.from ? { from: c.from } : {}, file: c.file, before: c.beforeCss ?? c.before, after: c.afterCss ?? c.after })) : p.tokens.edits } : {},
|
|
10915
|
+
...p.docs ? { docs: p.docs.map((x) => ({ component: x.component, path: x.path })) } : {},
|
|
10916
|
+
...p.componentSpec ? { componentSpec: { component: p.componentSpec.component, specPath: p.componentSpec.specPath } } : {},
|
|
10917
|
+
...p.files?.length ? { files: p.files.map((f) => ({ path: f.path, change: f.beforeSha === null ? "new" : f.afterSha === null ? "deleted" : "changed" })) } : {}
|
|
10918
|
+
},
|
|
10919
|
+
checks: p.checks,
|
|
10920
|
+
...p.links ? { links: p.links } : {},
|
|
10921
|
+
...p.previews?.length ? { previews: p.previews } : {},
|
|
10922
|
+
approvals: {
|
|
10923
|
+
approvedBy: approvals.approvers,
|
|
10924
|
+
missing: missing.map(roleText),
|
|
10925
|
+
changesRequestedBy: approvals.changesRequested,
|
|
10926
|
+
...approvals.stale.length ? { staleReviews: approvals.stale } : {},
|
|
10927
|
+
...approvals.ignored.length ? { notCounted: approvals.ignored } : {}
|
|
10928
|
+
},
|
|
10929
|
+
applied: d.applied,
|
|
10930
|
+
...p.componentSpec ? { brief: p.componentSpec.brief } : {},
|
|
10931
|
+
next: d.status === "approved" ? `Approved. Apply it: bridge_apply_suggestion { number: ${n}, mode: "dry-run" } first, then mode "branch" (or \`bridge apply ${n}\`). The pull request must say "Closes #${n}".` : d.status === "needs-review" ? `Waiting for ${missing.map(roleText).join(" and ") || "a review"} from the owners in ${CONFIG_FILE}: bridge_review_suggestion { number: ${n}, event: "approve" }.` : d.status === "changes-requested" ? "Changes requested: the author sends a new version (approvals then start again)." : d.status === "applying" ? `Being applied${d.linkedPrs.length ? ` in ${d.linkedPrs.map((x) => `#${x.number}`).join(", ")}` : ""}. It closes when the pull request merges.` : d.status === "outdated" ? "Outdated: the code changed since it was suggested. Ask the author to refresh it." : `${SUGGESTION_STATUS_LABEL[d.status]}: nothing to do.`
|
|
10932
|
+
};
|
|
10933
|
+
}
|
|
10934
|
+
async function reviewSuggestionOp(ctx, a) {
|
|
10935
|
+
const n = suggestionNumber(a.number);
|
|
10936
|
+
const gh = await writer(ctx, "Reviewing a suggestion", "Issues: read and write");
|
|
10937
|
+
const { gov } = await suggestionGovernance(ctx, gh);
|
|
10938
|
+
const viewer = await viewerOf(ctx, gh);
|
|
10939
|
+
if (a.event === "decline") {
|
|
10940
|
+
if (!a.message?.trim()) throw new Error("Say why it is declined, in a sentence: the author reads it.");
|
|
10941
|
+
if (a.payloadSha) {
|
|
10942
|
+
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
10943
|
+
if (d.parsed.ok && !d.parsed.sha.startsWith(a.payloadSha)) throw new Error(`#${n} changed since you read it. Read it again (bridge_get_suggestion) before declining.`);
|
|
10944
|
+
}
|
|
10945
|
+
const r2 = await declineSuggestionOnGitHub(gh, n, a.message.trim(), viewer);
|
|
10946
|
+
return { number: n, status: "declined", said: SUGGESTION_STATUS_LABEL.declined, ...r2.warnings.length ? { warnings: r2.warnings } : {}, next: "Closed as not planned, with your reason. Reopen it on GitHub to look at it again." };
|
|
10947
|
+
}
|
|
10948
|
+
if (a.event !== "comment" && viewer) {
|
|
10949
|
+
const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams ?? void 0);
|
|
10950
|
+
const owners = [...lists.design, ...lists.tech].map((l) => l.toLowerCase().replace(/^@/, ""));
|
|
10951
|
+
if (owners.length && !owners.includes(viewer.toLowerCase())) {
|
|
10952
|
+
throw new Error(`Only the owners in ${CONFIG_FILE} approve or request changes on suggestions, and @${viewer} is not one. Comment instead (event "comment"), or ask an owner.`);
|
|
10953
|
+
}
|
|
10954
|
+
}
|
|
10955
|
+
const payloadSha = a.payloadSha && /^[0-9a-f]{64}$/.test(a.payloadSha) ? a.payloadSha : void 0;
|
|
10956
|
+
if (a.payloadSha && !payloadSha) {
|
|
10957
|
+
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
10958
|
+
if (d.parsed.ok && !d.parsed.sha.startsWith(a.payloadSha)) throw new Error(`#${n} changed since you read it. Read it again (bridge_get_suggestion) and review the new version.`);
|
|
10959
|
+
}
|
|
10960
|
+
const r = await reviewSuggestionOnGitHub(gh, n, { event: a.event, role: a.role, message: a.message, payloadSha, ...viewer ? { by: viewer } : {} }, gov);
|
|
10961
|
+
return {
|
|
10962
|
+
number: n,
|
|
10963
|
+
event: a.event,
|
|
10964
|
+
counts: r.counts,
|
|
10965
|
+
version: r.payloadSha.slice(0, 7),
|
|
10966
|
+
status: r.status,
|
|
10967
|
+
said: SUGGESTION_STATUS_LABEL[r.status],
|
|
10968
|
+
comment: r.commentUrl,
|
|
10969
|
+
...r.warnings.length ? { warnings: r.warnings } : {},
|
|
10970
|
+
next: r.status === "approved" ? `Approved: a developer applies it with \`bridge apply ${n}\` or bridge_apply_suggestion.` : r.status === "changes-requested" ? "The author sends a new version; approvals start again on it." : "Waiting for the other approvals."
|
|
10971
|
+
};
|
|
10972
|
+
}
|
|
10973
|
+
function githubWorkspace(gh, ref) {
|
|
10974
|
+
let files;
|
|
10975
|
+
return {
|
|
10976
|
+
read: async (path) => await gh.getFile(path, ref) ?? null,
|
|
10977
|
+
readAt: async (sha, path) => await gh.readFile(path, sha).catch(() => void 0) ?? null,
|
|
10978
|
+
list: () => files ??= gh.listFiles(ref)
|
|
10979
|
+
};
|
|
10980
|
+
}
|
|
10981
|
+
var MAX_DIFF = 12e3;
|
|
10982
|
+
var cut = (s) => s && s.length > MAX_DIFF ? `${s.slice(0, MAX_DIFF)}
|
|
10983
|
+
... (truncated)` : s;
|
|
10984
|
+
function planFacts(plan) {
|
|
10985
|
+
return {
|
|
10986
|
+
outcome: plan.outcome,
|
|
10987
|
+
exitCode: plan.exitCode,
|
|
10988
|
+
version: plan.payloadSha.slice(0, 7),
|
|
10989
|
+
table: suggestionApplyTable(plan),
|
|
10990
|
+
...plan.problems.length ? { problems: plan.problems } : {},
|
|
10991
|
+
...plan.notes.length ? { notes: plan.notes } : {},
|
|
10992
|
+
...plan.gate ? { approvals: { review: plan.gate.review, missing: plan.gate.missing.map(roleText), forced: plan.gate.forced, edited: plan.gate.edited } } : {},
|
|
10993
|
+
...plan.checks ? {
|
|
10994
|
+
checks: {
|
|
10995
|
+
tier: plan.checks.tier.tier,
|
|
10996
|
+
tierWhenSuggested: plan.checks.tierWhenSuggested,
|
|
10997
|
+
tierIncreased: plan.checks.tierIncreased,
|
|
10998
|
+
newErrors: plan.checks.newErrors.length,
|
|
10999
|
+
newContrastFailures: plan.checks.newFailures.map((f) => `${f.fg} on ${f.bg}${f.modes ? ` (${f.modes})` : ""}`)
|
|
11000
|
+
}
|
|
11001
|
+
} : {},
|
|
11002
|
+
...plan.brief ? { brief: plan.brief } : {}
|
|
11003
|
+
};
|
|
11004
|
+
}
|
|
11005
|
+
async function applySuggestionOp(ctx, a) {
|
|
11006
|
+
const n = suggestionNumber(a.number);
|
|
11007
|
+
const gh = await reader(ctx);
|
|
11008
|
+
const { config, gov } = await suggestionGovernance(ctx, gh);
|
|
11009
|
+
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
11010
|
+
if (!d.parsed.ok) {
|
|
11011
|
+
if (!d.issue.labels.includes(SUGGESTION_LABEL) && d.parsed.reason === "no-block") throw new Error(`Issue #${n} is not a Bridge suggestion: it has no "${SUGGESTION_LABEL}" label and no suggestion block.`);
|
|
11012
|
+
return { number: n, status: "refused", outcome: "refused", exitCode: 1, message: `Not applied: ${d.parsed.message}` };
|
|
11013
|
+
}
|
|
11014
|
+
if (d.status === "declined") return { number: n, status: "refused", outcome: "refused", exitCode: 1, message: `Not applied: #${n} was declined. Reopen it on GitHub first if it should go in after all.` };
|
|
11015
|
+
const parsed = d.parsed;
|
|
11016
|
+
const dryRun = a.dryRun || a.mode === "dry-run";
|
|
11017
|
+
const mode = a.mode && a.mode !== "dry-run" ? a.mode : "branch";
|
|
11018
|
+
const common = { parsed, comments: d.comments, governance: gov, author: d.issue.author || null, acceptEdited: a.acceptEdited, forceUnapproved: a.forceUnapproved };
|
|
11019
|
+
if (ctx.mode !== "local" || !ctx.isGit || !ctx.root) {
|
|
11020
|
+
const ref = ctx.ref ?? baseBranchOf(config);
|
|
11021
|
+
const plan = await planSuggestionApply({ ...common, config, workspace: githubWorkspace(gh, ref) });
|
|
11022
|
+
return {
|
|
11023
|
+
number: n,
|
|
11024
|
+
status: plan.outcome === "ready" ? "planned" : plan.outcome,
|
|
11025
|
+
message: plan.message,
|
|
11026
|
+
...planFacts(plan),
|
|
11027
|
+
...plan.outcome === "ready" ? { files: plan.writes } : {},
|
|
11028
|
+
next: plan.outcome === "ready" ? `Bridge reads ${gh.fullName} over the API and writes nothing here. Write these files on a new branch, commit, push, and open a pull request whose body says "Closes #${n}", then bridge_link_suggestion { number: ${n}, pr }.` : plan.message
|
|
11029
|
+
};
|
|
11030
|
+
}
|
|
11031
|
+
const viewer = await viewerOf(ctx, gh);
|
|
11032
|
+
const r = await applySuggestionLocally({
|
|
11033
|
+
...common,
|
|
11034
|
+
root: ctx.root,
|
|
11035
|
+
number: n,
|
|
11036
|
+
mode,
|
|
11037
|
+
dryRun,
|
|
11038
|
+
...a.base ? { base: a.base } : {},
|
|
11039
|
+
...a.branch ? { branch: a.branch } : {},
|
|
11040
|
+
...viewer ? { by: viewer } : {},
|
|
11041
|
+
readBase: async (path, sha) => await gh.readFile(path, sha).catch(() => void 0) ?? null
|
|
11042
|
+
});
|
|
11043
|
+
let recorded = false;
|
|
11044
|
+
const warnings = [];
|
|
11045
|
+
if (r.appliedComment && !dryRun && (r.status === "branch-created" || r.status === "written" || r.status === "branch-exists")) {
|
|
11046
|
+
if (ctx.canWriteRemote) {
|
|
11047
|
+
try {
|
|
11048
|
+
await gh.addComment(n, r.appliedComment);
|
|
11049
|
+
recorded = true;
|
|
11050
|
+
const w = await syncSuggestionStateLabel(gh, n, d.issue.labels, "applying");
|
|
11051
|
+
if (w) warnings.push(w);
|
|
11052
|
+
} catch (e) {
|
|
11053
|
+
warnings.push(`#${n} does not say it is being applied yet (${e.message}). It will once the pull request says "Closes #${n}".`);
|
|
11054
|
+
}
|
|
11055
|
+
} else warnings.push(`#${n} does not say it is being applied: that needs a GitHub token with Issues write. It will once the pull request says "Closes #${n}".`);
|
|
11056
|
+
}
|
|
11057
|
+
const next = r.status === "dry-run" ? `Nothing written. Apply it: bridge_apply_suggestion { number: ${n}, mode: "branch" } (or "write" for the working tree, "patch" for a patch).` : r.status === "branch-created" || r.status === "branch-exists" ? `Push ${r.branch} (git push -u origin ${r.branch}) and open a pull request whose body says "Closes #${n}", then bridge_link_suggestion { number: ${n}, pr }.` : r.status === "written" ? `Review the files, commit them on a branch, push, and open a pull request whose body says "Closes #${n}", then bridge_link_suggestion { number: ${n}, pr }.` : r.status === "patch" ? `Apply the patch with \`git am\` on a branch, then open a pull request whose body says "Closes #${n}".` : r.message;
|
|
11058
|
+
return {
|
|
11059
|
+
number: n,
|
|
11060
|
+
status: r.status,
|
|
11061
|
+
mode: dryRun ? "dry-run" : mode,
|
|
11062
|
+
message: r.message,
|
|
11063
|
+
...planFacts(r.plan),
|
|
11064
|
+
exitCode: r.exitCode,
|
|
11065
|
+
...r.branch ? { branch: r.branch } : {},
|
|
11066
|
+
...r.commit ? { commit: r.commit } : {},
|
|
11067
|
+
...r.written ? { written: r.written } : {},
|
|
11068
|
+
...r.patch ? { patch: r.patch } : {},
|
|
11069
|
+
...r.diff ? { diff: cut(r.diff) } : {},
|
|
11070
|
+
recorded,
|
|
11071
|
+
...warnings.length ? { warnings } : {},
|
|
11072
|
+
next
|
|
11073
|
+
};
|
|
11074
|
+
}
|
|
11075
|
+
async function linkSuggestionOp(ctx, a) {
|
|
11076
|
+
const n = suggestionNumber(a.number);
|
|
11077
|
+
const gh = await writer(ctx, "Linking a suggestion", "Issues: read and write");
|
|
11078
|
+
if (!a.pr && !a.branch && !a.commit) throw new Error("Say where it is applied: pr (the pull request number), branch or commit.");
|
|
11079
|
+
const { gov } = await suggestionGovernance(ctx, gh);
|
|
11080
|
+
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
11081
|
+
if (!d.parsed.ok) throw new Error(`#${n} cannot be linked: ${d.parsed.message}`);
|
|
11082
|
+
const warnings = [];
|
|
11083
|
+
let branch = a.branch;
|
|
11084
|
+
if (a.pr) {
|
|
11085
|
+
const pr = await gh.getPullRequest(a.pr);
|
|
11086
|
+
branch ??= pr.headRef;
|
|
11087
|
+
if (!closingIssueNumbers(pr.body).includes(n)) warnings.push(`#${a.pr} does not say "Closes #${n}": add it to the pull request description, so the suggestion closes when it merges.`);
|
|
11088
|
+
}
|
|
11089
|
+
const viewer = await viewerOf(ctx, gh);
|
|
11090
|
+
const r = await linkSuggestionPrOnGitHub(gh, n, { pr: a.pr, branch, commit: a.commit, by: viewer ?? void 0, payloadSha: d.parsed.sha, forced: a.forced });
|
|
11091
|
+
warnings.push(...r.warnings);
|
|
11092
|
+
return { number: n, status: "applying", said: SUGGESTION_STATUS_LABEL.applying, ...a.pr ? { pr: a.pr } : {}, ...branch ? { branch } : {}, ...warnings.length ? { warnings } : {}, next: "It closes when the pull request merges." };
|
|
11093
|
+
}
|
|
11094
|
+
async function createSuggestionOp(ctx, a) {
|
|
11095
|
+
const { config } = await ctx.config();
|
|
11096
|
+
const gh = await ctx.github();
|
|
11097
|
+
const raw = a.payload ?? {};
|
|
11098
|
+
if (!SUGGESTION_KINDS.includes(raw.kind)) throw new Error(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
|
|
11099
|
+
const needsBase = !raw.base;
|
|
11100
|
+
const base = needsBase ? gh ? await suggestionBase(ctx, gh, config).catch(() => null) : null : raw.base;
|
|
11101
|
+
if (!base) throw new Error(`No base commit: pass "base" { "ref", "sha" }, or set "repo" in ${CONFIG_FILE} so Bridge reads it.`);
|
|
11102
|
+
const viewer = gh && !a.dryRun ? await viewerOf(ctx, gh) : null;
|
|
11103
|
+
const payload = {
|
|
11104
|
+
schema: SUGGESTION_SCHEMA,
|
|
11105
|
+
source: a.source ?? "mcp",
|
|
11106
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11107
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 },
|
|
11108
|
+
...viewer ? { suggestedBy: viewer } : {},
|
|
11109
|
+
...raw,
|
|
11110
|
+
base
|
|
11111
|
+
};
|
|
11112
|
+
const shape2 = suggestionShapeProblems(payload);
|
|
11113
|
+
if (shape2.length) throw new Error(`The payload is not a suggestion: ${shape2.join(" ")}`);
|
|
11114
|
+
if (payload.checks.blocked) throw new Error(`Not suggested: ${payload.checks.blocked}. Bridge never suggests a change that breaks the gate.`);
|
|
11115
|
+
const paths = suggestionPathProblems(payload, config);
|
|
11116
|
+
if (paths.length) throw new Error(`Not suggested: ${paths.join(" ")}`);
|
|
11117
|
+
const labels = [.../* @__PURE__ */ new Set([...config.governance.labels, ...a.labels ?? []])];
|
|
11118
|
+
if (a.dryRun) {
|
|
11119
|
+
const issue = composeSuggestionIssue(payload, { labels, repo: gh?.fullName });
|
|
11120
|
+
return { status: "dry-run", title: issue.title, labels: issue.labels, body: issue.body, parts: issue.parts.length, version: issue.sha.slice(0, 7) };
|
|
11121
|
+
}
|
|
11122
|
+
if (!gh || !ctx.canWriteRemote) return { status: "preview", message: noSuggestionAccess(ctx, gh), payload };
|
|
11123
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels, repo: gh.fullName, assignees: config.integrations?.requests?.assignees ?? [] });
|
|
11124
|
+
return { status: "suggested", number: created.number, url: created.url, version: created.sha.slice(0, 7), ...created.warnings.length ? { warnings: created.warnings } : {}, next: appliedNext(created.number) };
|
|
11125
|
+
}
|
|
11126
|
+
|
|
8963
11127
|
// src/ops.ts
|
|
8964
11128
|
var prefixPath = (set, f) => set.repoPath(f);
|
|
8965
11129
|
function normalizeTokenPath(set, input) {
|
|
@@ -8979,6 +11143,7 @@ async function getConfig(ctx) {
|
|
|
8979
11143
|
source: ctx.describe(),
|
|
8980
11144
|
configOrigin: origin === "file" ? "bridge.config.json" : "detected (no bridge.config.json; run `bridge init` to pin it)",
|
|
8981
11145
|
write: ctx.mode === "local" ? ctx.isGit ? "local branches only (never pushes)" : "none (not a git repo)" : ctx.canWriteRemote ? "pull requests on new bridge/* branches" : "read-only (set GITHUB_TOKEN to open PRs)",
|
|
11146
|
+
changes: changesMode(config) === "suggest" ? `suggest: token, docs, brand, revert and Figma changes are filed as suggestions (GitHub issues a developer applies with bridge apply or bridge_apply_suggestion)${ctx.canWriteRemote ? "" : "; filing one needs GITHUB_TOKEN with Issues write"}. Pass changes "direct" to a tool to open a branch instead.` : 'direct: changes open branches and pull requests. Pass changes "suggest" to a tool to file a suggestion instead.',
|
|
8982
11147
|
repo: gh ? gh.fullName : null,
|
|
8983
11148
|
tokenFiles: set.fileNames.length,
|
|
8984
11149
|
tokens: set.allPaths().length,
|
|
@@ -9306,6 +11471,25 @@ async function proposeTokenChange(ctx, a) {
|
|
|
9306
11471
|
if (newErrors.length || newFailures.length) {
|
|
9307
11472
|
return { status: "blocked", why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and "), newErrors, ...report, hint: "Fix the edits (or add edits for the aliases they break). Bridge never opens a PR that breaks the gate." };
|
|
9308
11473
|
}
|
|
11474
|
+
if (!target && await changesFor(ctx, a.changes) === "suggest") {
|
|
11475
|
+
const source = a.source ?? "mcp";
|
|
11476
|
+
const shown = { ...report, changesMode: "suggest", files: files.map((f) => f.path) };
|
|
11477
|
+
const gh2 = await ctx.github();
|
|
11478
|
+
if (a.dryRun) return { status: "dry-run", ...shown, suggestion: { title: suggestionIssueTitle({ kind: "token-change", title: a.title }), body }, fileContents: files };
|
|
11479
|
+
if (!gh2 || !ctx.canWriteRemote) return { status: "preview", message: noSuggestionAccess(ctx, gh2), ...shown, fileContents: files };
|
|
11480
|
+
const base2 = await suggestionBase(ctx, gh2, config);
|
|
11481
|
+
const viewer = await viewerOf(ctx, gh2);
|
|
11482
|
+
const s = await suggestTokenProposalOnGitHub(gh2, plan, {
|
|
11483
|
+
base: base2.ref,
|
|
11484
|
+
baseSha: base2.sha,
|
|
11485
|
+
source,
|
|
11486
|
+
...viewer ? { suggestedBy: viewer } : {},
|
|
11487
|
+
labels: config.governance.labels,
|
|
11488
|
+
labelsAfter: marks.labels,
|
|
11489
|
+
assignees: config.integrations?.requests?.assignees ?? []
|
|
11490
|
+
});
|
|
11491
|
+
return { status: "suggested", ...shown, number: s.number, url: s.url, version: s.sha.slice(0, 7), labels: s.labels, ...s.warnings.length ? { warnings: s.warnings } : {}, product: product.name, next: appliedNext(s.number) };
|
|
11492
|
+
}
|
|
9309
11493
|
let branch = target ? target.branch : branchWithJiraKey(`${config.governance.branchPrefix}${slugTitle}`, jira);
|
|
9310
11494
|
const preview = { ...report, branch, files: files.map((f) => f.path), prBody: body };
|
|
9311
11495
|
if (a.dryRun) return { status: "dry-run", ...preview, fileContents: files };
|
|
@@ -9457,8 +11641,8 @@ async function getReview(ctx, a) {
|
|
|
9457
11641
|
const delta2 = pairs.length ? core.contrastDelta(core.runContrast(before, pairs, config.contrast?.modes), core.runContrast(after, pairs, config.contrast?.modes)) : [];
|
|
9458
11642
|
const prevErrors = new Set(core.validate(before).map(issueKey3));
|
|
9459
11643
|
const errors = core.validate(after).filter((i) => i.level === "error");
|
|
9460
|
-
const
|
|
9461
|
-
const otherFiles = changedFiles.filter((f) => !f.startsWith(`${
|
|
11644
|
+
const tokenRoot2 = config.tokens.root.replace(/\/$/, "");
|
|
11645
|
+
const otherFiles = changedFiles.filter((f) => !f.startsWith(`${tokenRoot2}/`) && !f.startsWith(`${config.changesets?.dir ?? ".changeset"}/`));
|
|
9462
11646
|
const tier = core.classify(changes, otherFiles, config);
|
|
9463
11647
|
const imp = core.impact(before, after, changes, 20);
|
|
9464
11648
|
const labelTier = Array.isArray(meta.labels) ? meta.labels.map((l) => /^tier:\s*(\d)/i.exec(l)?.[1]).find(Boolean) : void 0;
|
|
@@ -9515,12 +11699,12 @@ async function scaffoldComponentOp(ctx, a) {
|
|
|
9515
11699
|
const write = ctx.mode === "local" && a.write !== false;
|
|
9516
11700
|
if (write) {
|
|
9517
11701
|
for (const f of res.files) {
|
|
9518
|
-
const abs =
|
|
11702
|
+
const abs = join5(ctx.root, f.path);
|
|
9519
11703
|
if (await access(abs).then(() => true, () => false)) throw new Error(`${f.path} exists; not overwriting.`);
|
|
9520
11704
|
}
|
|
9521
11705
|
for (const f of res.files) {
|
|
9522
|
-
await
|
|
9523
|
-
await
|
|
11706
|
+
await mkdir3(dirname4(join5(ctx.root, f.path)), { recursive: true });
|
|
11707
|
+
await writeFile3(join5(ctx.root, f.path), f.content);
|
|
9524
11708
|
}
|
|
9525
11709
|
}
|
|
9526
11710
|
return { written: write, tier: 2, files: write ? res.files.map((f) => f.path) : res.files, nextSteps: res.notes };
|
|
@@ -9532,8 +11716,8 @@ async function auditUi(ctx, code2) {
|
|
|
9532
11716
|
async function visualImpact(ctx, a, run2) {
|
|
9533
11717
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
9534
11718
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
9535
|
-
const out = a.out ? resolve3(ctx.root, a.out) :
|
|
9536
|
-
const runImpact = run2 ?? (await import("./src-
|
|
11719
|
+
const out = a.out ? resolve3(ctx.root, a.out) : join5(ctx.root, "visual-impact");
|
|
11720
|
+
const runImpact = run2 ?? (await import("./src-ABI7LARD.js")).runImpact;
|
|
9537
11721
|
const r = await runImpact({
|
|
9538
11722
|
root: ctx.root,
|
|
9539
11723
|
base: a.base ?? "main",
|
|
@@ -9566,7 +11750,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
9566
11750
|
status: s.status,
|
|
9567
11751
|
percent: s.percent,
|
|
9568
11752
|
boxes: s.boxes.length,
|
|
9569
|
-
...s.images.composite ? { composite:
|
|
11753
|
+
...s.images.composite ? { composite: join5(r.outDir, s.images.composite) } : {},
|
|
9570
11754
|
...s.error ? { error: s.error } : {}
|
|
9571
11755
|
})),
|
|
9572
11756
|
outDir: r.outDir,
|
|
@@ -9810,6 +11994,17 @@ async function proposeRevert(ctx, a) {
|
|
|
9810
11994
|
const plan = await buildPlan(ctx, a, ctx.mode === "local" ? "the bridge CLI" : "Bridge MCP");
|
|
9811
11995
|
const report = planReport(plan);
|
|
9812
11996
|
if (plan.status !== "ready") return { ...report, hint: plan.status === "blocked" ? "Bridge never opens a change that breaks the gate." : "Nothing to propose." };
|
|
11997
|
+
if (await changesFor(ctx, a.changes) === "suggest") {
|
|
11998
|
+
const summary2 = plan.commitMessage.split("\n").slice(1).join("\n").trim() || plan.prTitle;
|
|
11999
|
+
const r = await fileSuggestion(
|
|
12000
|
+
ctx,
|
|
12001
|
+
(base2, by) => filesSuggestionPayload(ctx, { kind: "revert", title: plan.prTitle, summary: summary2, files: plan.files, tier: { tier: plan.tier.tier, bump: plan.tier.bump }, base: base2, source: a.source ?? "mcp", suggestedBy: by, checks: { newErrors: plan.newErrors.length } }),
|
|
12002
|
+
{ labels: [plan.kind === "revert" ? "revert" : "restore", ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : []], details: plan.prBody, dryRun: a.dryRun }
|
|
12003
|
+
);
|
|
12004
|
+
if (r.status === "dry-run") return { ...report, status: "dry-run", changesMode: "suggest", suggestion: r.suggestion, fileContents: plan.files };
|
|
12005
|
+
if (r.status === "preview") return { ...report, status: "preview", message: r.message, changesMode: "suggest", fileContents: plan.files };
|
|
12006
|
+
return { ...report, status: "suggested", changesMode: "suggest", number: r.number, url: r.url, version: r.sha.slice(0, 7), labels: r.labels, ...r.warnings.length ? { warnings: r.warnings } : {}, next: r.next };
|
|
12007
|
+
}
|
|
9813
12008
|
if (a.dryRun) return { ...report, status: "dry-run", prBody: plan.prBody, fileContents: plan.files };
|
|
9814
12009
|
const prefix = config.governance.branchPrefix;
|
|
9815
12010
|
let branch = plan.branch;
|
|
@@ -9929,6 +12124,17 @@ async function proposeBrand(ctx, a) {
|
|
|
9929
12124
|
const prefix = config.governance.branchPrefix;
|
|
9930
12125
|
const stem2 = `${prefix}brand-${plan.slug}`;
|
|
9931
12126
|
let branch = stem2;
|
|
12127
|
+
if (await changesFor(ctx, a.changes) === "suggest") {
|
|
12128
|
+
const summary2 = a.notes?.trim() || `New brand "${plan.slug}", started from ${plan.from}. ${report.contrast.line} (${plan.contrast.themes.join(" and ")}).`;
|
|
12129
|
+
const r = await fileSuggestion(
|
|
12130
|
+
ctx,
|
|
12131
|
+
(base2, by) => filesSuggestionPayload(ctx, { kind: "brand", title: plan.title, summary: summary2, files, tier: { tier: plan.risk.tier, bump: plan.risk.bump }, base: base2, source: a.source ?? "mcp", suggestedBy: by }),
|
|
12132
|
+
{ labels: ["tokens"], details: report.prBody, dryRun: a.dryRun }
|
|
12133
|
+
);
|
|
12134
|
+
if (r.status === "dry-run") return { status: "dry-run", ...report, changesMode: "suggest", suggestion: r.suggestion, fileContents: files };
|
|
12135
|
+
if (r.status === "preview") return { status: "preview", message: r.message, ...report, changesMode: "suggest", fileContents: files };
|
|
12136
|
+
return { status: "suggested", ...report, changesMode: "suggest", number: r.number, url: r.url, version: r.sha.slice(0, 7), labels: r.labels, ...r.warnings.length ? { warnings: r.warnings } : {}, next: r.next };
|
|
12137
|
+
}
|
|
9932
12138
|
if (a.dryRun) return { status: "dry-run", ...report, branch, fileContents: files };
|
|
9933
12139
|
const message = `feat(tokens): add the ${plan.name} brand
|
|
9934
12140
|
|
|
@@ -9996,15 +12202,15 @@ function requestFromIssue(issue) {
|
|
|
9996
12202
|
const parsed = parseRequestIssue(issue.body);
|
|
9997
12203
|
if (parsed) return parsed;
|
|
9998
12204
|
const m = /^\s*([^:]{1,60}):\s*(.+)$/.exec(issue.title);
|
|
9999
|
-
const
|
|
10000
|
-
const
|
|
12205
|
+
const kindLabel3 = issue.labels.map((l) => /^request:\s*(.+)$/.exec(l)?.[1]?.trim()).find((k) => k && KINDS2.has(k));
|
|
12206
|
+
const sourceLabel3 = issue.labels.map((l) => /^source:\s*(.+)$/.exec(l)?.[1]?.trim()).find((s) => s === "figma" || s === "studio" || s === "cli" || s === "mcp");
|
|
10001
12207
|
return {
|
|
10002
12208
|
component: m ? m[1].trim() : "Unknown",
|
|
10003
|
-
kind:
|
|
12209
|
+
kind: kindLabel3 ?? "other",
|
|
10004
12210
|
title: m ? m[2].trim() : issue.title,
|
|
10005
12211
|
summary: issue.body.trim() || issue.title,
|
|
10006
12212
|
changes: [],
|
|
10007
|
-
source:
|
|
12213
|
+
source: sourceLabel3 ?? "studio"
|
|
10008
12214
|
};
|
|
10009
12215
|
}
|
|
10010
12216
|
function statusOf(issue, prs) {
|
|
@@ -10092,7 +12298,7 @@ async function createRequest(ctx, a) {
|
|
|
10092
12298
|
${marks.line}`, labels: [...composed.labels, ...marks.labels] } : composed;
|
|
10093
12299
|
if (a.dryRun) return { status: "dry-run", issue };
|
|
10094
12300
|
const gh = await requireGitHub2(ctx);
|
|
10095
|
-
if (!ctx.canWriteRemote) throw new Error("Creating a request needs GITHUB_TOKEN
|
|
12301
|
+
if (!ctx.canWriteRemote) throw new Error("Creating a request needs GITHUB_TOKEN with Issues: read and write on this repository (Contents: read is enough, like for suggestions). Without it Bridge is read-only.");
|
|
10096
12302
|
const created = await gh.createIssue(issue);
|
|
10097
12303
|
return {
|
|
10098
12304
|
status: "created",
|
|
@@ -10200,7 +12406,8 @@ async function saveDocs(ctx, a) {
|
|
|
10200
12406
|
return docsFileText(cleanDocs(merged), current);
|
|
10201
12407
|
};
|
|
10202
12408
|
const gh = await ctx.github();
|
|
10203
|
-
const
|
|
12409
|
+
const mode = a.pr === void 0 ? await changesFor(ctx, a.changes) : "direct";
|
|
12410
|
+
const open = a.pr === void 0 && gh && mode === "direct" ? await findOpenDocsProposalOnGitHub(gh, { name, slug: docsSlug(name) }, ctx.ref ?? baseBranchOf(config)).catch(() => null) : null;
|
|
10204
12411
|
const target = a.pr ?? open?.number;
|
|
10205
12412
|
if (target !== void 0) {
|
|
10206
12413
|
const n = branchNumber(target);
|
|
@@ -10236,7 +12443,36 @@ async function saveDocs(ctx, a) {
|
|
|
10236
12443
|
path,
|
|
10237
12444
|
content: text,
|
|
10238
12445
|
product: product.name,
|
|
10239
|
-
|
|
12446
|
+
changesMode: mode,
|
|
12447
|
+
...a.dryRun ? {} : { said: mode === "suggest" ? noSuggestionAccess(ctx, gh) : `Nothing sent: saving docs for review needs a GitHub token with ${RIGHTS.docs}. Set GITHUB_TOKEN, or write the file yourself on a branch.` }
|
|
12448
|
+
};
|
|
12449
|
+
}
|
|
12450
|
+
if (mode === "suggest") {
|
|
12451
|
+
const base = await suggestionBase(ctx, gh, config);
|
|
12452
|
+
const viewer = await viewerOf(ctx, gh);
|
|
12453
|
+
const r2 = await proposeDocsOnGitHub(gh, [edit], {
|
|
12454
|
+
base: base.ref,
|
|
12455
|
+
baseSha: base.sha,
|
|
12456
|
+
mode: "suggest",
|
|
12457
|
+
title: a.title,
|
|
12458
|
+
notes,
|
|
12459
|
+
jira,
|
|
12460
|
+
origin: DOCS_MCP_ORIGIN,
|
|
12461
|
+
extraLabels: marks.labels,
|
|
12462
|
+
source: a.source ?? "mcp",
|
|
12463
|
+
...viewer ? { suggestedBy: viewer } : {}
|
|
12464
|
+
});
|
|
12465
|
+
return {
|
|
12466
|
+
status: "suggested",
|
|
12467
|
+
number: r2.number,
|
|
12468
|
+
url: r2.url,
|
|
12469
|
+
title: r2.title,
|
|
12470
|
+
path,
|
|
12471
|
+
product: product.name,
|
|
12472
|
+
changesMode: mode,
|
|
12473
|
+
..."warnings" in r2 && r2.warnings.length ? { warnings: r2.warnings } : {},
|
|
12474
|
+
said: `Suggested the ${name} docs as #${r2.number}. Docs only: one approval, then a developer applies it.`,
|
|
12475
|
+
next: appliedNext(r2.number)
|
|
10240
12476
|
};
|
|
10241
12477
|
}
|
|
10242
12478
|
const r = await proposeDocsOnGitHub(gh.withBranchPrefix("docs/"), [edit], {
|
|
@@ -10343,13 +12579,18 @@ async function missingPiecesOp(ctx, a) {
|
|
|
10343
12579
|
next: r.pieces.length ? `${a.record ? "" : "Record them (record: true) so Studio shows them, then "}open a library branch for each with bridge_open_library_branch { pr: ${n}, piece: "<id>" }. Fixes are values and components the library already has: use them in the product code instead.` : "Nothing is missing from the library. Fix the values that equal a token, if any."
|
|
10344
12580
|
};
|
|
10345
12581
|
}
|
|
10346
|
-
function tokenWriter(ctx, gh, source) {
|
|
10347
|
-
return async (plan) => {
|
|
12582
|
+
function tokenWriter(ctx, gh, source, viewer) {
|
|
12583
|
+
return async (plan, how) => {
|
|
10348
12584
|
const { config } = await ctx.config();
|
|
10349
12585
|
const [before, pairs] = await Promise.all([ctx.tokens(), core.readContrastPairs(ctx.source, config).catch(() => [])]);
|
|
10350
12586
|
const p = planTokenProposal({ before, pairs, edits: plan.edits, title: plan.title, summary: plan.summary, config, tool: source === "mcp" ? "Bridge MCP" : "the bridge CLI" });
|
|
10351
12587
|
if (!p) throw new Error("The token already exists with that value: nothing to add.");
|
|
10352
|
-
if (p.blocked) throw new Error(`The library branch was not opened: ${p.blocked}. Change the value (bridge_propose_token_change with dryRun shows why).`);
|
|
12588
|
+
if (p.blocked) throw new Error(`The library ${how.mode === "suggest" ? "suggestion was not sent" : "branch was not opened"}: ${p.blocked}. Change the value (bridge_propose_token_change with dryRun shows why).`);
|
|
12589
|
+
if (how.mode === "suggest") {
|
|
12590
|
+
const base = await suggestionBase(ctx, gh, config);
|
|
12591
|
+
const s = await suggestTokenProposalOnGitHub(gh, p, { base: base.ref, baseSha: base.sha, source, ...viewer ? { suggestedBy: viewer } : {}, labels: config.governance.labels, labelsAfter: [NEEDED_BY_LABEL], neededBy: plan.neededBy });
|
|
12592
|
+
return { status: "suggested", number: s.number, url: s.url };
|
|
12593
|
+
}
|
|
10353
12594
|
const opened = await writeTokenProposalOnGitHub(gh, p, { base: baseBranchOf(config), branchPrefix: config.governance.branchPrefix, labels: config.governance.labels, labelsAfter: [NEEDED_BY_LABEL], tool: source === "mcp" ? "Bridge MCP (source: mcp)" : "the bridge CLI" });
|
|
10354
12595
|
return { number: opened.pr, url: opened.url, branch: opened.branch };
|
|
10355
12596
|
};
|
|
@@ -10357,7 +12598,8 @@ function tokenWriter(ctx, gh, source) {
|
|
|
10357
12598
|
async function openLibraryBranchOp(ctx, a) {
|
|
10358
12599
|
const n = branchNumber(a.pr);
|
|
10359
12600
|
const source = a.source ?? "mcp";
|
|
10360
|
-
const
|
|
12601
|
+
const mode = await changesFor(ctx, a.changes);
|
|
12602
|
+
const gh = a.dryRun ? await reader(ctx) : mode === "suggest" ? await writer(ctx, "Suggesting a library change", SUGGEST_RIGHTS) : await writer(ctx, "Opening a library branch", NEED);
|
|
10361
12603
|
const { config } = await ctx.config();
|
|
10362
12604
|
const viewer = await viewerOf(ctx, gh);
|
|
10363
12605
|
const s = settingsOf(config);
|
|
@@ -10367,7 +12609,8 @@ async function openLibraryBranchOp(ctx, a) {
|
|
|
10367
12609
|
base: s.base,
|
|
10368
12610
|
products: productsOf(config),
|
|
10369
12611
|
exploration: { packages: s.packages, apps: s.apps },
|
|
10370
|
-
writeTokens: tokenWriter(ctx, gh, source),
|
|
12612
|
+
writeTokens: tokenWriter(ctx, gh, source, viewer),
|
|
12613
|
+
mode,
|
|
10371
12614
|
...viewer ? { openedBy: viewer } : {},
|
|
10372
12615
|
...source === "mcp" ? { agent: "claude" } : {},
|
|
10373
12616
|
source,
|
|
@@ -10375,8 +12618,8 @@ async function openLibraryBranchOp(ctx, a) {
|
|
|
10375
12618
|
dryRun: a.dryRun
|
|
10376
12619
|
});
|
|
10377
12620
|
const plan = r.plan;
|
|
10378
|
-
const what = plan.kind === "token-change" ? "token change" : plan.kind === "exploration" ? "library exploration" : "component request";
|
|
10379
|
-
const next = r.status === "dry-run" ? `Run it without dryRun to open the ${what}.` : plan.kind === "exploration" ? `Build it on ${r.link?.branch}: ${plan.brief} Then bridge_use_library_preview { pr: ${n}, piece: "${a.piece}" } so the product branch shows it.` : plan.kind === "request" ? "A person or Claude builds the component from the request (bridge_get_request). The product branch keeps its own version until it ships." : r.preview?.status === "conflict" ? r.preview.said : "Design and tech owners review the token change. Once it ships, bridge_switch_to_published moves the product branch to the real version.";
|
|
12621
|
+
const what = plan.kind === "token-change" ? mode === "suggest" ? "token suggestion" : "token change" : plan.kind === "exploration" ? "library exploration" : "component request";
|
|
12622
|
+
const next = r.status === "dry-run" ? `Run it without dryRun to open the ${what}.` : r.status === "suggested" && r.link ? appliedNext(r.link.number) : plan.kind === "exploration" ? `Build it on ${r.link?.branch}: ${plan.brief} Then bridge_use_library_preview { pr: ${n}, piece: "${a.piece}" } so the product branch shows it.` : plan.kind === "request" ? "A person or Claude builds the component from the request (bridge_get_request). The product branch keeps its own version until it ships." : r.preview?.status === "conflict" ? r.preview.said : "Design and tech owners review the token change. Once it ships, bridge_switch_to_published moves the product branch to the real version.";
|
|
10380
12623
|
return {
|
|
10381
12624
|
status: r.status,
|
|
10382
12625
|
pr: n,
|
|
@@ -10386,6 +12629,7 @@ async function openLibraryBranchOp(ctx, a) {
|
|
|
10386
12629
|
...plan.kind === "token-change" ? { edits: plan.edits } : {},
|
|
10387
12630
|
...plan.kind === "exploration" ? { exploration: plan.exploration.name, brief: plan.brief } : {},
|
|
10388
12631
|
...plan.kind === "request" ? { request: plan.request.title } : {},
|
|
12632
|
+
changesMode: mode,
|
|
10389
12633
|
neededBy: `${plan.neededBy.title} (#${plan.neededBy.pr}) in ${plan.neededBy.productName}`,
|
|
10390
12634
|
...r.link ? { library: { kind: r.link.kind, number: r.link.number, url: r.link.url, ...r.link.branch ? { branch: r.link.branch } : {} } } : {},
|
|
10391
12635
|
...r.preview ? { preview: r.preview } : {},
|
|
@@ -10435,15 +12679,118 @@ async function switchToPublishedOp(ctx, a) {
|
|
|
10435
12679
|
return { pr: n, ...await switchToPublishedOnGitHub(gh, n) };
|
|
10436
12680
|
}
|
|
10437
12681
|
|
|
12682
|
+
// src/figma.ts
|
|
12683
|
+
var figma_exports = {};
|
|
12684
|
+
__export(figma_exports, {
|
|
12685
|
+
figmaChangesOp: () => figmaChangesOp,
|
|
12686
|
+
getFigmaChangeOp: () => getFigmaChangeOp,
|
|
12687
|
+
markFigmaDraftedOp: () => markFigmaDraftedOp,
|
|
12688
|
+
pushFigmaComponentOp: () => pushFigmaComponentOp
|
|
12689
|
+
});
|
|
12690
|
+
var NEED2 = "Contents and Pull requests: read and write";
|
|
12691
|
+
async function tokenFacts(ctx, set, component) {
|
|
12692
|
+
const { config } = await ctx.config();
|
|
12693
|
+
const text = await ctx.source.read(config.components?.specs ?? DEFAULT_COMPONENT_SPECS_FILE).catch(() => void 0);
|
|
12694
|
+
const usage = text ? specsUsage(parseComponentSpecs(text), component) : () => ({ variants: [], components: [] });
|
|
12695
|
+
return { tierOf: (p) => set.tierOf(p), usedElsewhere: usage };
|
|
12696
|
+
}
|
|
12697
|
+
async function figmaChangesOp(ctx, a = {}) {
|
|
12698
|
+
const gh = await reader(ctx);
|
|
12699
|
+
const rows = await listFigmaChangesOnGitHub(gh, { state: a.state ?? "open" });
|
|
12700
|
+
return {
|
|
12701
|
+
changes: rows.map((r) => ({ pr: r.number, title: r.title, url: r.url, branch: r.branch, component: r.origin.component, route: r.origin.route === "token-change" ? "exact token change" : "Claude AI drafts", status: r.status.label, said: r.status.sentence })),
|
|
12702
|
+
next: rows.some((r) => r.status.key === "waiting-for-claude") ? "Draft one: bridge_get_figma_change { pr } gives the spec and the steps." : rows.length ? "Owners review them on GitHub or in Bridge Studio." : "Nothing sent from Figma is open."
|
|
12703
|
+
};
|
|
12704
|
+
}
|
|
12705
|
+
async function getFigmaChangeOp(ctx, a) {
|
|
12706
|
+
const n = branchNumber(a.pr);
|
|
12707
|
+
const gh = await reader(ctx);
|
|
12708
|
+
const { config } = await ctx.config();
|
|
12709
|
+
const r = await figmaChangeOnGitHub(gh, n, { componentsDir: config.components?.dir });
|
|
12710
|
+
return {
|
|
12711
|
+
pr: n,
|
|
12712
|
+
title: r.pull.title,
|
|
12713
|
+
url: r.pull.url,
|
|
12714
|
+
branch: r.pull.headRef,
|
|
12715
|
+
component: r.origin.component,
|
|
12716
|
+
route: r.origin.route === "token-change" ? "exact token change" : "Claude AI drafts",
|
|
12717
|
+
status: r.status.label,
|
|
12718
|
+
said: r.status.sentence,
|
|
12719
|
+
...r.spec ? { spec: { path: r.origin.spec, changes: r.spec.changes, variantAxes: r.spec.variantAxes, properties: r.spec.properties, tokenEdits: r.spec.tokenEdits ?? [], reasons: r.spec.reasons ?? [] } } : {},
|
|
12720
|
+
...r.brief ? { brief: r.brief } : {},
|
|
12721
|
+
next: r.brief && r.status.key === "waiting-for-claude" ? "Follow the brief, push the code to the branch, then bridge_mark_figma_change_drafted." : r.status.sentence
|
|
12722
|
+
};
|
|
12723
|
+
}
|
|
12724
|
+
async function markFigmaDraftedOp(ctx, a) {
|
|
12725
|
+
const n = branchNumber(a.pr);
|
|
12726
|
+
const gh = await writer(ctx, "Marking a Figma change drafted", NEED2);
|
|
12727
|
+
const r = await markFigmaChangeDraftedOnGitHub(gh, n);
|
|
12728
|
+
return { pr: n, status: r.status.label, said: r.status.sentence, files: r.files, next: "Send it for review when it is ready (bridge_send_for_review), or tell the designer it is ready to look at." };
|
|
12729
|
+
}
|
|
12730
|
+
async function pushFigmaComponentOp(ctx, a) {
|
|
12731
|
+
const d = a.diff;
|
|
12732
|
+
if (!d || d.v !== 1 || typeof d.component !== "string" || !Array.isArray(d.changes)) throw new Error("The diff is not a Bridge component diff (v 1): see the spec of a branch from bridge_get_figma_change for its shape.");
|
|
12733
|
+
const source = a.source ?? "mcp";
|
|
12734
|
+
const tool = source === "mcp" ? "Bridge MCP" : "the bridge CLI";
|
|
12735
|
+
const { config } = await ctx.config();
|
|
12736
|
+
const set = await ctx.tokens();
|
|
12737
|
+
const tokens = await tokenFacts(ctx, set, d.component);
|
|
12738
|
+
const mode = await changesFor(ctx, a.changes);
|
|
12739
|
+
const gh = a.dryRun ? await reader(ctx) : mode === "suggest" ? await writer(ctx, "Suggesting a component change from Figma", SUGGEST_RIGHTS) : await writer(ctx, "Sending a component change from Figma", NEED2);
|
|
12740
|
+
const viewer = a.dryRun ? null : await viewerOf(ctx, gh);
|
|
12741
|
+
const base = a.dryRun || mode !== "suggest" ? null : await suggestionBase(ctx, gh, config);
|
|
12742
|
+
const pairs = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
12743
|
+
const aliasOfPath = (p) => aliasOf(set.defsOf(p)[0]?.value) ?? void 0;
|
|
12744
|
+
const r = await openComponentPushOnGitHub(gh, {
|
|
12745
|
+
diff: { ...d, source: d.source ?? source },
|
|
12746
|
+
tokens,
|
|
12747
|
+
base: baseBranchOf(config),
|
|
12748
|
+
branchPrefix: config.governance.branchPrefix,
|
|
12749
|
+
labels: [...config.governance.labels, `source: ${source}`],
|
|
12750
|
+
tool,
|
|
12751
|
+
dryRun: a.dryRun,
|
|
12752
|
+
mode,
|
|
12753
|
+
...viewer ? { suggestedBy: viewer } : {},
|
|
12754
|
+
...base ? { base: base.ref, baseSha: base.sha } : {},
|
|
12755
|
+
writeTokens: async ({ edits, title, summary: summary2, bodyExtra, mode: how, figma }) => {
|
|
12756
|
+
const all = [...pairs, ...componentContrastPairs(edits, pairs, { aliasOf: aliasOfPath, paths: set.allPaths() })];
|
|
12757
|
+
const p = planTokenProposal({ before: set, pairs: all, edits, title, summary: summary2, config, tool, bodyExtra });
|
|
12758
|
+
if (!p) throw new Error("The library already paints it this way: nothing to send.");
|
|
12759
|
+
if (p.blocked) throw new Error(`Not sent: ${p.blocked}. Pick another variable.`);
|
|
12760
|
+
if (how === "suggest") {
|
|
12761
|
+
const b = base ?? await suggestionBase(ctx, gh, config);
|
|
12762
|
+
const s = await suggestTokenProposalOnGitHub(gh, p, { base: b.ref, baseSha: b.sha, source, ...viewer ? { suggestedBy: viewer } : {}, labels: config.governance.labels, labelsAfter: [`source: ${source}`], links: { figma } });
|
|
12763
|
+
return { status: "suggested", number: s.number, url: s.url };
|
|
12764
|
+
}
|
|
12765
|
+
const opened = await writeTokenProposalOnGitHub(gh, p, { base: baseBranchOf(config), branchPrefix: config.governance.branchPrefix, labels: config.governance.labels, labelsAfter: [`source: ${source}`], tool: `${tool} (source: ${source})` });
|
|
12766
|
+
return { number: opened.pr, url: opened.url, branch: opened.branch };
|
|
12767
|
+
}
|
|
12768
|
+
});
|
|
12769
|
+
const plan = r.plan;
|
|
12770
|
+
return {
|
|
12771
|
+
status: r.status,
|
|
12772
|
+
component: d.component,
|
|
12773
|
+
route: plan.route === "token-change" ? "exact token change" : plan.route === "claude-draft" ? "Claude AI drafts" : "fix in Figma",
|
|
12774
|
+
said: plan.said,
|
|
12775
|
+
...plan.edits.length ? { edits: plan.edits } : {},
|
|
12776
|
+
...plan.reasons.length ? { reasons: plan.reasons } : {},
|
|
12777
|
+
...plan.issues.length ? { fixInFigma: plan.issues } : {},
|
|
12778
|
+
changesMode: mode,
|
|
12779
|
+
...r.link ? { library: r.link } : {},
|
|
12780
|
+
...r.suggestion ? { suggestion: r.suggestion } : {},
|
|
12781
|
+
next: r.status === "dry-run" ? `Run it without dryRun to ${mode === "suggest" ? "file the suggestion" : "open the library branch"}.` : r.status === "not-sent" ? "Nothing was sent: fix these in Figma, then push again." : r.suggestion ? `${r.suggestion.route === "claude-draft" ? "A component spec suggestion: Claude AI drafts the code from it once a developer applies it (bridge_apply_suggestion writes the spec file and returns the brief). " : ""}${appliedNext(r.suggestion.number)}` : plan.route === "claude-draft" ? `Draft the code: bridge_get_figma_change { pr: ${r.link.number} }.` : "Design and tech owners review the token change. Once it ships, a pull in the DS Bridge plugin rebuilds the component."
|
|
12782
|
+
};
|
|
12783
|
+
}
|
|
12784
|
+
|
|
10438
12785
|
// src/server.ts
|
|
10439
|
-
var VERSION = true ? "0.
|
|
12786
|
+
var VERSION = true ? "0.7.0" : "dev";
|
|
10440
12787
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
10441
|
-
var
|
|
12788
|
+
var fail2 = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
10442
12789
|
var run = (fn) => async (a) => {
|
|
10443
12790
|
try {
|
|
10444
12791
|
return ok(await fn(a));
|
|
10445
12792
|
} catch (e) {
|
|
10446
|
-
return
|
|
12793
|
+
return fail2(e);
|
|
10447
12794
|
}
|
|
10448
12795
|
};
|
|
10449
12796
|
var ModeArg = z.record(z.string(), z.string()).describe('Mode values, e.g. {"theme":"dark","brand":"ocean"}. Omitted modes use their default. See bridge_get_config for the modes this repo has.');
|
|
@@ -10458,6 +12805,7 @@ var EditArg = z.object({
|
|
|
10458
12805
|
mode: ModeArg.optional().describe('Pick the file that defines the token for this mode, e.g. {"theme":"dark"}. Alternative to `file`.')
|
|
10459
12806
|
});
|
|
10460
12807
|
var RO = { readOnlyHint: true, openWorldHint: false };
|
|
12808
|
+
var ChangesArg = z.enum(["suggest", "direct"]).optional().describe('How this change reaches the code: "suggest" files a suggestion (a GitHub issue the owners approve and a developer applies, Issues write only), "direct" opens a branch and a pull request. Default: "changes" in bridge.config.json, else "suggest". Only set it when the person asked.');
|
|
10461
12809
|
function createServer(ctx) {
|
|
10462
12810
|
const server = new McpServer(
|
|
10463
12811
|
{ name: "bridge", version: VERSION },
|
|
@@ -10465,6 +12813,7 @@ function createServer(ctx) {
|
|
|
10465
12813
|
instructions: [
|
|
10466
12814
|
"Bridge exposes a design system repo (DTCG tokens, components, docs) to you.",
|
|
10467
12815
|
"Every write goes through a branch and a pull request, never straight to the default branch: the library only changes through reviewed pull requests, and on a protected repo GitHub refuses direct pushes anyway.",
|
|
12816
|
+
'How changes reach the code is the "changes" setting of bridge.config.json (bridge_get_config says which): "suggest" (the default) files token, docs, brand, revert, missing piece and Figma changes as suggestions, GitHub issues with the exact change that the owners approve (bridge_review_suggestion) and a developer applies (bridge_apply_suggestion, or `bridge apply <n>`); "direct" opens branches and pull requests. Writing tools take "changes" to pick one for a call; only pass it when the person asked. When the person says "Apply Bridge suggestion #n", call bridge_apply_suggestion with mode "dry-run" first, show the result, then apply it on a branch.',
|
|
10468
12817
|
"Read before you write: bridge_get_guidelines for the repo rules, bridge_search_tokens / bridge_get_token before using or proposing a token.",
|
|
10469
12818
|
"Never hardcode a value in UI code when a token exists; never invent a token name (check it with bridge_get_token).",
|
|
10470
12819
|
"bridge_audit measures how a product codebase adopts the design system (hardcoded values, foreign UI components) and suggests the tokens and components to use.",
|
|
@@ -10477,6 +12826,7 @@ function createServer(ctx) {
|
|
|
10477
12826
|
"Studio parity: a designer can do in Claude what they do in Bridge Studio. Branches in review: bridge_what_needs_me (the queue), bridge_branch_status, bridge_send_for_review, bridge_back_to_draft, bridge_review_branch, bridge_ask_someone_else, bridge_comment, bridge_update_from_main (token conflicts too), bridge_try_branch (try it in a product, share it), bridge_combine_branches, bridge_close_branch, bridge_reopen_branch, bridge_merge_branch (same gate as Studio, owner override with a reason). Docs: bridge_save_docs. Writes act under the person's GitHub token: only do them when the person asked, and merge only when they explicitly asked to merge.",
|
|
10478
12827
|
'Products: work starts for a product (bridge_list_products). Tools that start work take "product"; without it Bridge uses the product of the current folder or repo. Ask the person when it is not obvious.',
|
|
10479
12828
|
"Product branches and the library: when a designer explores a product feature with you (an exploration for a product), check it with bridge_missing_pieces (record: true) before they share it; for each piece the library lacks, offer bridge_open_library_branch, which opens the library side linked to their branch. bridge_product_branch says where each library branch stands; bridge_switch_to_published moves the product branch to the real version once the library ships.",
|
|
12829
|
+
"Components from Figma: the DS Bridge plugin sends a component changed in Figma to a library branch. Token swaps go as exact token changes; new variants, properties and components go to a branch carrying the spec, waiting for Claude AI. When the person asks you to draft one, bridge_figma_changes lists them and bridge_get_figma_change gives the spec and the steps; write the code on that branch, push it, then bridge_mark_figma_change_drafted. Never claim a branch is drafted before its code is pushed.",
|
|
10480
12830
|
'Explorations try experimental variables and components in a product staging branch without touching main: an explore/<slug> branch with a draft pull request labeled exploration, previews and a snapshot package. bridge_start_exploration starts one, bridge_list_explorations lists them. To work on one, pass "branch": "explore/<slug>" to bridge_propose_token_change, bridge_draft_docs and bridge_get_request, and never open a pull request to main from it: a person promotes it to review or closes it.'
|
|
10481
12831
|
].join(" ")
|
|
10482
12832
|
}
|
|
@@ -10605,7 +12955,7 @@ function createServer(ctx) {
|
|
|
10605
12955
|
"bridge_propose_token_change",
|
|
10606
12956
|
{
|
|
10607
12957
|
title: "Propose token change",
|
|
10608
|
-
description: 'Propose token edits as a reviewable change. Applies edits with minimal JSON diffs, validates, runs the contrast gate before/after, computes the risk tier and semver bump, writes a changeset and a designer-friendly PR body. Blocked (nothing written) if it adds validation errors or breaks a contrast pair.
|
|
12958
|
+
description: 'Propose token edits as a reviewable change. Applies edits with minimal JSON diffs, validates, runs the contrast gate before/after, computes the risk tier and semver bump, writes a changeset and a designer-friendly PR body. Blocked (nothing written) if it adds validation errors or breaks a contrast pair. Suggest mode (the default unless bridge.config.json says "changes": "direct"): files a suggestion, a GitHub issue with the exact edits, the checks and the tier, status "suggested" with its number; owners approve it and a developer applies it (bridge_apply_suggestion). Direct mode: local repo, commits to a new local bridge/* branch without touching your working tree (never pushes); GitHub with token, new branch + draft PR labelled "source: mcp" and its tier. With branch or pr it writes to that branch whatever the mode. Never writes to main. Use dryRun to preview. Tell the user token changes are Tier 3.',
|
|
10609
12959
|
inputSchema: {
|
|
10610
12960
|
title: z.string().min(3).describe('Short imperative title, e.g. "Darken muted text in dark theme". Becomes the branch name and PR title.'),
|
|
10611
12961
|
summary: z.string().optional().describe("Why: the problem and the intent, for the reviewers. Plain sentences, no em dash."),
|
|
@@ -10614,11 +12964,12 @@ function createServer(ctx) {
|
|
|
10614
12964
|
branch: z.string().optional().describe("Exploration branch (explore/<slug>) to commit the edits to instead of opening a new pull request. GitHub: one commit on that branch. Local: written to the working tree, which must have that branch checked out. Never main."),
|
|
10615
12965
|
pr: z.union([z.number().int().positive(), z.string().min(1)]).optional().describe('A branch already in review (12 or "#12") to add the edits to, as "Edit tokens in this branch" in Studio: one commit there and a comment for the reviewers. Its author or someone with write access; needs GITHUB_TOKEN.'),
|
|
10616
12966
|
jira: z.string().optional().describe("Jira key like DS-42, as in Studio's Create branch: first in the branch name and title so Jira links them."),
|
|
10617
|
-
product: z.string().optional().describe("Product key or name (bridge_list_products) the change is for. Default: the product of the current folder or repo. Ask the person when it is not obvious.")
|
|
12967
|
+
product: z.string().optional().describe("Product key or name (bridge_list_products) the change is for. Default: the product of the current folder or repo. Ask the person when it is not obvious."),
|
|
12968
|
+
changes: ChangesArg
|
|
10618
12969
|
},
|
|
10619
12970
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
10620
12971
|
},
|
|
10621
|
-
run((a) => proposeTokenChange(ctx, a))
|
|
12972
|
+
run((a) => proposeTokenChange(ctx, { ...a, source: "mcp" }))
|
|
10622
12973
|
);
|
|
10623
12974
|
const Hue = z.union([z.string(), z.number()]);
|
|
10624
12975
|
const BrandArgs = {
|
|
@@ -10652,11 +13003,11 @@ function createServer(ctx) {
|
|
|
10652
13003
|
"bridge_propose_brand",
|
|
10653
13004
|
{
|
|
10654
13005
|
title: "Propose a new brand",
|
|
10655
|
-
description: 'Propose a new brand as a reviewable change. Same plan as bridge_plan_brand; blocked (nothing written) when an input is invalid or a contrast pair fails. Local repo: commits to a new local bridge/brand-<slug> branch without touching your working tree (never pushes). GitHub with token: new branch + draft PR labelled "source: mcp", "tier: 3" and "tokens". Never writes to main. Tell the user a new brand is Tier 3: it needs design and tech owner approval.',
|
|
10656
|
-
inputSchema: { ...BrandArgs, dryRun: z.boolean().optional().describe("Return the plan and file contents without writing.") },
|
|
13006
|
+
description: 'Propose a new brand as a reviewable change. Same plan as bridge_plan_brand; blocked (nothing written) when an input is invalid or a contrast pair fails. Local repo: commits to a new local bridge/brand-<slug> branch without touching your working tree (never pushes). GitHub with token: new branch + draft PR labelled "source: mcp", "tier: 3" and "tokens". Never writes to main. Suggest mode (the default unless bridge.config.json says "changes": "direct"): one suggestion issue with the brand files instead, applied by a developer once approved. Tell the user a new brand is Tier 3: it needs design and tech owner approval.',
|
|
13007
|
+
inputSchema: { ...BrandArgs, dryRun: z.boolean().optional().describe("Return the plan and file contents without writing."), changes: ChangesArg },
|
|
10657
13008
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
10658
13009
|
},
|
|
10659
|
-
run((a) => proposeBrand(ctx, a))
|
|
13010
|
+
run((a) => proposeBrand(ctx, { ...a, source: "mcp" }))
|
|
10660
13011
|
);
|
|
10661
13012
|
server.registerTool(
|
|
10662
13013
|
"bridge_list_reviews",
|
|
@@ -10748,17 +13099,18 @@ function createServer(ctx) {
|
|
|
10748
13099
|
"bridge_propose_revert",
|
|
10749
13100
|
{
|
|
10750
13101
|
title: "Propose a revert",
|
|
10751
|
-
description:
|
|
13102
|
+
description: 'Plans the revert (or restore) like bridge_plan_revert, then: local repo, commits to a new local bridge/* branch without touching your working tree (never pushes); GitHub with token, new bridge/* branch and a PR labelled revert and its tier (draft for Tier 2 and 3). Blocked when it would add validation errors or break a contrast pair. Never writes to main. Suggest mode (the default unless bridge.config.json says "changes": "direct"): one suggestion issue with the files instead, applied by a developer once approved. Show the user the plan and its conflicts first.',
|
|
10752
13103
|
inputSchema: {
|
|
10753
13104
|
target: z.string().optional(),
|
|
10754
13105
|
restore: RestoreArg.optional(),
|
|
10755
13106
|
resolutions: ResolutionsArg.optional(),
|
|
10756
13107
|
restoreConflicts: z.boolean().optional(),
|
|
10757
|
-
dryRun: z.boolean().optional()
|
|
13108
|
+
dryRun: z.boolean().optional(),
|
|
13109
|
+
changes: ChangesArg
|
|
10758
13110
|
},
|
|
10759
13111
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
10760
13112
|
},
|
|
10761
|
-
run((a) => proposeRevert(ctx, a))
|
|
13113
|
+
run((a) => proposeRevert(ctx, { ...a, source: "mcp" }))
|
|
10762
13114
|
);
|
|
10763
13115
|
server.registerTool(
|
|
10764
13116
|
"bridge_scaffold_component",
|
|
@@ -10892,7 +13244,7 @@ function createServer(ctx) {
|
|
|
10892
13244
|
]
|
|
10893
13245
|
};
|
|
10894
13246
|
} catch (e) {
|
|
10895
|
-
return
|
|
13247
|
+
return fail2(e);
|
|
10896
13248
|
}
|
|
10897
13249
|
}
|
|
10898
13250
|
);
|
|
@@ -10989,8 +13341,8 @@ function createServer(ctx) {
|
|
|
10989
13341
|
"bridge_open_library_branch",
|
|
10990
13342
|
{
|
|
10991
13343
|
title: "Open a library branch for a missing piece",
|
|
10992
|
-
description: 'Turn one recorded missing piece into a library branch linked to the product branch, as Bridge Studio does: a token piece opens a token change (same gate and body as bridge_propose_token_change, a draft for design and tech review) and the product branch takes its preview; a variant opens a library exploration with the brief to build it (build it there, then bridge_use_library_preview); a new component opens a component request. Both sides link: the library side says "Needed by" the product branch (label needed-by-product), the product branch records it.
|
|
10993
|
-
inputSchema: { pr: Pr, piece: Piece, usePreview: z.boolean().optional().describe("Merge the new library branch into the product branch when it already changes something (default true)."), dryRun: z.boolean().optional() },
|
|
13344
|
+
description: 'Turn one recorded missing piece into a library branch linked to the product branch, as Bridge Studio does: a token piece opens a token change (same gate and body as bridge_propose_token_change, a draft for design and tech review) and the product branch takes its preview; a variant opens a library exploration with the brief to build it (build it there, then bridge_use_library_preview); a new component opens a component request. Both sides link: the library side says "Needed by" the product branch (label needed-by-product), the product branch records it. Suggest mode (the default unless bridge.config.json says "changes": "direct"): a token piece becomes a suggestion and a variant a component request, no branch; needs Issues write. Direct mode needs GITHUB_TOKEN (contents, pull requests and issues: write). dryRun shows what it would open. Only when the person asked.',
|
|
13345
|
+
inputSchema: { pr: Pr, piece: Piece, usePreview: z.boolean().optional().describe("Merge the new library branch into the product branch when it already changes something (default true)."), dryRun: z.boolean().optional(), changes: ChangesArg },
|
|
10994
13346
|
annotations: WRITE
|
|
10995
13347
|
},
|
|
10996
13348
|
run((a) => openLibraryBranchOp(ctx, { ...a, source: "mcp" }))
|
|
@@ -11025,6 +13377,46 @@ function createServer(ctx) {
|
|
|
11025
13377
|
},
|
|
11026
13378
|
run((a) => switchToPublishedOp(ctx, a))
|
|
11027
13379
|
);
|
|
13380
|
+
server.registerTool(
|
|
13381
|
+
"bridge_figma_changes",
|
|
13382
|
+
{
|
|
13383
|
+
title: "Component changes sent from Figma",
|
|
13384
|
+
description: "The library branches the DS Bridge Figma plugin opened for components changed in Figma (label from-figma), with where each stands: an exact token change (no AI, contrast gated), or a branch carrying the spec, waiting for Claude AI to draft the code, then drafted by Claude AI and reviewed by a person.",
|
|
13385
|
+
inputSchema: { state: z.enum(["open", "closed", "all"]).optional().describe("Default open.") },
|
|
13386
|
+
annotations: READ
|
|
13387
|
+
},
|
|
13388
|
+
run((a) => figmaChangesOp(ctx, a))
|
|
13389
|
+
);
|
|
13390
|
+
server.registerTool(
|
|
13391
|
+
"bridge_get_figma_change",
|
|
13392
|
+
{
|
|
13393
|
+
title: "Get a component change from Figma, to draft it",
|
|
13394
|
+
description: "One library branch sent from Figma: its spec (variant matrix, properties, bound variables per variant, layers, what code gave Figma before, the exact token swaps) and the brief to draft the code on that branch. Check out the branch, write the TSX, CSS module, examples, docs and changeset, run npm run specs and the tests, push, then call bridge_mark_figma_change_drafted.",
|
|
13395
|
+
inputSchema: { pr: Pr },
|
|
13396
|
+
annotations: READ
|
|
13397
|
+
},
|
|
13398
|
+
run((a) => getFigmaChangeOp(ctx, a))
|
|
13399
|
+
);
|
|
13400
|
+
server.registerTool(
|
|
13401
|
+
"bridge_mark_figma_change_drafted",
|
|
13402
|
+
{
|
|
13403
|
+
title: "Mark a Figma change drafted by Claude AI",
|
|
13404
|
+
description: "After you pushed the code for a component change from Figma to its branch: label it drafted-by-claude (no longer waiting-for-claude) and say so in its description, for a person to review. Refused while the branch only carries the spec. Needs GITHUB_TOKEN (pull requests: write).",
|
|
13405
|
+
inputSchema: { pr: Pr },
|
|
13406
|
+
annotations: WRITE
|
|
13407
|
+
},
|
|
13408
|
+
run((a) => markFigmaDraftedOp(ctx, a))
|
|
13409
|
+
);
|
|
13410
|
+
server.registerTool(
|
|
13411
|
+
"bridge_push_figma_component",
|
|
13412
|
+
{
|
|
13413
|
+
title: "Send a component change from Figma",
|
|
13414
|
+
description: 'What the DS Bridge plugin Push tab does, from a structured diff of a Figma component (the v 1 spec shape: component, figmaName, coded, kind, title, summary, changes, rebinds, needsCode, unknown, variantAxes, properties, bindings, anatomy). An exact token swap on a component token used nowhere else opens a token change (contrast gated, reviewed like any token change); anything that needs code opens a draft branch with the spec, waiting for Claude AI; detached layers, raw values and variables the library lacks are listed with what to do and never sent. Suggest mode (the default unless bridge.config.json says "changes": "direct"): a token suggestion or a component spec suggestion instead of a branch, Issues write only. dryRun shows the route. Direct mode needs GITHUB_TOKEN (contents and pull requests: write). Only when the person asked.',
|
|
13415
|
+
inputSchema: { diff: z.record(z.string(), z.unknown()).describe("The structured component diff (v 1)."), dryRun: z.boolean().optional(), changes: ChangesArg },
|
|
13416
|
+
annotations: WRITE
|
|
13417
|
+
},
|
|
13418
|
+
run((a) => pushFigmaComponentOp(ctx, { diff: a.diff, dryRun: a.dryRun, source: "mcp", changes: a.changes }))
|
|
13419
|
+
);
|
|
11028
13420
|
server.registerTool(
|
|
11029
13421
|
"bridge_list_products",
|
|
11030
13422
|
{
|
|
@@ -11214,7 +13606,7 @@ function createServer(ctx) {
|
|
|
11214
13606
|
"bridge_save_docs",
|
|
11215
13607
|
{
|
|
11216
13608
|
title: "Save component docs",
|
|
11217
|
-
description: `Save a component's usage guidance (docs.json sections: summary, whenToUse, whenNotToUse, dos, donts, accessibility, contentGuidelines, anatomy, preview) the way the Library edit mode does: merged into the docs on the branch, cleaned and checked (a summary, no empty items, no em dash). Without pr: a new branch sent for review (docs only: one approval, patch release note), with an optional Jira key and product. With pr: one commit on that open branch. dryRun returns the file.
|
|
13609
|
+
description: `Save a component's usage guidance (docs.json sections: summary, whenToUse, whenNotToUse, dos, donts, accessibility, contentGuidelines, anatomy, preview) the way the Library edit mode does: merged into the docs on the branch, cleaned and checked (a summary, no empty items, no em dash). Without pr: a new branch sent for review (docs only: one approval, patch release note), with an optional Jira key and product. With pr: one commit on that open branch. Suggest mode (the default unless bridge.config.json says "changes": "direct"), without pr: a docs suggestion issue instead of a branch (Issues write only). dryRun returns the file. Direct mode needs GITHUB_TOKEN (contents and pull requests: write). Prose you drafted carries "draftedBy": "claude" and "needsReview": true so a reviewer accepts it in Studio.`,
|
|
11218
13610
|
inputSchema: {
|
|
11219
13611
|
component: z.string().min(1).describe("Component name, e.g. Button."),
|
|
11220
13612
|
docs: z.record(z.string(), z.unknown()).describe('The sections to set, in docs.json shapes: summary a string; whenToUse, whenNotToUse, accessibility, contentGuidelines lists of strings; dos and donts lists of {"title","text"}.'),
|
|
@@ -11223,11 +13615,100 @@ function createServer(ctx) {
|
|
|
11223
13615
|
notes: z.string().optional().describe("Why, for the reviewers."),
|
|
11224
13616
|
jira: z.string().optional().describe("Jira key like DS-42: first in the branch name and title so Jira links them."),
|
|
11225
13617
|
product: z.string().optional().describe("Product key or name (bridge_list_products). Default: the product of the current folder or repo."),
|
|
11226
|
-
dryRun: z.boolean().optional()
|
|
13618
|
+
dryRun: z.boolean().optional(),
|
|
13619
|
+
changes: ChangesArg
|
|
13620
|
+
},
|
|
13621
|
+
annotations: WRITE
|
|
13622
|
+
},
|
|
13623
|
+
run((a) => saveDocs(ctx, { ...a, source: "mcp" }))
|
|
13624
|
+
);
|
|
13625
|
+
const SuggestionNumber = z.union([z.number().int().positive(), z.string().min(1)]).describe('The suggestion issue number (42 or "#42"), or its link.');
|
|
13626
|
+
server.registerTool(
|
|
13627
|
+
"bridge_list_suggestions",
|
|
13628
|
+
{
|
|
13629
|
+
title: "List suggestions",
|
|
13630
|
+
description: "Suggestions in the repo: GitHub issues labeled bridge-suggestion, each an exact change (tokens, docs, a component spec, a brand, a revert, settings) that the owners approve and a developer applies. Each row has the number, title, kind, tier, status (needs-review, changes-requested, approved, applying, applied, declined, outdated) and linked pull requests. Statuses here come from labels; bridge_get_suggestion counts the approvals.",
|
|
13631
|
+
inputSchema: {
|
|
13632
|
+
status: z.enum(["needs-review", "changes-requested", "approved", "applying", "applied", "declined", "outdated", "all"]).optional().describe("Default all open ones."),
|
|
13633
|
+
state: z.enum(["open", "closed", "all"]).optional().describe("Issue state. Default: open, or closed for applied and declined."),
|
|
13634
|
+
kind: z.enum(["token-change", "docs", "component-spec", "brand", "revert", "config"]).optional(),
|
|
13635
|
+
limit: z.number().int().min(1).max(100).optional().describe("Default 100.")
|
|
13636
|
+
},
|
|
13637
|
+
annotations: READ
|
|
13638
|
+
},
|
|
13639
|
+
run((a) => listSuggestionsOp(ctx, a))
|
|
13640
|
+
);
|
|
13641
|
+
server.registerTool(
|
|
13642
|
+
"bridge_get_suggestion",
|
|
13643
|
+
{
|
|
13644
|
+
title: "Get a suggestion",
|
|
13645
|
+
description: "One suggestion: the change (tokens before and after, docs, component spec, files), the checks when suggested, links, its version (payloadSha: what approvals bind to), who approved it, which approvals are missing for its tier, reviews that no longer count (an older version, or not an owner), whether the machine-readable block was edited by hand, linked pull requests, and for a component spec the brief to implement it.",
|
|
13646
|
+
inputSchema: { number: SuggestionNumber },
|
|
13647
|
+
annotations: READ
|
|
13648
|
+
},
|
|
13649
|
+
run((a) => getSuggestionOp(ctx, a))
|
|
13650
|
+
);
|
|
13651
|
+
server.registerTool(
|
|
13652
|
+
"bridge_create_suggestion",
|
|
13653
|
+
{
|
|
13654
|
+
title: "Create a suggestion",
|
|
13655
|
+
description: "File any change as a suggestion from its payload (schema bridge.suggestion/1: kind, title, summary, and tokens edits, docs, componentSpec or files). Missing schema, source, date, base commit and checks are filled. The paths are checked against what the kind may change and the guardrail files before anything is sent. For token, docs, brand, revert and Figma changes prefer their own tools, which plan and check the change first. Needs GITHUB_TOKEN with Issues write. dryRun returns the issue. Only when the person asked.",
|
|
13656
|
+
inputSchema: {
|
|
13657
|
+
payload: z.record(z.string(), z.unknown()).describe("The suggestion payload (bridge.suggestion/1)."),
|
|
13658
|
+
labels: z.array(z.string()).optional().describe("More labels for the issue."),
|
|
13659
|
+
dryRun: z.boolean().optional().describe("Return the issue title, body and labels without creating it.")
|
|
13660
|
+
},
|
|
13661
|
+
annotations: WRITE
|
|
13662
|
+
},
|
|
13663
|
+
run((a) => createSuggestionOp(ctx, a))
|
|
13664
|
+
);
|
|
13665
|
+
server.registerTool(
|
|
13666
|
+
"bridge_review_suggestion",
|
|
13667
|
+
{
|
|
13668
|
+
title: "Review a suggestion",
|
|
13669
|
+
description: "Approve, request changes on, comment on or decline a suggestion, as the person whose GitHub token the server has. Approvals and requested changes count only from the owners in bridge.config.json (read from the default branch) and only on the version reviewed: pass payloadSha from bridge_get_suggestion so a newer version is never approved by mistake. Request changes and decline need a message. Decline closes the issue as not planned. Needs GITHUB_TOKEN (Issues: read and write). Only when the person asked, with the decision they asked for.",
|
|
13670
|
+
inputSchema: {
|
|
13671
|
+
number: SuggestionNumber,
|
|
13672
|
+
event: z.enum(["approve", "request-changes", "comment", "decline"]),
|
|
13673
|
+
message: z.string().optional().describe("What should change, why it is declined, or the comment. Plain sentences, no em dash."),
|
|
13674
|
+
role: z.enum(["design", "tech", "approval"]).optional().describe("The role you approve for, shown in the comment. Informative: what counts is who you are in the owner lists."),
|
|
13675
|
+
payloadSha: z.string().optional().describe("The version you reviewed (payloadSha or version from bridge_get_suggestion).")
|
|
13676
|
+
},
|
|
13677
|
+
annotations: WRITE
|
|
13678
|
+
},
|
|
13679
|
+
run((a) => reviewSuggestionOp(ctx, a))
|
|
13680
|
+
);
|
|
13681
|
+
server.registerTool(
|
|
13682
|
+
"bridge_apply_suggestion",
|
|
13683
|
+
{
|
|
13684
|
+
title: "Apply a suggestion",
|
|
13685
|
+
description: `Apply a Bridge suggestion to today's code ("Apply Bridge suggestion #42"). Bridge checks the paths, the approvals of the owners on its current version, re-applies token edits on the current tokens, merges files, re-runs validation, contrast and the tier, and stops on a conflict, a missing approval or a check that would fail. Local repo: mode "dry-run" (the plan and the diff, nothing written; run it first), "branch" (default: one commit on a new local branch, working tree untouched, never pushes), "write" (into the working tree, refused over uncommitted changes) or "patch" (a git format-patch text). Remote repo: the plan and the files to write yourself. Once a branch or files exist, it records on the issue that it is being applied (needs Issues write). Then push and open a pull request whose body says "Closes #<n>", and call bridge_link_suggestion. acceptEdited and forceUnapproved only when the person asked.`,
|
|
13686
|
+
inputSchema: {
|
|
13687
|
+
number: SuggestionNumber,
|
|
13688
|
+
mode: z.enum(["dry-run", "branch", "write", "patch"]).optional().describe("Default branch. Run dry-run first."),
|
|
13689
|
+
acceptEdited: z.boolean().optional().describe("Apply even though the machine-readable block was edited by hand after Bridge wrote it (check it first)."),
|
|
13690
|
+
forceUnapproved: z.boolean().optional().describe("Apply without every approval, or with a tier that went up. Recorded on the issue."),
|
|
13691
|
+
base: z.string().optional().describe("Local mode, branch and patch: the ref to build on. Default HEAD."),
|
|
13692
|
+
branch: z.string().optional().describe("Local mode, branch: the branch name. Default <branchPrefix>suggestion-<n>-<slug>.")
|
|
13693
|
+
},
|
|
13694
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
13695
|
+
},
|
|
13696
|
+
run((a) => applySuggestionOp(ctx, a))
|
|
13697
|
+
);
|
|
13698
|
+
server.registerTool(
|
|
13699
|
+
"bridge_link_suggestion",
|
|
13700
|
+
{
|
|
13701
|
+
title: "Link the pull request that applies a suggestion",
|
|
13702
|
+
description: 'Record the pull request (or branch, or commit) that applies a suggestion: a comment on the issue and the "suggestion: applying" label. Warns when the pull request does not say "Closes #<n>": add it so the suggestion closes when it merges. Needs GITHUB_TOKEN (Issues: read and write).',
|
|
13703
|
+
inputSchema: {
|
|
13704
|
+
number: SuggestionNumber,
|
|
13705
|
+
pr: z.number().int().positive().optional().describe("The pull request number."),
|
|
13706
|
+
branch: z.string().optional(),
|
|
13707
|
+
commit: z.string().optional()
|
|
11227
13708
|
},
|
|
11228
13709
|
annotations: WRITE
|
|
11229
13710
|
},
|
|
11230
|
-
run((a) =>
|
|
13711
|
+
run((a) => linkSuggestionOp(ctx, a))
|
|
11231
13712
|
);
|
|
11232
13713
|
server.registerResource("resolved-tokens", "bridge://tokens/resolved", { title: "Resolved tokens", description: "Every token resolved in the default mode, plus only the values that differ in each other mode combination.", mimeType: "application/json" }, async (uri) => {
|
|
11233
13714
|
const set = await ctx.tokens();
|
|
@@ -11444,6 +13925,7 @@ export {
|
|
|
11444
13925
|
auditSnippet,
|
|
11445
13926
|
branches_exports,
|
|
11446
13927
|
explorations_exports,
|
|
13928
|
+
suggestions_exports,
|
|
11447
13929
|
ops_exports,
|
|
11448
13930
|
history_exports,
|
|
11449
13931
|
brand_exports,
|
|
@@ -11451,6 +13933,7 @@ export {
|
|
|
11451
13933
|
drafts_exports,
|
|
11452
13934
|
studio_exports,
|
|
11453
13935
|
pieces_exports,
|
|
13936
|
+
figma_exports,
|
|
11454
13937
|
VERSION,
|
|
11455
13938
|
createServer,
|
|
11456
13939
|
serveStdio,
|