@bridge-ds/mcp 0.8.2 → 0.8.4
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 +3 -8
- package/dist/{chunk-QCPYJXK4.js → chunk-HZWFI7A2.js} +735 -584
- package/dist/{chunk-DPK46GA2.js → chunk-PMZGEZGX.js} +3 -1
- package/dist/index.js +2 -2
- package/dist/lib.js +2 -2
- package/dist/{src-UNWS2OVM.js → src-DRJFSLRV.js} +1 -1
- package/dist/suggestions.d.ts +8 -0
- package/package.json +3 -3
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
validateLayered,
|
|
80
80
|
variantKeyOf,
|
|
81
81
|
whoCanShipSentence
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-PMZGEZGX.js";
|
|
83
83
|
|
|
84
84
|
// src/lib.ts
|
|
85
85
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -3923,6 +3923,33 @@ function explorationRules(branch) {
|
|
|
3923
3923
|
];
|
|
3924
3924
|
}
|
|
3925
3925
|
|
|
3926
|
+
// ../core/src/titles.ts
|
|
3927
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3928
|
+
function properWord(word, names3) {
|
|
3929
|
+
const bare = word.replace(/[^\p{L}\p{N}]+$/u, "");
|
|
3930
|
+
if (!bare) return false;
|
|
3931
|
+
if (names3.some((n) => n === bare)) return true;
|
|
3932
|
+
return new RegExp("\\p{Lu}", "u").test(bare.slice(1));
|
|
3933
|
+
}
|
|
3934
|
+
function lowerFirstWord(text, names3 = []) {
|
|
3935
|
+
const first = text.split(/\s+/)[0] ?? "";
|
|
3936
|
+
if (!first || properWord(first, names3)) return text;
|
|
3937
|
+
return `${text.charAt(0).toLowerCase()}${text.slice(1)}`;
|
|
3938
|
+
}
|
|
3939
|
+
function subjectTitle(subject, title, names3 = []) {
|
|
3940
|
+
const t = title.trim().replace(/\.$/, "");
|
|
3941
|
+
const keep = [subject, ...names3];
|
|
3942
|
+
const s = subject.trim();
|
|
3943
|
+
if (!s) return t;
|
|
3944
|
+
const m = new RegExp(`^${escape(s)}(?=$|[^\\p{L}\\p{N}])\\s*(:)?\\s*`, "iu").exec(t);
|
|
3945
|
+
if (m) {
|
|
3946
|
+
const rest = t.slice(m[0].length);
|
|
3947
|
+
if (!rest) return s;
|
|
3948
|
+
return m[1] ? `${s}: ${lowerFirstWord(rest, keep)}` : `${s} ${rest}`;
|
|
3949
|
+
}
|
|
3950
|
+
return `${s}: ${lowerFirstWord(t, keep)}`;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3926
3953
|
// ../core/src/requests.ts
|
|
3927
3954
|
var REQUEST_KINDS = [
|
|
3928
3955
|
{ value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
|
|
@@ -3939,8 +3966,7 @@ var MARK2 = "bridge-request";
|
|
|
3939
3966
|
var kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Other";
|
|
3940
3967
|
var kindSlug = (k) => `request: ${k}`;
|
|
3941
3968
|
function requestIssueTitle(r) {
|
|
3942
|
-
|
|
3943
|
-
return `${r.component}: ${t.charAt(0).toLowerCase()}${t.slice(1)}`;
|
|
3969
|
+
return subjectTitle(r.component, r.title);
|
|
3944
3970
|
}
|
|
3945
3971
|
function composeRequestIssue(r, opts = {}) {
|
|
3946
3972
|
const rows = r.changes.length ? ["| What | Now | Requested |", "| --- | --- | --- |", ...r.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`)] : [];
|
|
@@ -4549,15 +4575,36 @@ function reviewStatusKind(input) {
|
|
|
4549
4575
|
if (input.mergeable === false) return out("conflict");
|
|
4550
4576
|
if (input.ci?.state === "pending") return out("ci-running");
|
|
4551
4577
|
if (!input.ci || !input.approvals) return out("unknown");
|
|
4552
|
-
if (input.mergeable === null) return out("checking");
|
|
4553
4578
|
const tier = input.tier ?? 3;
|
|
4554
4579
|
if (tier >= 2) {
|
|
4555
4580
|
if (!input.approvals.design) return out("needs-design");
|
|
4556
4581
|
if (!input.approvals.tech) return out("needs-tech");
|
|
4557
4582
|
} else if (!input.approvals.approvers.length) return out("needs-approval");
|
|
4583
|
+
if (input.mergeable === null) return out("checking");
|
|
4558
4584
|
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: Boolean(input.draft), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
|
|
4559
4585
|
return gate.allowed ? out("ready") : out("unknown");
|
|
4560
4586
|
}
|
|
4587
|
+
var REVIEW_STATUS_LABEL = {
|
|
4588
|
+
merged: "Merged",
|
|
4589
|
+
closed: "Closed",
|
|
4590
|
+
draft: "Draft",
|
|
4591
|
+
"ci-failing": "Blocked by tests",
|
|
4592
|
+
"changes-requested": "Changes requested",
|
|
4593
|
+
"ci-running": "Tests running",
|
|
4594
|
+
"needs-design": "Needs design approval",
|
|
4595
|
+
"needs-tech": "Needs tech approval",
|
|
4596
|
+
"needs-approval": "Needs approval",
|
|
4597
|
+
conflict: "Conflict with main",
|
|
4598
|
+
checking: "Checking for conflicts",
|
|
4599
|
+
ready: "Ready to merge",
|
|
4600
|
+
unknown: "Status unknown"
|
|
4601
|
+
};
|
|
4602
|
+
var CI_STATE_LABEL = {
|
|
4603
|
+
pass: "Tests pass",
|
|
4604
|
+
fail: "Tests fail",
|
|
4605
|
+
pending: "Tests running",
|
|
4606
|
+
none: "No automated checks"
|
|
4607
|
+
};
|
|
4561
4608
|
var BOTS = /* @__PURE__ */ new Set(["github-actions[bot]", "app/github-actions", "changeset-bot[bot]"]);
|
|
4562
4609
|
var isReleasePull = (p) => p.head.ref.startsWith("changeset-release/");
|
|
4563
4610
|
function isAutomationPull(p) {
|
|
@@ -5400,6 +5447,10 @@ var SUGGESTION_STATE_LABELS = {
|
|
|
5400
5447
|
approved: "suggestion: approved",
|
|
5401
5448
|
applying: "suggestion: applying"
|
|
5402
5449
|
};
|
|
5450
|
+
var SUGGESTION_FINAL_LABELS = {
|
|
5451
|
+
applied: "suggestion: applied",
|
|
5452
|
+
declined: "suggestion: declined"
|
|
5453
|
+
};
|
|
5403
5454
|
var SUGGESTION_STATUS_LABEL = {
|
|
5404
5455
|
"needs-review": "Needs review",
|
|
5405
5456
|
"changes-requested": "Changes requested",
|
|
@@ -5620,8 +5671,8 @@ function suggestionSubject(p) {
|
|
|
5620
5671
|
return KIND_SUBJECT[p.kind];
|
|
5621
5672
|
}
|
|
5622
5673
|
function suggestionIssueTitle(p) {
|
|
5623
|
-
const
|
|
5624
|
-
return
|
|
5674
|
+
const names3 = [...(p.docs ?? []).map((d) => d.component), ...p.componentSpec?.component ? [p.componentSpec.component] : []];
|
|
5675
|
+
return subjectTitle(suggestionSubject(p), p.title, names3);
|
|
5625
5676
|
}
|
|
5626
5677
|
function suggestionLabels(p, opts = {}) {
|
|
5627
5678
|
return [
|
|
@@ -5655,9 +5706,22 @@ function suggestionStatus(i) {
|
|
|
5655
5706
|
if (i.outdated) return "outdated";
|
|
5656
5707
|
return i.review ?? "needs-review";
|
|
5657
5708
|
}
|
|
5709
|
+
function withOutdated(status, outdated) {
|
|
5710
|
+
return outdated && (status === "needs-review" || status === "changes-requested" || status === "approved") ? "outdated" : status;
|
|
5711
|
+
}
|
|
5658
5712
|
function suggestionStateLabel(status) {
|
|
5713
|
+
if (status === "applied" || status === "declined") return SUGGESTION_FINAL_LABELS[status];
|
|
5659
5714
|
return status in SUGGESTION_STATE_LABELS ? SUGGESTION_STATE_LABELS[status] : null;
|
|
5660
5715
|
}
|
|
5716
|
+
var SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
|
|
5717
|
+
function suggestionLabelChanges(labels, status) {
|
|
5718
|
+
if (status === "outdated") return { add: [], remove: [] };
|
|
5719
|
+
const want = suggestionStateLabel(status);
|
|
5720
|
+
return {
|
|
5721
|
+
add: want && !labels.includes(want) ? [want] : [],
|
|
5722
|
+
remove: SUGGESTION_STATUS_LABELS.filter((l) => l !== want && labels.includes(l))
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5661
5725
|
var bridgeMcpInstallCommand = (repo) => `claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo ${repo?.trim() || "<owner/name>"}`;
|
|
5662
5726
|
var MARK3 = "bridge-suggestion";
|
|
5663
5727
|
var PART_SCHEMA = "bridge.suggestion-part/1";
|
|
@@ -6044,97 +6108,422 @@ function suggestionPathProblems(p, config) {
|
|
|
6044
6108
|
return [...new Set(out)];
|
|
6045
6109
|
}
|
|
6046
6110
|
|
|
6047
|
-
// ../core/src/
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
const
|
|
6060
|
-
|
|
6061
|
-
if (created.labels.length && missing.length) warnings.push(`GitHub did not keep these labels on #${created.number}: ${missing.join(", ")}.`);
|
|
6062
|
-
return { number: created.number, url: created.url, sha: issue.sha, parts: issue.parts.length, warnings };
|
|
6111
|
+
// ../core/src/suggestion-apply.ts
|
|
6112
|
+
var SUGGESTION_APPLY_EXIT = {
|
|
6113
|
+
ready: 0,
|
|
6114
|
+
"already-applied": 0,
|
|
6115
|
+
conflict: 3,
|
|
6116
|
+
"not-approved": 1,
|
|
6117
|
+
blocked: 1,
|
|
6118
|
+
refused: 1
|
|
6119
|
+
};
|
|
6120
|
+
var same2 = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
6121
|
+
var show3 = (v) => v === void 0 ? "nothing" : typeof v === "string" ? v : JSON.stringify(v);
|
|
6122
|
+
function tokenRoot(config) {
|
|
6123
|
+
const r = config.tokens.root.replace(/\/+$/, "");
|
|
6124
|
+
return r === "." ? "" : r;
|
|
6063
6125
|
}
|
|
6064
|
-
function
|
|
6065
|
-
|
|
6126
|
+
function tokenFileOf(config, path) {
|
|
6127
|
+
const root = tokenRoot(config);
|
|
6128
|
+
if (root && !path.startsWith(`${root}/`)) return null;
|
|
6129
|
+
if (!selectTokenFiles(config, [path]).length) return null;
|
|
6130
|
+
return root ? path.slice(root.length + 1) : path;
|
|
6066
6131
|
}
|
|
6067
|
-
|
|
6068
|
-
const
|
|
6069
|
-
const
|
|
6070
|
-
|
|
6132
|
+
function planTokenEdit(set, e, changes) {
|
|
6133
|
+
const base = { type: "token", path: e.path, edit: e, ...e.file ? { file: e.file } : {} };
|
|
6134
|
+
const defsAt = (path) => set.defs.filter((d) => d.path === path && (!e.file || d.file === e.file));
|
|
6135
|
+
const recorded = (file, path = e.path) => changes.find((c) => c.path === path && c.file === file && (e.op === "rename" ? c.kind === "renamed" : true));
|
|
6136
|
+
const conflict = (detail, extra = {}) => ({ op: { ...base, state: "conflict", detail, ...extra }, notes: [] });
|
|
6137
|
+
const done = (detail) => ({ op: { ...base, state: "already-applied", detail }, notes: [] });
|
|
6138
|
+
let result;
|
|
6139
|
+
try {
|
|
6140
|
+
result = applyTokenEdits(set, [e]);
|
|
6141
|
+
} catch (err) {
|
|
6142
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6143
|
+
switch (e.op) {
|
|
6144
|
+
case "rename": {
|
|
6145
|
+
const hasOld = defsAt(e.path).length > 0;
|
|
6146
|
+
const hasNew = set.defs.some((d) => d.path === e.newPath);
|
|
6147
|
+
if (!hasOld && hasNew) return done(`Already renamed to ${e.newPath}.`);
|
|
6148
|
+
return conflict(hasOld && hasNew ? `Both ${e.path} and ${e.newPath} exist now: rename it by hand.` : `${e.path} no longer exists: ${why}`);
|
|
6149
|
+
}
|
|
6150
|
+
case "remove":
|
|
6151
|
+
if (!defsAt(e.path).length) return done("Already removed.");
|
|
6152
|
+
return conflict(why);
|
|
6153
|
+
case "add": {
|
|
6154
|
+
const existing = defsAt(e.path);
|
|
6155
|
+
if (!existing.length) return conflict(why);
|
|
6156
|
+
const want = (file) => {
|
|
6157
|
+
const c = recorded(file);
|
|
6158
|
+
if (c && c.after !== void 0) return c.after;
|
|
6159
|
+
return coerceValue(e.value, e.type ?? existing.find((d2) => d2.file === file)?.type);
|
|
6160
|
+
};
|
|
6161
|
+
const all = existing.every((d2) => same2(d2.value, want(d2.file)));
|
|
6162
|
+
if (all) return done("Already added with the suggested value.");
|
|
6163
|
+
const d = existing.find((x) => !same2(x.value, want(x.file)));
|
|
6164
|
+
return conflict(`Someone added ${e.path} with another value since.`, { file: d.file, after: want(d.file), now: d.value });
|
|
6165
|
+
}
|
|
6166
|
+
default:
|
|
6167
|
+
return conflict(defsAt(e.path).length ? why : `${e.path} no longer exists, so its value cannot be set.`);
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
if (!result.touched.length) return done("Already has the suggested value.");
|
|
6171
|
+
if (e.op === "set" || e.op === "remove" || e.op === "rename") {
|
|
6172
|
+
for (const d of defsAt(e.path)) {
|
|
6173
|
+
const c = recorded(d.file, e.op === "rename" ? e.newPath ?? e.path : e.path);
|
|
6174
|
+
if (!c) continue;
|
|
6175
|
+
const okNow = same2(d.value, c.before) || e.op === "set" && same2(d.value, c.after);
|
|
6176
|
+
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 });
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
const verb = e.op === "set" ? "Set" : e.op === "add" ? "Add" : e.op === "remove" ? "Remove" : `Rename to ${e.newPath ?? ""}`;
|
|
6180
|
+
return { op: { ...base, state: "apply", detail: `${verb} in ${result.touched.join(", ")}.` }, files: result.files, notes: result.notes };
|
|
6071
6181
|
}
|
|
6072
|
-
function
|
|
6073
|
-
const
|
|
6074
|
-
const
|
|
6075
|
-
const
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
};
|
|
6182
|
+
async function planFile(ws, baseSha, f) {
|
|
6183
|
+
const base = { type: "file", path: f.path };
|
|
6184
|
+
const cur = await ws.read(f.path);
|
|
6185
|
+
const curSha = cur?.sha ?? null;
|
|
6186
|
+
const theirs = f.afterSha === null ? null : f.content ?? null;
|
|
6187
|
+
if (curSha === f.afterSha) return { op: { ...base, state: "already-applied", detail: f.afterSha === null ? "Already deleted." : "Already has the suggested content." } };
|
|
6188
|
+
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 };
|
|
6189
|
+
if (f.beforeSha === null) return { op: { ...base, state: "conflict", detail: "The suggestion creates this file, but a different one exists now." } };
|
|
6190
|
+
if (!cur) return { op: { ...base, state: "conflict", detail: "The suggestion changes this file, but it was deleted since." } };
|
|
6191
|
+
if (theirs === null) return { op: { ...base, state: "conflict", detail: "The suggestion deletes this file, but it changed since." } };
|
|
6192
|
+
const original = await ws.readAt(baseSha, f.path);
|
|
6193
|
+
if (original === null || gitBlobSha(original) !== f.beforeSha || !ws.merge) {
|
|
6194
|
+
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." } };
|
|
6195
|
+
}
|
|
6196
|
+
const merged = await ws.merge({ path: f.path, ours: cur.text, base: original, theirs });
|
|
6197
|
+
if (!merged.clean) return { op: { ...base, state: "conflict", detail: `The file changed since and the merge has ${merged.conflicts} conflict(s).`, hunks: merged.conflicts } };
|
|
6198
|
+
if (merged.text === cur.text) return { op: { ...base, state: "already-applied", detail: "Already has the suggested lines." } };
|
|
6199
|
+
return { op: { ...base, state: "apply-merged", detail: "The file changed since: merged with the suggested lines, no conflict." }, content: merged.text };
|
|
6090
6200
|
}
|
|
6091
|
-
async function
|
|
6092
|
-
const
|
|
6093
|
-
const
|
|
6094
|
-
const
|
|
6095
|
-
const
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6201
|
+
async function planSuggestionApply(input) {
|
|
6202
|
+
const { parsed, config, workspace: ws } = input;
|
|
6203
|
+
const payload = parsed.payload;
|
|
6204
|
+
const notes = [];
|
|
6205
|
+
const out = (outcome, message, rest2 = {}) => ({
|
|
6206
|
+
outcome,
|
|
6207
|
+
exitCode: SUGGESTION_APPLY_EXIT[outcome],
|
|
6208
|
+
message,
|
|
6209
|
+
payloadSha: parsed.sha,
|
|
6210
|
+
payload,
|
|
6211
|
+
gate: null,
|
|
6212
|
+
ops: [],
|
|
6213
|
+
writes: [],
|
|
6214
|
+
checks: null,
|
|
6215
|
+
problems: [],
|
|
6216
|
+
notes,
|
|
6217
|
+
...payload.componentSpec ? { brief: payload.componentSpec.brief } : {},
|
|
6218
|
+
...rest2
|
|
6104
6219
|
});
|
|
6105
|
-
|
|
6106
|
-
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6220
|
+
const problems = suggestionPathProblems(payload, config);
|
|
6221
|
+
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.`);
|
|
6222
|
+
for (const f of payload.files ?? []) {
|
|
6223
|
+
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.`);
|
|
6224
|
+
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.`);
|
|
6225
|
+
}
|
|
6226
|
+
if (problems.length) return out("refused", "Not applied: the suggestion asks for something Bridge never writes.", { problems: [...new Set(problems)] });
|
|
6227
|
+
if (parsed.edited && !input.acceptEdited) {
|
|
6228
|
+
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.', {
|
|
6229
|
+
problems: ["The payload no longer matches the sha Bridge recorded."]
|
|
6230
|
+
});
|
|
6231
|
+
}
|
|
6232
|
+
const root = tokenRoot(config);
|
|
6233
|
+
const tokenFiles = selectTokenFiles(config, await ws.list());
|
|
6234
|
+
const targets = [
|
|
6235
|
+
...(payload.files ?? []).map((f) => f.path),
|
|
6236
|
+
...(payload.docs ?? []).map((d) => d.path),
|
|
6237
|
+
...payload.componentSpec ? [payload.componentSpec.specPath] : [],
|
|
6238
|
+
...payload.tokens?.edits.length ? tokenFiles.map((f) => root ? `${root}/${f}` : f) : []
|
|
6239
|
+
];
|
|
6240
|
+
if (ws.isSymlink) {
|
|
6241
|
+
const links = [];
|
|
6242
|
+
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.`);
|
|
6243
|
+
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
6244
|
+
}
|
|
6245
|
+
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
6246
|
+
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
|
|
6247
|
+
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
6248
|
+
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
6249
|
+
const approved = review === "approved";
|
|
6250
|
+
const gate = { review, missing, approvals, forced: !approved && Boolean(input.forceUnapproved), edited: parsed.edited };
|
|
6251
|
+
const ops = [];
|
|
6252
|
+
const writes = /* @__PURE__ */ new Map();
|
|
6253
|
+
const current = /* @__PURE__ */ new Map();
|
|
6254
|
+
for (const f of tokenFiles) {
|
|
6255
|
+
const r = await ws.read(root ? `${root}/${f}` : f);
|
|
6256
|
+
if (r) current.set(f, r.text);
|
|
6257
|
+
}
|
|
6258
|
+
const before = new LayeredTokenSet(config, current);
|
|
6259
|
+
let set = before;
|
|
6260
|
+
const appliedEdits = [];
|
|
6261
|
+
for (const e of payload.tokens?.edits ?? []) {
|
|
6262
|
+
const r = planTokenEdit(set, e, payload.tokens.changes);
|
|
6263
|
+
ops.push(r.op);
|
|
6264
|
+
notes.push(...r.notes);
|
|
6265
|
+
if (r.op.state === "apply" && r.files) {
|
|
6266
|
+
set = new LayeredTokenSet(config, r.files);
|
|
6267
|
+
appliedEdits.push(e);
|
|
6116
6268
|
}
|
|
6117
6269
|
}
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
const
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6270
|
+
for (const [f, text] of set.files) if (text !== before.files.get(f)) writes.set(set.repoPath(f), text);
|
|
6271
|
+
for (const f of payload.files ?? []) {
|
|
6272
|
+
if (writes.has(f.path)) {
|
|
6273
|
+
notes.push(`${f.path}: the token edits write this file, its copy in the suggestion is not used.`);
|
|
6274
|
+
continue;
|
|
6275
|
+
}
|
|
6276
|
+
const r = await planFile(ws, payload.base.sha, f);
|
|
6277
|
+
ops.push(r.op);
|
|
6278
|
+
if (r.op.state === "apply" || r.op.state === "apply-merged") writes.set(f.path, r.content ?? null);
|
|
6279
|
+
}
|
|
6280
|
+
for (const d of payload.docs ?? []) {
|
|
6281
|
+
const base = { type: "docs", path: d.path };
|
|
6282
|
+
const docs = d.after;
|
|
6283
|
+
let text;
|
|
6284
|
+
try {
|
|
6285
|
+
const issues = validateDocs(docs);
|
|
6286
|
+
if (issues.length) {
|
|
6287
|
+
problems.push(`${d.path}: ${issues.map((i) => i.message).join(" ")}`);
|
|
6288
|
+
continue;
|
|
6289
|
+
}
|
|
6290
|
+
const cur = await ws.read(d.path);
|
|
6291
|
+
text = docsFileText(docs, cur?.text ?? null);
|
|
6292
|
+
if (cur && text === cur.text) {
|
|
6293
|
+
ops.push({ ...base, state: "already-applied", detail: "The docs already say this." });
|
|
6294
|
+
continue;
|
|
6295
|
+
}
|
|
6296
|
+
ops.push({ ...base, state: "apply", detail: cur ? "Update the changed fields; other fields stay as they are now." : "Create the docs file." });
|
|
6297
|
+
writes.set(d.path, text);
|
|
6298
|
+
} catch (err) {
|
|
6299
|
+
problems.push(`${d.path}: the suggested docs are not a docs.json Bridge can read (${err instanceof Error ? err.message : String(err)}).`);
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
if (payload.componentSpec) {
|
|
6303
|
+
const s = payload.componentSpec;
|
|
6304
|
+
const text = `${JSON.stringify(s.spec, null, 2)}
|
|
6305
|
+
`;
|
|
6306
|
+
const cur = await ws.read(s.specPath);
|
|
6307
|
+
if (cur && cur.text === text) ops.push({ type: "component-spec", path: s.specPath, state: "already-applied", detail: "The spec file is already there." });
|
|
6308
|
+
else {
|
|
6309
|
+
ops.push({ type: "component-spec", path: s.specPath, state: "apply", detail: `Write the spec; the ${s.component} code itself is written from the brief.` });
|
|
6310
|
+
writes.set(s.specPath, text);
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
const afterFiles = new Map(set.files);
|
|
6314
|
+
for (const [p, content] of writes) {
|
|
6315
|
+
const tf = tokenFileOf(config, p);
|
|
6316
|
+
if (!tf) continue;
|
|
6317
|
+
if (content === null) afterFiles.delete(tf);
|
|
6318
|
+
else afterFiles.set(tf, content);
|
|
6319
|
+
}
|
|
6320
|
+
const after = new LayeredTokenSet(config, afterFiles);
|
|
6321
|
+
const tokenChanges = diffLayered(before, after);
|
|
6322
|
+
const issueKey4 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
6323
|
+
const prevErrors = new Set(validateLayered(before).map(issueKey4));
|
|
6324
|
+
const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey4(i))) : [];
|
|
6325
|
+
let pairs = [];
|
|
6326
|
+
if (config.contrast && tokenChanges.length) {
|
|
6327
|
+
try {
|
|
6328
|
+
pairs = parseContrastChecks((await ws.read(config.contrast.path))?.text);
|
|
6329
|
+
} catch {
|
|
6330
|
+
notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
const modes = config.contrast?.modes;
|
|
6334
|
+
const contrast = pairs.length ? contrastDelta(runLayeredContrast(before, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
6335
|
+
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
6336
|
+
const otherFiles = [...writes.keys()].filter((p) => !tokenFileOf(config, p));
|
|
6337
|
+
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 });
|
|
6338
|
+
const tierIncreased = writes.size > 0 && tier.tier > payload.tier.tier;
|
|
6339
|
+
const checks = { tierWhenSuggested: payload.tier.tier, tier, tierIncreased, newErrors, newFailures, contrast, tokenChanges };
|
|
6340
|
+
const rest = { gate, ops, checks, problems };
|
|
6341
|
+
if (problems.length) return out("refused", "Not applied: part of the suggestion is not valid.", rest);
|
|
6342
|
+
const conflicts = ops.filter((o) => o.state === "conflict");
|
|
6343
|
+
if (conflicts.length) {
|
|
6344
|
+
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);
|
|
6345
|
+
}
|
|
6346
|
+
if (!writes.size) return out("already-applied", "Already applied: the code has every change of this suggestion.", rest);
|
|
6347
|
+
if (newErrors.length || newFailures.length) {
|
|
6348
|
+
const why = [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ");
|
|
6349
|
+
return out("blocked", `Not applied: on today's code it would add ${why}. Ask the author to refresh the suggestion.`, rest);
|
|
6350
|
+
}
|
|
6351
|
+
if (tierIncreased && !input.forceUnapproved) {
|
|
6352
|
+
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);
|
|
6353
|
+
}
|
|
6354
|
+
if (!approved && !input.forceUnapproved) {
|
|
6355
|
+
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 ")}`;
|
|
6356
|
+
return out("not-approved", `Not applied: it is not approved yet (${who}). Ask the owners to review it on the issue.`, rest);
|
|
6357
|
+
}
|
|
6358
|
+
const forced = gate.forced || tierIncreased && Boolean(input.forceUnapproved);
|
|
6359
|
+
return out("ready", `Ready: ${writes.size} file(s) to write${forced ? ", without every approval" : ""}.`, {
|
|
6360
|
+
...rest,
|
|
6361
|
+
gate: { ...gate, forced },
|
|
6362
|
+
writes: [...writes].map(([path, content]) => ({ path, content })).sort((a, b) => a.path.localeCompare(b.path))
|
|
6363
|
+
});
|
|
6364
|
+
}
|
|
6365
|
+
function suggestionAppliedComment(plan, where2) {
|
|
6366
|
+
return composeSuggestionAppliedComment({ ...where2, payloadSha: plan.payloadSha, ...plan.gate?.forced ? { forced: true } : {} });
|
|
6367
|
+
}
|
|
6368
|
+
function suggestionApplyTable(plan) {
|
|
6369
|
+
const label = { "already-applied": "already applied", apply: "apply", "apply-merged": "apply (merged)", conflict: "**conflict**" };
|
|
6370
|
+
const cell4 = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
6371
|
+
return ["| Change | Result | Detail |", "| --- | --- | --- |", ...plan.ops.map((o) => `| \`${cell4(o.path)}\` | ${label[o.state]} | ${cell4(o.detail)} |`)].join("\n");
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
// ../core/src/suggestions-github.ts
|
|
6375
|
+
async function createSuggestionOnGitHub(gh, payload, opts = {}) {
|
|
6376
|
+
const issue = composeSuggestionIssue(payload, { repo: gh.fullName, ...opts });
|
|
6377
|
+
const created = await gh.createIssue({ title: issue.title, body: issue.body, labels: issue.labels, assignees: issue.assignees });
|
|
6378
|
+
for (const [i, part] of issue.parts.entries()) {
|
|
6379
|
+
try {
|
|
6380
|
+
await gh.createIssueComment(created.number, part);
|
|
6381
|
+
} catch (err) {
|
|
6382
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6383
|
+
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}.`);
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
const warnings = [];
|
|
6387
|
+
const missing = issue.labels.filter((l) => !created.labels.includes(l));
|
|
6388
|
+
if (created.labels.length && missing.length) warnings.push(`GitHub did not keep these labels on #${created.number}: ${missing.join(", ")}.`);
|
|
6389
|
+
return { number: created.number, url: created.url, sha: issue.sha, parts: issue.parts.length, warnings };
|
|
6390
|
+
}
|
|
6391
|
+
function linkedPullsOf(n, pulls) {
|
|
6392
|
+
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" }));
|
|
6393
|
+
}
|
|
6394
|
+
async function listSuggestionsOnGitHub(gh, opts = {}) {
|
|
6395
|
+
const issues = await gh.listIssues({ label: SUGGESTION_LABEL, state: opts.state ?? "open", perPage: opts.perPage ?? 50 });
|
|
6396
|
+
const pulls = opts.pulls ?? (issues.length ? await gh.listPullRequests("all").catch(() => []) : []);
|
|
6397
|
+
return issues.map((i) => listItem(i, pulls));
|
|
6398
|
+
}
|
|
6399
|
+
function listItem(i, pulls) {
|
|
6400
|
+
const linkedPrs = linkedPullsOf(i.number, pulls);
|
|
6401
|
+
const cached = suggestionStateFromLabels(i.labels);
|
|
6402
|
+
const t = i.labels.map((l) => /^tier: ([123])$/.exec(l)?.[1]).find(Boolean);
|
|
6403
|
+
return {
|
|
6404
|
+
number: i.number,
|
|
6405
|
+
title: i.title,
|
|
6406
|
+
url: i.url,
|
|
6407
|
+
author: i.author,
|
|
6408
|
+
createdAt: i.createdAt,
|
|
6409
|
+
state: i.state,
|
|
6410
|
+
stateReason: i.stateReason,
|
|
6411
|
+
labels: i.labels,
|
|
6412
|
+
kind: suggestionKindFromLabels(i.labels),
|
|
6413
|
+
tier: t ? Number(t) : void 0,
|
|
6414
|
+
linkedPrs,
|
|
6415
|
+
status: suggestionStatus({ state: i.state, stateReason: i.stateReason, linkedPrs, applied: cached === "applying", review: cached === "applying" ? void 0 : cached })
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
6419
|
+
const [issue, comments, pulls] = await Promise.all([gh.getIssue(n), gh.listIssueComments(n), opts.pulls ? Promise.resolve(opts.pulls) : gh.listPullRequests("all").catch(() => [])]);
|
|
6420
|
+
const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
|
|
6421
|
+
const linkedPrs = linkedPullsOf(n, pulls);
|
|
6422
|
+
const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
|
|
6423
|
+
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
|
|
6424
|
+
const status = suggestionStatus({
|
|
6425
|
+
state: issue.state,
|
|
6426
|
+
stateReason: issue.stateReason,
|
|
6427
|
+
linkedPrs,
|
|
6428
|
+
applied: parsed.ok && applied.some((a) => a.payloadSha === parsed.sha),
|
|
6429
|
+
outdated: opts.outdated,
|
|
6430
|
+
review: parsed.ok && approvals ? suggestionReviewState(parsed.payload.tier.tier, approvals) : void 0
|
|
6431
|
+
});
|
|
6432
|
+
return { issue, parsed, comments, approvals, linkedPrs, applied, status };
|
|
6433
|
+
}
|
|
6434
|
+
async function syncSuggestionStateLabel(gh, n, labels, status) {
|
|
6435
|
+
const { add, remove } = suggestionLabelChanges(labels, status);
|
|
6436
|
+
let ok2 = true;
|
|
6437
|
+
for (const l of remove) {
|
|
6438
|
+
try {
|
|
6439
|
+
await gh.removeLabel(n, l);
|
|
6440
|
+
} catch {
|
|
6441
|
+
ok2 = false;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
if (add.length) ok2 = await gh.addLabels(n, add) && ok2;
|
|
6445
|
+
return ok2 ? null : `The status label on #${n} could not be updated; the status shown in Bridge is still right.`;
|
|
6446
|
+
}
|
|
6447
|
+
async function markSuggestionsAppliedOnGitHub(gh, pr) {
|
|
6448
|
+
const applied = [];
|
|
6449
|
+
const warnings = [];
|
|
6450
|
+
for (const n of closingIssueNumbers(pr.body)) {
|
|
6451
|
+
try {
|
|
6452
|
+
const issue = await gh.getIssue(n);
|
|
6453
|
+
if (!issue.labels.includes(SUGGESTION_LABEL)) continue;
|
|
6454
|
+
const w = await syncSuggestionStateLabel(gh, n, issue.labels, "applied");
|
|
6455
|
+
if (w) warnings.push(w);
|
|
6456
|
+
else applied.push(n);
|
|
6457
|
+
} catch (err) {
|
|
6458
|
+
warnings.push(`Suggestion #${n} could not be marked applied (${err instanceof Error ? err.message : String(err)}).`);
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
return { applied, warnings };
|
|
6462
|
+
}
|
|
6463
|
+
function githubSuggestionWorkspace(gh, ref) {
|
|
6464
|
+
let files;
|
|
6465
|
+
return {
|
|
6466
|
+
read: async (path) => await gh.getFile(path, ref) ?? null,
|
|
6467
|
+
readAt: async (sha, path) => await gh.readFile(path, sha).catch(() => void 0) ?? null,
|
|
6468
|
+
list: () => files ??= gh.listFiles(ref)
|
|
6469
|
+
};
|
|
6470
|
+
}
|
|
6471
|
+
function suggestionTouches(payload, config, path) {
|
|
6472
|
+
if (path === CONFIG_FILE) return true;
|
|
6473
|
+
if ((payload.files ?? []).some((f) => f.path === path)) return true;
|
|
6474
|
+
if ((payload.docs ?? []).some((d) => d.path === path)) return true;
|
|
6475
|
+
if (payload.componentSpec?.specPath === path) return true;
|
|
6476
|
+
return Boolean(payload.tokens?.edits.length) && selectTokenFiles(config, [path]).length > 0;
|
|
6477
|
+
}
|
|
6478
|
+
var MAX_COMPARE_FILES = 300;
|
|
6479
|
+
async function suggestionStalenessOnGitHub(gh, d, input) {
|
|
6480
|
+
const none = (mainSha2, mainMoved) => ({ mainSha: mainSha2, mainMoved, outdated: false, conflicts: [], plan: null });
|
|
6481
|
+
if (!d.parsed.ok || d.issue.state !== "open") return none(null, false);
|
|
6482
|
+
const payload = d.parsed.payload;
|
|
6483
|
+
const ref = input.ref ?? (payload.base.ref || input.config.repo?.defaultBranch || "main");
|
|
6484
|
+
const mainSha = await gh.getBranchSha(ref).catch(() => void 0) ?? null;
|
|
6485
|
+
if (!mainSha) return none(null, false);
|
|
6486
|
+
if (mainSha === payload.base.sha) return none(mainSha, false);
|
|
6487
|
+
const changed = await gh.compare(payload.base.sha, mainSha).catch(() => null);
|
|
6488
|
+
if (changed && changed.files.length < MAX_COMPARE_FILES && !changed.files.some((f) => suggestionTouches(payload, input.config, f.filename))) return none(mainSha, true);
|
|
6489
|
+
try {
|
|
6490
|
+
const plan = await planSuggestionApply({
|
|
6491
|
+
parsed: d.parsed,
|
|
6492
|
+
config: input.config,
|
|
6493
|
+
comments: d.comments,
|
|
6494
|
+
governance: input.governance,
|
|
6495
|
+
author: d.issue.author || null,
|
|
6496
|
+
// Only whether it still applies: approvals and a hand edit are reported apart.
|
|
6497
|
+
forceUnapproved: true,
|
|
6498
|
+
acceptEdited: true,
|
|
6499
|
+
workspace: githubSuggestionWorkspace(gh, mainSha)
|
|
6500
|
+
});
|
|
6501
|
+
const conflicts = plan.ops.filter((o) => o.state === "conflict");
|
|
6502
|
+
return { mainSha, mainMoved: true, outdated: conflicts.length > 0, conflicts, plan };
|
|
6503
|
+
} catch {
|
|
6504
|
+
return none(mainSha, true);
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
function stalenessStatus(status, staleness) {
|
|
6508
|
+
return withOutdated(status, staleness?.outdated);
|
|
6509
|
+
}
|
|
6510
|
+
async function reviewSuggestionOnGitHub(gh, n, input, gov) {
|
|
6511
|
+
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.");
|
|
6512
|
+
const before = await getSuggestionOnGitHub(gh, n, gov);
|
|
6513
|
+
if (!before.parsed.ok) throw new GitHubError(422, `#${n} cannot be reviewed: ${before.parsed.message}`);
|
|
6514
|
+
if (before.issue.state === "closed") throw new GitHubError(422, `#${n} is closed. Reopen it on GitHub to review it again.`);
|
|
6515
|
+
const sha = before.parsed.sha;
|
|
6516
|
+
if (input.payloadSha && input.payloadSha !== sha) throw new GitHubError(409, `#${n} changed since you opened it. Reload it and review the new version.`);
|
|
6517
|
+
const by = input.by ?? (await gh.getViewer().catch(() => ({ login: "" }))).login;
|
|
6518
|
+
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 });
|
|
6519
|
+
const posted = await gh.createIssueComment(n, body);
|
|
6520
|
+
const after = await getSuggestionOnGitHub(gh, n, gov);
|
|
6521
|
+
const status = after.status;
|
|
6522
|
+
const who = (after.comments.find((c) => c.id === posted.id)?.author ?? by).toLowerCase();
|
|
6523
|
+
const counts = input.event === "comment" || [...after.approvals?.approvers ?? [], ...after.approvals?.changesRequested ?? []].some((l) => l.toLowerCase() === who);
|
|
6524
|
+
const warnings = [];
|
|
6525
|
+
if (!counts) warnings.push("Your review is posted but does not count: only the owners listed in bridge.config.json approve suggestions.");
|
|
6526
|
+
if (before.parsed.edited) warnings.push(`#${n} was edited by hand after Bridge wrote it: check the machine-readable block before applying it.`);
|
|
6138
6527
|
const labelWarning = await syncSuggestionStateLabel(gh, n, before.issue.labels, status);
|
|
6139
6528
|
if (labelWarning) warnings.push(labelWarning);
|
|
6140
6529
|
return { commentUrl: posted.url, payloadSha: sha, status, counts, warnings };
|
|
@@ -6143,8 +6532,12 @@ async function linkSuggestionPrOnGitHub(gh, n, link) {
|
|
|
6143
6532
|
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.");
|
|
6144
6533
|
const issue = await gh.getIssue(n);
|
|
6145
6534
|
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() }));
|
|
6146
|
-
const
|
|
6147
|
-
|
|
6535
|
+
const status = appliedStatus(issue, link.merged);
|
|
6536
|
+
const warning = await syncSuggestionStateLabel(gh, n, issue.labels, status);
|
|
6537
|
+
return { warnings: warning ? [warning] : [], status };
|
|
6538
|
+
}
|
|
6539
|
+
function appliedStatus(issue, merged) {
|
|
6540
|
+
return merged || issue.state === "closed" && issue.stateReason === "completed" ? "applied" : "applying";
|
|
6148
6541
|
}
|
|
6149
6542
|
async function declineSuggestionOnGitHub(gh, n, reason, by) {
|
|
6150
6543
|
const issue = await gh.getIssue(n);
|
|
@@ -6675,472 +7068,209 @@ async function switchToPublishedOnGitHub(gh, pr) {
|
|
|
6675
7068
|
return { status: "switched", said: `Your branch now uses the published version from ${pull.baseRef}.` };
|
|
6676
7069
|
}
|
|
6677
7070
|
|
|
6678
|
-
// ../core/src/token-proposal.ts
|
|
6679
|
-
var issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
6680
|
-
function planTokenProposal(input) {
|
|
6681
|
-
const { before, config } = input;
|
|
6682
|
-
const r = applyTokenEdits(before, input.edits);
|
|
6683
|
-
const after = new LayeredTokenSet(before.config, r.files);
|
|
6684
|
-
const changes = diffLayered(before, after);
|
|
6685
|
-
if (!changes.length) return null;
|
|
6686
|
-
const prevErrors = new Set(validateLayered(before).map(issueKey2));
|
|
6687
|
-
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
|
|
6688
|
-
const modes = config.contrast?.modes;
|
|
6689
|
-
const contrast = input.pairs.length ? contrastDelta(runLayeredContrast(before, input.pairs, modes), runLayeredContrast(after, input.pairs, modes)) : [];
|
|
6690
|
-
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
6691
|
-
const impact = layeredImpact(before, after, changes);
|
|
6692
|
-
const tier = classifyLayered(changes, [], config);
|
|
6693
|
-
const summary2 = input.summary?.trim() || input.title;
|
|
6694
|
-
const body = [composeProposalBody({ summary: summary2, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: input.tool ?? "Bridge MCP" }), ...input.bodyExtra ?? []].filter(Boolean).join("\n\n");
|
|
6695
|
-
const slug2 = slugify(input.title, 48) || "change";
|
|
6696
|
-
const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slug2}.md`;
|
|
6697
|
-
const touched = r.touched.map((f) => after.repoPath(f));
|
|
6698
|
-
const files = [
|
|
6699
|
-
...r.touched.map((f) => ({ path: after.repoPath(f), content: after.files.get(f) })),
|
|
6700
|
-
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
6701
|
-
];
|
|
6702
|
-
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;
|
|
6703
|
-
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 };
|
|
6704
|
-
}
|
|
6705
|
-
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
6706
|
-
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
6707
|
-
const jira = opts.jira ?? null;
|
|
6708
|
-
let branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}`, jira);
|
|
6709
|
-
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}-${i}`, jira);
|
|
6710
|
-
const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
|
|
6711
|
-
|
|
6712
|
-
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
6713
|
-
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
6714
|
-
const draft = plan.tier.tier >= 2;
|
|
6715
|
-
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
6716
|
-
await gh.addLabels(pr.number, labels);
|
|
6717
|
-
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
6718
|
-
}
|
|
6719
|
-
var tokenLabels = (plan, before = [], after = []) => [
|
|
6720
|
-
.../* @__PURE__ */ new Set([...before, `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...after])
|
|
6721
|
-
];
|
|
6722
|
-
function tokenSuggestionPayload(plan, ctx) {
|
|
6723
|
-
const tokenFiles = new Set(plan.touched);
|
|
6724
|
-
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 }));
|
|
6725
|
-
return {
|
|
6726
|
-
schema: SUGGESTION_SCHEMA,
|
|
6727
|
-
kind: "token-change",
|
|
6728
|
-
title: plan.title,
|
|
6729
|
-
summary: plan.summary,
|
|
6730
|
-
source: ctx.source,
|
|
6731
|
-
...ctx.suggestedBy ? { suggestedBy: ctx.suggestedBy.replace(/^@/, "") } : {},
|
|
6732
|
-
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6733
|
-
base: ctx.base,
|
|
6734
|
-
tier: { tier: plan.tier.tier, bump: plan.tier.bump },
|
|
6735
|
-
tokens: {
|
|
6736
|
-
edits: plan.edits,
|
|
6737
|
-
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 }))
|
|
6738
|
-
},
|
|
6739
|
-
...files.length ? { files } : {},
|
|
6740
|
-
checks: {
|
|
6741
|
-
newErrors: plan.newErrors.length,
|
|
6742
|
-
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 })),
|
|
6743
|
-
blocked: plan.blocked,
|
|
6744
|
-
impact: plan.impact.total
|
|
6745
|
-
},
|
|
6746
|
-
...ctx.links ? { links: ctx.links } : {},
|
|
6747
|
-
...ctx.previews?.length ? { previews: ctx.previews } : {}
|
|
6748
|
-
};
|
|
6749
|
-
}
|
|
6750
|
-
async function suggestTokenProposalOnGitHub(gh, plan, opts) {
|
|
6751
|
-
if (plan.blocked) throw new Error(`Not suggested: ${plan.blocked}. Bridge never suggests a change that breaks the gate.`);
|
|
6752
|
-
const sha = opts.baseSha ?? await gh.getBranchSha(opts.base);
|
|
6753
|
-
if (!sha) throw new Error(`Branch ${opts.base} not found. Check the default branch in bridge.config.json.`);
|
|
6754
|
-
const links = opts.neededBy || opts.links ? { ...opts.links, ...opts.neededBy ? { neededBy: opts.neededBy } : {} } : void 0;
|
|
6755
|
-
const payload = tokenSuggestionPayload(plan, { base: { ref: opts.base, sha }, source: opts.source, suggestedBy: opts.suggestedBy, links, previews: opts.previews });
|
|
6756
|
-
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
6757
|
-
const created = await createSuggestionOnGitHub(gh, payload, { labels, assignees: opts.assignees, details: plan.body });
|
|
6758
|
-
return { status: "suggested", number: created.number, url: created.url, sha: created.sha, labels, warnings: created.warnings };
|
|
6759
|
-
}
|
|
6760
|
-
|
|
6761
|
-
// ../core/src/figma-push-github.ts
|
|
6762
|
-
var pushSource = (d) => ["figma", "studio", "cli", "mcp"].includes(d.source) ? d.source : "figma";
|
|
6763
|
-
var specTier = (d) => ({ tier: 2, bump: d.kind === "new-component" || d.kind === "new-variant" || d.kind === "prop-change" ? "minor" : "patch" });
|
|
6764
|
-
function componentSpecSuggestionPayload(d, plan, ctx) {
|
|
6765
|
-
const specPath = figmaSpecPath(d);
|
|
6766
|
-
const spec = JSON.parse(formatFigmaSpec(d, plan));
|
|
6767
|
-
const brief = figmaDraftBrief({ ...d, tokenEdits: plan.edits, reasons: plan.reasons }, { suggestion: true, specPath, componentsDir: ctx.componentsDir });
|
|
6768
|
-
const by = (ctx.suggestedBy ?? d.requestedBy)?.replace(/^@/, "");
|
|
6769
|
-
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 } : {}, ...figmaNodes(d) } : void 0;
|
|
6770
|
-
const previews = (d.images ?? []).map((url, i) => ({ label: `${d.component} in Figma${(d.images?.length ?? 0) > 1 ? ` ${i + 1}` : ""}`, url }));
|
|
6771
|
-
return {
|
|
6772
|
-
schema: SUGGESTION_SCHEMA,
|
|
6773
|
-
kind: "component-spec",
|
|
6774
|
-
title: d.title,
|
|
6775
|
-
summary: d.summary.trim() || d.title,
|
|
6776
|
-
source: pushSource(d),
|
|
6777
|
-
...by ? { suggestedBy: by } : {},
|
|
6778
|
-
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6779
|
-
base: ctx.base,
|
|
6780
|
-
tier: specTier(d),
|
|
6781
|
-
componentSpec: { component: d.component, specPath, spec, brief },
|
|
6782
|
-
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 },
|
|
6783
|
-
...figma ? { links: { figma } } : {},
|
|
6784
|
-
...previews.length ? { previews } : {}
|
|
6785
|
-
};
|
|
6786
|
-
}
|
|
6787
|
-
function figmaNodes(d) {
|
|
6788
|
-
const f = d.figma;
|
|
6789
|
-
if (!f) return {};
|
|
6790
|
-
const url = (id, given) => given ?? (f.fileKey ? figmaNodeUrl(f.fileKey, id) : void 0);
|
|
6791
|
-
const nodes = [];
|
|
6792
|
-
if (f.before?.nodeId) nodes.push({ nodeId: f.before.nodeId, state: "before", ...url(f.before.nodeId, f.before.url) ? { url: url(f.before.nodeId, f.before.url) } : {}, ...f.before.nodeName ? { name: f.before.nodeName } : {} });
|
|
6793
|
-
if (f.nodeId) nodes.push({ nodeId: f.nodeId, state: "after", ...url(f.nodeId, f.url) ? { url: url(f.nodeId, f.url) } : {}, ...f.nodeName ? { name: f.nodeName } : {} });
|
|
6794
|
-
return nodes.length ? { nodes } : {};
|
|
6795
|
-
}
|
|
6796
|
-
async function openComponentPushOnGitHub(gh, input) {
|
|
6797
|
-
const d = input.diff;
|
|
6798
|
-
if (!input.plan && !input.tokens) throw new Error("Route the change first: pass the token facts or the plan.");
|
|
6799
|
-
const plan = input.plan ?? planComponentPush(d, input.tokens);
|
|
6800
|
-
if (plan.route === "manual") return { status: "not-sent", plan, link: null };
|
|
6801
|
-
if (input.dryRun) return { status: "dry-run", plan, link: null };
|
|
6802
|
-
const mode = input.mode ?? "direct";
|
|
6803
|
-
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).");
|
|
6804
|
-
const origin = {
|
|
6805
|
-
component: d.component,
|
|
6806
|
-
figmaName: d.figmaName,
|
|
6807
|
-
route: plan.route,
|
|
6808
|
-
kind: d.kind,
|
|
6809
|
-
...d.figma?.nodeId ? { nodeId: d.figma.nodeId } : {},
|
|
6810
|
-
...d.figma?.fileKey ? { fileKey: d.figma.fileKey } : {},
|
|
6811
|
-
...d.figma?.url ? { url: d.figma.url } : {},
|
|
6812
|
-
...d.requestedBy ? { requestedBy: d.requestedBy } : {}
|
|
6813
|
-
};
|
|
6814
|
-
if (plan.route === "token-change") {
|
|
6815
|
-
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`).");
|
|
6816
|
-
const figma = { ...origin.fileKey ? { fileKey: origin.fileKey } : {}, ...origin.nodeId ? { nodeId: origin.nodeId } : {}, ...origin.url ? { url: origin.url } : {}, ...figmaNodes(d) };
|
|
6817
|
-
const r = await input.writeTokens({ edits: plan.edits, title: figmaBranchTitle(d), summary: d.summary.trim() || d.title, bodyExtra: [figmaOriginSection(origin)], mode, figma });
|
|
6818
|
-
await gh.addLabels(r.number, [FROM_FIGMA_LABEL]);
|
|
6819
|
-
if (r.status === "suggested") return { status: "suggested", plan, link: null, suggestion: { route: "token-change", number: r.number, url: r.url } };
|
|
6820
|
-
return { status: "opened", plan, link: { route: "token-change", number: r.number, url: r.url, branch: r.branch } };
|
|
6821
|
-
}
|
|
6822
|
-
if (mode === "suggest") {
|
|
6823
|
-
const sha = input.baseSha ?? await gh.getBranchSha(input.base);
|
|
6824
|
-
if (!sha) throw new GitHubError(404, `Branch ${input.base} not found. Check the default branch in bridge.config.json.`);
|
|
6825
|
-
const payload = componentSpecSuggestionPayload(d, plan, { base: { ref: input.base, sha }, suggestedBy: input.suggestedBy });
|
|
6826
|
-
const created = await createSuggestionOnGitHub(gh, payload, { labels: [...input.labels ?? [], WAITING_FOR_CLAUDE_LABEL] });
|
|
6827
|
-
return { status: "suggested", plan, link: null, suggestion: { route: "claude-draft", number: created.number, url: created.url } };
|
|
6828
|
-
}
|
|
6829
|
-
const spec = figmaSpecPath(d);
|
|
6830
|
-
const o = { ...origin, spec, status: "spec" };
|
|
6831
|
-
let branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}`;
|
|
6832
|
-
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}-${i}`;
|
|
6833
|
-
await gh.commitToNewBranch({
|
|
6834
|
-
base: input.base,
|
|
6835
|
-
branch,
|
|
6836
|
-
message: `chore(figma): spec for ${figmaBranchTitle(d)}
|
|
6837
|
-
|
|
6838
|
-
Sent from Figma with ${input.tool ?? "the DS Bridge Figma plugin"}. Claude AI drafts the code on this branch; a person reviews it.`,
|
|
6839
|
-
files: [{ path: spec, content: formatFigmaSpec(d, plan) }]
|
|
6840
|
-
});
|
|
6841
|
-
const pr = await gh.createPullRequest({ base: input.base, head: branch, title: figmaBranchTitle(d), body: figmaDraftBody(d, plan, o), draft: true });
|
|
6842
|
-
await gh.addLabels(pr.number, [.../* @__PURE__ */ new Set([...input.labels ?? [], FROM_FIGMA_LABEL, REQUEST_LABEL, WAITING_FOR_CLAUDE_LABEL])]);
|
|
6843
|
-
return { status: "opened", plan, link: { route: "claude-draft", number: pr.number, url: pr.url, branch } };
|
|
6844
|
-
}
|
|
6845
|
-
async function listFigmaChangesOnGitHub(gh, opts = {}) {
|
|
6846
|
-
const pulls = await gh.listPullRequests(opts.state ?? "open");
|
|
6847
|
-
const out = [];
|
|
6848
|
-
for (const p of pulls) {
|
|
6849
|
-
if (!p.labels.includes(FROM_FIGMA_LABEL)) continue;
|
|
6850
|
-
const origin = parseFigmaOrigin(p.body);
|
|
6851
|
-
if (!origin) continue;
|
|
6852
|
-
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 }) });
|
|
6853
|
-
}
|
|
6854
|
-
return out;
|
|
6855
|
-
}
|
|
6856
|
-
async function figmaChangeOnGitHub(gh, n, opts = {}) {
|
|
6857
|
-
const pull = await gh.getPullRequest(n);
|
|
6858
|
-
const origin = parseFigmaOrigin(pull.body);
|
|
6859
|
-
if (!origin) throw new GitHubError(404, `#${n} was not sent from Figma (no "From Figma" block in its description).`);
|
|
6860
|
-
const status = figmaPushStatus({ state: pull.state, draft: pull.draft, labels: pull.labels, route: origin.route });
|
|
6861
|
-
if (origin.route === "token-change") return { pull, origin, status, spec: void 0, brief: void 0 };
|
|
6862
|
-
const text = origin.spec ? await gh.getFileText(origin.spec, pull.headSha).catch(() => void 0) : void 0;
|
|
6863
|
-
const spec = parseFigmaSpec(text);
|
|
6864
|
-
if (!spec) throw new GitHubError(404, `The spec ${origin.spec ?? FIGMA_SPEC_DIR} is missing from ${pull.headRef}. Send the component from Figma again.`);
|
|
6865
|
-
const brief = figmaDraftBrief(spec, { pr: n, branch: pull.headRef, specPath: origin.spec, componentsDir: opts.componentsDir });
|
|
6866
|
-
return { pull, origin, status, spec, brief };
|
|
6867
|
-
}
|
|
6868
|
-
async function markFigmaChangeDraftedOnGitHub(gh, n) {
|
|
6869
|
-
if (!gh.canWrite) throw new GitHubError(401, "Marking a branch drafted needs a GitHub token (pull requests: write).");
|
|
6870
|
-
const pull = await gh.getPullRequest(n);
|
|
6871
|
-
const origin = parseFigmaOrigin(pull.body);
|
|
6872
|
-
if (!origin || origin.route !== "claude-draft") throw new GitHubError(422, `#${n} is not a Claude draft branch from Figma.`);
|
|
6873
|
-
if (pull.state !== "open") throw new GitHubError(422, `#${n} is ${pull.state}: only an open branch is marked drafted.`);
|
|
6874
|
-
const files = (await gh.listPullRequestFiles(n)).map((f) => f.filename).filter((f) => !f.startsWith(`${FIGMA_SPEC_DIR}/`));
|
|
6875
|
-
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.`);
|
|
6876
|
-
await gh.updatePullRequest(n, { body: withFigmaOrigin(pull.body, { ...origin, status: "drafted" }) });
|
|
6877
|
-
await gh.removeLabel(n, WAITING_FOR_CLAUDE_LABEL).catch(() => void 0);
|
|
6878
|
-
await gh.addLabels(n, [DRAFTED_BY_CLAUDE_LABEL]);
|
|
6879
|
-
const labels = [...pull.labels.filter((l) => l !== WAITING_FOR_CLAUDE_LABEL), DRAFTED_BY_CLAUDE_LABEL];
|
|
6880
|
-
return { status: figmaPushStatus({ state: pull.state, labels, route: origin.route }), files };
|
|
6881
|
-
}
|
|
6882
|
-
|
|
6883
|
-
// ../core/src/suggestion-apply.ts
|
|
6884
|
-
var SUGGESTION_APPLY_EXIT = {
|
|
6885
|
-
ready: 0,
|
|
6886
|
-
"already-applied": 0,
|
|
6887
|
-
conflict: 3,
|
|
6888
|
-
"not-approved": 1,
|
|
6889
|
-
blocked: 1,
|
|
6890
|
-
refused: 1
|
|
6891
|
-
};
|
|
6892
|
-
var same2 = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
6893
|
-
var show3 = (v) => v === void 0 ? "nothing" : typeof v === "string" ? v : JSON.stringify(v);
|
|
6894
|
-
function tokenRoot(config) {
|
|
6895
|
-
const r = config.tokens.root.replace(/\/+$/, "");
|
|
6896
|
-
return r === "." ? "" : r;
|
|
6897
|
-
}
|
|
6898
|
-
function tokenFileOf(config, path) {
|
|
6899
|
-
const root = tokenRoot(config);
|
|
6900
|
-
if (root && !path.startsWith(`${root}/`)) return null;
|
|
6901
|
-
if (!selectTokenFiles(config, [path]).length) return null;
|
|
6902
|
-
return root ? path.slice(root.length + 1) : path;
|
|
6903
|
-
}
|
|
6904
|
-
function planTokenEdit(set, e, changes) {
|
|
6905
|
-
const base = { type: "token", path: e.path, edit: e, ...e.file ? { file: e.file } : {} };
|
|
6906
|
-
const defsAt = (path) => set.defs.filter((d) => d.path === path && (!e.file || d.file === e.file));
|
|
6907
|
-
const recorded = (file, path = e.path) => changes.find((c) => c.path === path && c.file === file && (e.op === "rename" ? c.kind === "renamed" : true));
|
|
6908
|
-
const conflict = (detail, extra = {}) => ({ op: { ...base, state: "conflict", detail, ...extra }, notes: [] });
|
|
6909
|
-
const done = (detail) => ({ op: { ...base, state: "already-applied", detail }, notes: [] });
|
|
6910
|
-
let result;
|
|
6911
|
-
try {
|
|
6912
|
-
result = applyTokenEdits(set, [e]);
|
|
6913
|
-
} catch (err) {
|
|
6914
|
-
const why = err instanceof Error ? err.message : String(err);
|
|
6915
|
-
switch (e.op) {
|
|
6916
|
-
case "rename": {
|
|
6917
|
-
const hasOld = defsAt(e.path).length > 0;
|
|
6918
|
-
const hasNew = set.defs.some((d) => d.path === e.newPath);
|
|
6919
|
-
if (!hasOld && hasNew) return done(`Already renamed to ${e.newPath}.`);
|
|
6920
|
-
return conflict(hasOld && hasNew ? `Both ${e.path} and ${e.newPath} exist now: rename it by hand.` : `${e.path} no longer exists: ${why}`);
|
|
6921
|
-
}
|
|
6922
|
-
case "remove":
|
|
6923
|
-
if (!defsAt(e.path).length) return done("Already removed.");
|
|
6924
|
-
return conflict(why);
|
|
6925
|
-
case "add": {
|
|
6926
|
-
const existing = defsAt(e.path);
|
|
6927
|
-
if (!existing.length) return conflict(why);
|
|
6928
|
-
const want = (file) => {
|
|
6929
|
-
const c = recorded(file);
|
|
6930
|
-
if (c && c.after !== void 0) return c.after;
|
|
6931
|
-
return coerceValue(e.value, e.type ?? existing.find((d2) => d2.file === file)?.type);
|
|
6932
|
-
};
|
|
6933
|
-
const all = existing.every((d2) => same2(d2.value, want(d2.file)));
|
|
6934
|
-
if (all) return done("Already added with the suggested value.");
|
|
6935
|
-
const d = existing.find((x) => !same2(x.value, want(x.file)));
|
|
6936
|
-
return conflict(`Someone added ${e.path} with another value since.`, { file: d.file, after: want(d.file), now: d.value });
|
|
6937
|
-
}
|
|
6938
|
-
default:
|
|
6939
|
-
return conflict(defsAt(e.path).length ? why : `${e.path} no longer exists, so its value cannot be set.`);
|
|
6940
|
-
}
|
|
6941
|
-
}
|
|
6942
|
-
if (!result.touched.length) return done("Already has the suggested value.");
|
|
6943
|
-
if (e.op === "set" || e.op === "remove" || e.op === "rename") {
|
|
6944
|
-
for (const d of defsAt(e.path)) {
|
|
6945
|
-
const c = recorded(d.file, e.op === "rename" ? e.newPath ?? e.path : e.path);
|
|
6946
|
-
if (!c) continue;
|
|
6947
|
-
const okNow = same2(d.value, c.before) || e.op === "set" && same2(d.value, c.after);
|
|
6948
|
-
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 });
|
|
6949
|
-
}
|
|
6950
|
-
}
|
|
6951
|
-
const verb = e.op === "set" ? "Set" : e.op === "add" ? "Add" : e.op === "remove" ? "Remove" : `Rename to ${e.newPath ?? ""}`;
|
|
6952
|
-
return { op: { ...base, state: "apply", detail: `${verb} in ${result.touched.join(", ")}.` }, files: result.files, notes: result.notes };
|
|
6953
|
-
}
|
|
6954
|
-
async function planFile(ws, baseSha, f) {
|
|
6955
|
-
const base = { type: "file", path: f.path };
|
|
6956
|
-
const cur = await ws.read(f.path);
|
|
6957
|
-
const curSha = cur?.sha ?? null;
|
|
6958
|
-
const theirs = f.afterSha === null ? null : f.content ?? null;
|
|
6959
|
-
if (curSha === f.afterSha) return { op: { ...base, state: "already-applied", detail: f.afterSha === null ? "Already deleted." : "Already has the suggested content." } };
|
|
6960
|
-
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 };
|
|
6961
|
-
if (f.beforeSha === null) return { op: { ...base, state: "conflict", detail: "The suggestion creates this file, but a different one exists now." } };
|
|
6962
|
-
if (!cur) return { op: { ...base, state: "conflict", detail: "The suggestion changes this file, but it was deleted since." } };
|
|
6963
|
-
if (theirs === null) return { op: { ...base, state: "conflict", detail: "The suggestion deletes this file, but it changed since." } };
|
|
6964
|
-
const original = await ws.readAt(baseSha, f.path);
|
|
6965
|
-
if (original === null || gitBlobSha(original) !== f.beforeSha || !ws.merge) {
|
|
6966
|
-
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." } };
|
|
6967
|
-
}
|
|
6968
|
-
const merged = await ws.merge({ path: f.path, ours: cur.text, base: original, theirs });
|
|
6969
|
-
if (!merged.clean) return { op: { ...base, state: "conflict", detail: `The file changed since and the merge has ${merged.conflicts} conflict(s).`, hunks: merged.conflicts } };
|
|
6970
|
-
if (merged.text === cur.text) return { op: { ...base, state: "already-applied", detail: "Already has the suggested lines." } };
|
|
6971
|
-
return { op: { ...base, state: "apply-merged", detail: "The file changed since: merged with the suggested lines, no conflict." }, content: merged.text };
|
|
6972
|
-
}
|
|
6973
|
-
async function planSuggestionApply(input) {
|
|
6974
|
-
const { parsed, config, workspace: ws } = input;
|
|
6975
|
-
const payload = parsed.payload;
|
|
6976
|
-
const notes = [];
|
|
6977
|
-
const out = (outcome, message, rest2 = {}) => ({
|
|
6978
|
-
outcome,
|
|
6979
|
-
exitCode: SUGGESTION_APPLY_EXIT[outcome],
|
|
6980
|
-
message,
|
|
6981
|
-
payloadSha: parsed.sha,
|
|
6982
|
-
payload,
|
|
6983
|
-
gate: null,
|
|
6984
|
-
ops: [],
|
|
6985
|
-
writes: [],
|
|
6986
|
-
checks: null,
|
|
6987
|
-
problems: [],
|
|
6988
|
-
notes,
|
|
6989
|
-
...payload.componentSpec ? { brief: payload.componentSpec.brief } : {},
|
|
6990
|
-
...rest2
|
|
6991
|
-
});
|
|
6992
|
-
const problems = suggestionPathProblems(payload, config);
|
|
6993
|
-
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.`);
|
|
6994
|
-
for (const f of payload.files ?? []) {
|
|
6995
|
-
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.`);
|
|
6996
|
-
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.`);
|
|
6997
|
-
}
|
|
6998
|
-
if (problems.length) return out("refused", "Not applied: the suggestion asks for something Bridge never writes.", { problems: [...new Set(problems)] });
|
|
6999
|
-
if (parsed.edited && !input.acceptEdited) {
|
|
7000
|
-
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.', {
|
|
7001
|
-
problems: ["The payload no longer matches the sha Bridge recorded."]
|
|
7002
|
-
});
|
|
7003
|
-
}
|
|
7004
|
-
const root = tokenRoot(config);
|
|
7005
|
-
const tokenFiles = selectTokenFiles(config, await ws.list());
|
|
7006
|
-
const targets = [
|
|
7007
|
-
...(payload.files ?? []).map((f) => f.path),
|
|
7008
|
-
...(payload.docs ?? []).map((d) => d.path),
|
|
7009
|
-
...payload.componentSpec ? [payload.componentSpec.specPath] : [],
|
|
7010
|
-
...payload.tokens?.edits.length ? tokenFiles.map((f) => root ? `${root}/${f}` : f) : []
|
|
7011
|
-
];
|
|
7012
|
-
if (ws.isSymlink) {
|
|
7013
|
-
const links = [];
|
|
7014
|
-
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.`);
|
|
7015
|
-
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
7016
|
-
}
|
|
7017
|
-
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
7018
|
-
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
|
|
7019
|
-
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
7020
|
-
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
7021
|
-
const approved = review === "approved";
|
|
7022
|
-
const gate = { review, missing, approvals, forced: !approved && Boolean(input.forceUnapproved), edited: parsed.edited };
|
|
7023
|
-
const ops = [];
|
|
7024
|
-
const writes = /* @__PURE__ */ new Map();
|
|
7025
|
-
const current = /* @__PURE__ */ new Map();
|
|
7026
|
-
for (const f of tokenFiles) {
|
|
7027
|
-
const r = await ws.read(root ? `${root}/${f}` : f);
|
|
7028
|
-
if (r) current.set(f, r.text);
|
|
7029
|
-
}
|
|
7030
|
-
const before = new LayeredTokenSet(config, current);
|
|
7031
|
-
let set = before;
|
|
7032
|
-
const appliedEdits = [];
|
|
7033
|
-
for (const e of payload.tokens?.edits ?? []) {
|
|
7034
|
-
const r = planTokenEdit(set, e, payload.tokens.changes);
|
|
7035
|
-
ops.push(r.op);
|
|
7036
|
-
notes.push(...r.notes);
|
|
7037
|
-
if (r.op.state === "apply" && r.files) {
|
|
7038
|
-
set = new LayeredTokenSet(config, r.files);
|
|
7039
|
-
appliedEdits.push(e);
|
|
7040
|
-
}
|
|
7041
|
-
}
|
|
7042
|
-
for (const [f, text] of set.files) if (text !== before.files.get(f)) writes.set(set.repoPath(f), text);
|
|
7043
|
-
for (const f of payload.files ?? []) {
|
|
7044
|
-
if (writes.has(f.path)) {
|
|
7045
|
-
notes.push(`${f.path}: the token edits write this file, its copy in the suggestion is not used.`);
|
|
7046
|
-
continue;
|
|
7047
|
-
}
|
|
7048
|
-
const r = await planFile(ws, payload.base.sha, f);
|
|
7049
|
-
ops.push(r.op);
|
|
7050
|
-
if (r.op.state === "apply" || r.op.state === "apply-merged") writes.set(f.path, r.content ?? null);
|
|
7051
|
-
}
|
|
7052
|
-
for (const d of payload.docs ?? []) {
|
|
7053
|
-
const base = { type: "docs", path: d.path };
|
|
7054
|
-
const docs = d.after;
|
|
7055
|
-
let text;
|
|
7056
|
-
try {
|
|
7057
|
-
const issues = validateDocs(docs);
|
|
7058
|
-
if (issues.length) {
|
|
7059
|
-
problems.push(`${d.path}: ${issues.map((i) => i.message).join(" ")}`);
|
|
7060
|
-
continue;
|
|
7061
|
-
}
|
|
7062
|
-
const cur = await ws.read(d.path);
|
|
7063
|
-
text = docsFileText(docs, cur?.text ?? null);
|
|
7064
|
-
if (cur && text === cur.text) {
|
|
7065
|
-
ops.push({ ...base, state: "already-applied", detail: "The docs already say this." });
|
|
7066
|
-
continue;
|
|
7067
|
-
}
|
|
7068
|
-
ops.push({ ...base, state: "apply", detail: cur ? "Update the changed fields; other fields stay as they are now." : "Create the docs file." });
|
|
7069
|
-
writes.set(d.path, text);
|
|
7070
|
-
} catch (err) {
|
|
7071
|
-
problems.push(`${d.path}: the suggested docs are not a docs.json Bridge can read (${err instanceof Error ? err.message : String(err)}).`);
|
|
7072
|
-
}
|
|
7073
|
-
}
|
|
7074
|
-
if (payload.componentSpec) {
|
|
7075
|
-
const s = payload.componentSpec;
|
|
7076
|
-
const text = `${JSON.stringify(s.spec, null, 2)}
|
|
7077
|
-
`;
|
|
7078
|
-
const cur = await ws.read(s.specPath);
|
|
7079
|
-
if (cur && cur.text === text) ops.push({ type: "component-spec", path: s.specPath, state: "already-applied", detail: "The spec file is already there." });
|
|
7080
|
-
else {
|
|
7081
|
-
ops.push({ type: "component-spec", path: s.specPath, state: "apply", detail: `Write the spec; the ${s.component} code itself is written from the brief.` });
|
|
7082
|
-
writes.set(s.specPath, text);
|
|
7083
|
-
}
|
|
7084
|
-
}
|
|
7085
|
-
const afterFiles = new Map(set.files);
|
|
7086
|
-
for (const [p, content] of writes) {
|
|
7087
|
-
const tf = tokenFileOf(config, p);
|
|
7088
|
-
if (!tf) continue;
|
|
7089
|
-
if (content === null) afterFiles.delete(tf);
|
|
7090
|
-
else afterFiles.set(tf, content);
|
|
7091
|
-
}
|
|
7092
|
-
const after = new LayeredTokenSet(config, afterFiles);
|
|
7093
|
-
const tokenChanges = diffLayered(before, after);
|
|
7094
|
-
const issueKey4 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
7095
|
-
const prevErrors = new Set(validateLayered(before).map(issueKey4));
|
|
7096
|
-
const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey4(i))) : [];
|
|
7097
|
-
let pairs = [];
|
|
7098
|
-
if (config.contrast && tokenChanges.length) {
|
|
7099
|
-
try {
|
|
7100
|
-
pairs = parseContrastChecks((await ws.read(config.contrast.path))?.text);
|
|
7101
|
-
} catch {
|
|
7102
|
-
notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
|
|
7103
|
-
}
|
|
7104
|
-
}
|
|
7071
|
+
// ../core/src/token-proposal.ts
|
|
7072
|
+
var issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
7073
|
+
function planTokenProposal(input) {
|
|
7074
|
+
const { before, config } = input;
|
|
7075
|
+
const r = applyTokenEdits(before, input.edits);
|
|
7076
|
+
const after = new LayeredTokenSet(before.config, r.files);
|
|
7077
|
+
const changes = diffLayered(before, after);
|
|
7078
|
+
if (!changes.length) return null;
|
|
7079
|
+
const prevErrors = new Set(validateLayered(before).map(issueKey2));
|
|
7080
|
+
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i)));
|
|
7105
7081
|
const modes = config.contrast?.modes;
|
|
7106
|
-
const contrast = pairs.length ? contrastDelta(runLayeredContrast(before, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
7082
|
+
const contrast = input.pairs.length ? contrastDelta(runLayeredContrast(before, input.pairs, modes), runLayeredContrast(after, input.pairs, modes)) : [];
|
|
7107
7083
|
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
7108
|
-
const
|
|
7109
|
-
const tier =
|
|
7110
|
-
const
|
|
7111
|
-
const
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
const
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
if (
|
|
7124
|
-
|
|
7084
|
+
const impact = layeredImpact(before, after, changes);
|
|
7085
|
+
const tier = classifyLayered(changes, [], config);
|
|
7086
|
+
const summary2 = input.summary?.trim() || input.title;
|
|
7087
|
+
const body = [composeProposalBody({ summary: summary2, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: input.tool ?? "Bridge MCP" }), ...input.bodyExtra ?? []].filter(Boolean).join("\n\n");
|
|
7088
|
+
const slug2 = slugify(input.title, 48) || "change";
|
|
7089
|
+
const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slug2}.md`;
|
|
7090
|
+
const touched = r.touched.map((f) => after.repoPath(f));
|
|
7091
|
+
const files = [
|
|
7092
|
+
...r.touched.map((f) => ({ path: after.repoPath(f), content: after.files.get(f) })),
|
|
7093
|
+
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary2) }] : []
|
|
7094
|
+
];
|
|
7095
|
+
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;
|
|
7096
|
+
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 };
|
|
7097
|
+
}
|
|
7098
|
+
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
7099
|
+
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
7100
|
+
const jira = opts.jira ?? null;
|
|
7101
|
+
let branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}`, jira);
|
|
7102
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = branchWithJiraKey(`${opts.branchPrefix}${plan.slug}-${i}`, jira);
|
|
7103
|
+
const commit = await gh.commitToNewBranch({ base: opts.base, branch, message: `feat(tokens): ${plan.title}
|
|
7104
|
+
|
|
7105
|
+
Proposed through ${opts.tool ?? "Bridge MCP"}. Tier ${plan.tier.tier}.`, files: plan.files });
|
|
7106
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
7107
|
+
const draft = plan.tier.tier >= 2;
|
|
7108
|
+
const pr = await gh.createPullRequest({ base: opts.base, head: branch, title: titleWithJiraKey(`feat(tokens): ${plan.title}`, jira), body: plan.body, draft });
|
|
7109
|
+
await gh.addLabels(pr.number, labels);
|
|
7110
|
+
return { branch, commit: commit.sha, pr: pr.number, url: pr.url, labels, draft };
|
|
7111
|
+
}
|
|
7112
|
+
var tokenLabels = (plan, before = [], after = []) => [
|
|
7113
|
+
.../* @__PURE__ */ new Set([...before, `tier: ${plan.tier.tier}`, ...plan.tier.tier === 3 ? ["tokens"] : [], ...plan.tier.bump === "major" ? ["breaking"] : [], ...after])
|
|
7114
|
+
];
|
|
7115
|
+
function tokenSuggestionPayload(plan, ctx) {
|
|
7116
|
+
const tokenFiles = new Set(plan.touched);
|
|
7117
|
+
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 }));
|
|
7118
|
+
return {
|
|
7119
|
+
schema: SUGGESTION_SCHEMA,
|
|
7120
|
+
kind: "token-change",
|
|
7121
|
+
title: plan.title,
|
|
7122
|
+
summary: plan.summary,
|
|
7123
|
+
source: ctx.source,
|
|
7124
|
+
...ctx.suggestedBy ? { suggestedBy: ctx.suggestedBy.replace(/^@/, "") } : {},
|
|
7125
|
+
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7126
|
+
base: ctx.base,
|
|
7127
|
+
tier: { tier: plan.tier.tier, bump: plan.tier.bump },
|
|
7128
|
+
tokens: {
|
|
7129
|
+
edits: plan.edits,
|
|
7130
|
+
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 }))
|
|
7131
|
+
},
|
|
7132
|
+
...files.length ? { files } : {},
|
|
7133
|
+
checks: {
|
|
7134
|
+
newErrors: plan.newErrors.length,
|
|
7135
|
+
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 })),
|
|
7136
|
+
blocked: plan.blocked,
|
|
7137
|
+
impact: plan.impact.total
|
|
7138
|
+
},
|
|
7139
|
+
...ctx.links ? { links: ctx.links } : {},
|
|
7140
|
+
...ctx.previews?.length ? { previews: ctx.previews } : {}
|
|
7141
|
+
};
|
|
7142
|
+
}
|
|
7143
|
+
async function suggestTokenProposalOnGitHub(gh, plan, opts) {
|
|
7144
|
+
if (plan.blocked) throw new Error(`Not suggested: ${plan.blocked}. Bridge never suggests a change that breaks the gate.`);
|
|
7145
|
+
const sha = opts.baseSha ?? await gh.getBranchSha(opts.base);
|
|
7146
|
+
if (!sha) throw new Error(`Branch ${opts.base} not found. Check the default branch in bridge.config.json.`);
|
|
7147
|
+
const links = opts.neededBy || opts.links ? { ...opts.links, ...opts.neededBy ? { neededBy: opts.neededBy } : {} } : void 0;
|
|
7148
|
+
const payload = tokenSuggestionPayload(plan, { base: { ref: opts.base, sha }, source: opts.source, suggestedBy: opts.suggestedBy, links, previews: opts.previews });
|
|
7149
|
+
const labels = tokenLabels(plan, opts.labels, opts.labelsAfter);
|
|
7150
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels, assignees: opts.assignees, details: plan.body });
|
|
7151
|
+
return { status: "suggested", number: created.number, url: created.url, sha: created.sha, labels, warnings: created.warnings };
|
|
7152
|
+
}
|
|
7153
|
+
|
|
7154
|
+
// ../core/src/figma-push-github.ts
|
|
7155
|
+
var pushSource = (d) => ["figma", "studio", "cli", "mcp"].includes(d.source) ? d.source : "figma";
|
|
7156
|
+
var specTier = (d) => ({ tier: 2, bump: d.kind === "new-component" || d.kind === "new-variant" || d.kind === "prop-change" ? "minor" : "patch" });
|
|
7157
|
+
function componentSpecSuggestionPayload(d, plan, ctx) {
|
|
7158
|
+
const specPath = figmaSpecPath(d);
|
|
7159
|
+
const spec = JSON.parse(formatFigmaSpec(d, plan));
|
|
7160
|
+
const brief = figmaDraftBrief({ ...d, tokenEdits: plan.edits, reasons: plan.reasons }, { suggestion: true, specPath, componentsDir: ctx.componentsDir });
|
|
7161
|
+
const by = (ctx.suggestedBy ?? d.requestedBy)?.replace(/^@/, "");
|
|
7162
|
+
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 } : {}, ...figmaNodes(d) } : void 0;
|
|
7163
|
+
const previews = (d.images ?? []).map((url, i) => ({ label: `${d.component} in Figma${(d.images?.length ?? 0) > 1 ? ` ${i + 1}` : ""}`, url }));
|
|
7164
|
+
return {
|
|
7165
|
+
schema: SUGGESTION_SCHEMA,
|
|
7166
|
+
kind: "component-spec",
|
|
7167
|
+
title: d.title,
|
|
7168
|
+
summary: d.summary.trim() || d.title,
|
|
7169
|
+
source: pushSource(d),
|
|
7170
|
+
...by ? { suggestedBy: by } : {},
|
|
7171
|
+
createdAt: ctx.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7172
|
+
base: ctx.base,
|
|
7173
|
+
tier: specTier(d),
|
|
7174
|
+
componentSpec: { component: d.component, specPath, spec, brief },
|
|
7175
|
+
checks: { newErrors: 0, contrast: [], blocked: null, impact: 0 },
|
|
7176
|
+
...figma ? { links: { figma } } : {},
|
|
7177
|
+
...previews.length ? { previews } : {}
|
|
7178
|
+
};
|
|
7179
|
+
}
|
|
7180
|
+
function figmaNodes(d) {
|
|
7181
|
+
const f = d.figma;
|
|
7182
|
+
if (!f) return {};
|
|
7183
|
+
const url = (id, given) => given ?? (f.fileKey ? figmaNodeUrl(f.fileKey, id) : void 0);
|
|
7184
|
+
const nodes = [];
|
|
7185
|
+
if (f.before?.nodeId) nodes.push({ nodeId: f.before.nodeId, state: "before", ...url(f.before.nodeId, f.before.url) ? { url: url(f.before.nodeId, f.before.url) } : {}, ...f.before.nodeName ? { name: f.before.nodeName } : {} });
|
|
7186
|
+
if (f.nodeId) nodes.push({ nodeId: f.nodeId, state: "after", ...url(f.nodeId, f.url) ? { url: url(f.nodeId, f.url) } : {}, ...f.nodeName ? { name: f.nodeName } : {} });
|
|
7187
|
+
return nodes.length ? { nodes } : {};
|
|
7188
|
+
}
|
|
7189
|
+
async function openComponentPushOnGitHub(gh, input) {
|
|
7190
|
+
const d = input.diff;
|
|
7191
|
+
if (!input.plan && !input.tokens) throw new Error("Route the change first: pass the token facts or the plan.");
|
|
7192
|
+
const plan = input.plan ?? planComponentPush(d, input.tokens);
|
|
7193
|
+
if (plan.route === "manual") return { status: "not-sent", plan, link: null };
|
|
7194
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null };
|
|
7195
|
+
const mode = input.mode ?? "direct";
|
|
7196
|
+
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).");
|
|
7197
|
+
const origin = {
|
|
7198
|
+
component: d.component,
|
|
7199
|
+
figmaName: d.figmaName,
|
|
7200
|
+
route: plan.route,
|
|
7201
|
+
kind: d.kind,
|
|
7202
|
+
...d.figma?.nodeId ? { nodeId: d.figma.nodeId } : {},
|
|
7203
|
+
...d.figma?.fileKey ? { fileKey: d.figma.fileKey } : {},
|
|
7204
|
+
...d.figma?.url ? { url: d.figma.url } : {},
|
|
7205
|
+
...d.requestedBy ? { requestedBy: d.requestedBy } : {}
|
|
7206
|
+
};
|
|
7207
|
+
if (plan.route === "token-change") {
|
|
7208
|
+
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`).");
|
|
7209
|
+
const figma = { ...origin.fileKey ? { fileKey: origin.fileKey } : {}, ...origin.nodeId ? { nodeId: origin.nodeId } : {}, ...origin.url ? { url: origin.url } : {}, ...figmaNodes(d) };
|
|
7210
|
+
const r = await input.writeTokens({ edits: plan.edits, title: figmaBranchTitle(d), summary: d.summary.trim() || d.title, bodyExtra: [figmaOriginSection(origin)], mode, figma });
|
|
7211
|
+
await gh.addLabels(r.number, [FROM_FIGMA_LABEL]);
|
|
7212
|
+
if (r.status === "suggested") return { status: "suggested", plan, link: null, suggestion: { route: "token-change", number: r.number, url: r.url } };
|
|
7213
|
+
return { status: "opened", plan, link: { route: "token-change", number: r.number, url: r.url, branch: r.branch } };
|
|
7125
7214
|
}
|
|
7126
|
-
if (
|
|
7127
|
-
const
|
|
7128
|
-
|
|
7215
|
+
if (mode === "suggest") {
|
|
7216
|
+
const sha = input.baseSha ?? await gh.getBranchSha(input.base);
|
|
7217
|
+
if (!sha) throw new GitHubError(404, `Branch ${input.base} not found. Check the default branch in bridge.config.json.`);
|
|
7218
|
+
const payload = componentSpecSuggestionPayload(d, plan, { base: { ref: input.base, sha }, suggestedBy: input.suggestedBy });
|
|
7219
|
+
const created = await createSuggestionOnGitHub(gh, payload, { labels: [...input.labels ?? [], WAITING_FOR_CLAUDE_LABEL] });
|
|
7220
|
+
return { status: "suggested", plan, link: null, suggestion: { route: "claude-draft", number: created.number, url: created.url } };
|
|
7129
7221
|
}
|
|
7130
|
-
const
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7222
|
+
const spec = figmaSpecPath(d);
|
|
7223
|
+
const o = { ...origin, spec, status: "spec" };
|
|
7224
|
+
let branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}`;
|
|
7225
|
+
for (let i = 2; await gh.getBranchSha(branch).then(Boolean, () => false); i++) branch = `${input.branchPrefix}figma-${figmaSpecSlug(d)}-${i}`;
|
|
7226
|
+
await gh.commitToNewBranch({
|
|
7227
|
+
base: input.base,
|
|
7228
|
+
branch,
|
|
7229
|
+
message: `chore(figma): spec for ${figmaBranchTitle(d)}
|
|
7230
|
+
|
|
7231
|
+
Sent from Figma with ${input.tool ?? "the DS Bridge Figma plugin"}. Claude AI drafts the code on this branch; a person reviews it.`,
|
|
7232
|
+
files: [{ path: spec, content: formatFigmaSpec(d, plan) }]
|
|
7135
7233
|
});
|
|
7234
|
+
const pr = await gh.createPullRequest({ base: input.base, head: branch, title: figmaBranchTitle(d), body: figmaDraftBody(d, plan, o), draft: true });
|
|
7235
|
+
await gh.addLabels(pr.number, [.../* @__PURE__ */ new Set([...input.labels ?? [], FROM_FIGMA_LABEL, REQUEST_LABEL, WAITING_FOR_CLAUDE_LABEL])]);
|
|
7236
|
+
return { status: "opened", plan, link: { route: "claude-draft", number: pr.number, url: pr.url, branch } };
|
|
7136
7237
|
}
|
|
7137
|
-
function
|
|
7138
|
-
|
|
7238
|
+
async function listFigmaChangesOnGitHub(gh, opts = {}) {
|
|
7239
|
+
const pulls = await gh.listPullRequests(opts.state ?? "open");
|
|
7240
|
+
const out = [];
|
|
7241
|
+
for (const p of pulls) {
|
|
7242
|
+
if (!p.labels.includes(FROM_FIGMA_LABEL)) continue;
|
|
7243
|
+
const origin = parseFigmaOrigin(p.body);
|
|
7244
|
+
if (!origin) continue;
|
|
7245
|
+
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 }) });
|
|
7246
|
+
}
|
|
7247
|
+
return out;
|
|
7139
7248
|
}
|
|
7140
|
-
function
|
|
7141
|
-
const
|
|
7142
|
-
const
|
|
7143
|
-
|
|
7249
|
+
async function figmaChangeOnGitHub(gh, n, opts = {}) {
|
|
7250
|
+
const pull = await gh.getPullRequest(n);
|
|
7251
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
7252
|
+
if (!origin) throw new GitHubError(404, `#${n} was not sent from Figma (no "From Figma" block in its description).`);
|
|
7253
|
+
const status = figmaPushStatus({ state: pull.state, draft: pull.draft, labels: pull.labels, route: origin.route });
|
|
7254
|
+
if (origin.route === "token-change") return { pull, origin, status, spec: void 0, brief: void 0 };
|
|
7255
|
+
const text = origin.spec ? await gh.getFileText(origin.spec, pull.headSha).catch(() => void 0) : void 0;
|
|
7256
|
+
const spec = parseFigmaSpec(text);
|
|
7257
|
+
if (!spec) throw new GitHubError(404, `The spec ${origin.spec ?? FIGMA_SPEC_DIR} is missing from ${pull.headRef}. Send the component from Figma again.`);
|
|
7258
|
+
const brief = figmaDraftBrief(spec, { pr: n, branch: pull.headRef, specPath: origin.spec, componentsDir: opts.componentsDir });
|
|
7259
|
+
return { pull, origin, status, spec, brief };
|
|
7260
|
+
}
|
|
7261
|
+
async function markFigmaChangeDraftedOnGitHub(gh, n) {
|
|
7262
|
+
if (!gh.canWrite) throw new GitHubError(401, "Marking a branch drafted needs a GitHub token (pull requests: write).");
|
|
7263
|
+
const pull = await gh.getPullRequest(n);
|
|
7264
|
+
const origin = parseFigmaOrigin(pull.body);
|
|
7265
|
+
if (!origin || origin.route !== "claude-draft") throw new GitHubError(422, `#${n} is not a Claude draft branch from Figma.`);
|
|
7266
|
+
if (pull.state !== "open") throw new GitHubError(422, `#${n} is ${pull.state}: only an open branch is marked drafted.`);
|
|
7267
|
+
const files = (await gh.listPullRequestFiles(n)).map((f) => f.filename).filter((f) => !f.startsWith(`${FIGMA_SPEC_DIR}/`));
|
|
7268
|
+
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.`);
|
|
7269
|
+
await gh.updatePullRequest(n, { body: withFigmaOrigin(pull.body, { ...origin, status: "drafted" }) });
|
|
7270
|
+
await gh.removeLabel(n, WAITING_FOR_CLAUDE_LABEL).catch(() => void 0);
|
|
7271
|
+
await gh.addLabels(n, [DRAFTED_BY_CLAUDE_LABEL]);
|
|
7272
|
+
const labels = [...pull.labels.filter((l) => l !== WAITING_FOR_CLAUDE_LABEL), DRAFTED_BY_CLAUDE_LABEL];
|
|
7273
|
+
return { status: figmaPushStatus({ state: pull.state, labels, route: origin.route }), files };
|
|
7144
7274
|
}
|
|
7145
7275
|
|
|
7146
7276
|
// ../core/src/node/git-history.ts
|
|
@@ -10424,21 +10554,7 @@ async function branchFacts(ctx, gh, n, viewer) {
|
|
|
10424
10554
|
const product = productOfBranch(productsOf(config), { files: files.map((f) => f.filename), labels: pr.labels });
|
|
10425
10555
|
return { config, pr, files, ci, approvals, tier, gate, status, fresh, compare, exploration, product, lifecycle: pullState(pullLike), write: repo ? hasWriteAccess(repo.permissions) : null, repo, solo: who.solo };
|
|
10426
10556
|
}
|
|
10427
|
-
var STATUS_WORDS =
|
|
10428
|
-
merged: "Merged",
|
|
10429
|
-
closed: "Closed",
|
|
10430
|
-
draft: "Draft",
|
|
10431
|
-
"ci-failing": "Blocked by tests",
|
|
10432
|
-
"changes-requested": "Changes requested",
|
|
10433
|
-
"ci-running": "Tests running",
|
|
10434
|
-
"needs-design": "Needs design approval",
|
|
10435
|
-
"needs-tech": "Needs tech approval",
|
|
10436
|
-
"needs-approval": "Needs approval",
|
|
10437
|
-
conflict: "Conflict with main",
|
|
10438
|
-
checking: "Checking",
|
|
10439
|
-
ready: "Ready to merge",
|
|
10440
|
-
unknown: "Status unknown"
|
|
10441
|
-
};
|
|
10557
|
+
var STATUS_WORDS = REVIEW_STATUS_LABEL;
|
|
10442
10558
|
async function branchStatus(ctx, a) {
|
|
10443
10559
|
const n = branchNumber(a.pr);
|
|
10444
10560
|
const gh = await reader(ctx);
|
|
@@ -10454,7 +10570,7 @@ async function branchStatus(ctx, a) {
|
|
|
10454
10570
|
status: { kind: f.status, label: STATUS_WORDS[f.status] },
|
|
10455
10571
|
approvalNeeded: f.tier === 1 ? "One approval" : "Design and tech approval",
|
|
10456
10572
|
approvals: { approvedBy: f.approvals.approvers, changesRequestedBy: f.approvals.changesRequested, design: f.approvals.design, tech: f.approvals.tech, ...f.solo && (f.tier ?? 3) > 1 ? { soloOwner: soloOwnerSentence(f.solo, Boolean(viewer && viewer.toLowerCase() === f.solo)) } : {} },
|
|
10457
|
-
tests: { state: f.ci.state, failing: f.ci.failed.map((x) => x.name), running: f.ci.pending.map((x) => x.name) },
|
|
10573
|
+
tests: { state: f.ci.state, label: CI_STATE_LABEL[f.ci.state], failing: f.ci.failed.map((x) => x.name), running: f.ci.pending.map((x) => x.name) },
|
|
10458
10574
|
main: { state: f.fresh.state, behindBy: f.fresh.behindBy },
|
|
10459
10575
|
merge: {
|
|
10460
10576
|
allowed: f.gate.allowed,
|
|
@@ -10502,6 +10618,7 @@ async function whatNeedsMe(ctx, a = {}) {
|
|
|
10502
10618
|
else if (f.status === "conflict") needsYou = "Your branch conflicts with main";
|
|
10503
10619
|
else if (f.status === "ready" && f.gate.canShip) needsYou = "Your branch is ready to merge";
|
|
10504
10620
|
}
|
|
10621
|
+
if (viewer && !needsYou && f.gate.overridable && !reviewersToAsk({ tier: f.tier, approvals: f.approvals, owners: who.lists, author: p.author, viewer }).length) needsYou = "Only the owner override can merge it";
|
|
10505
10622
|
return { pr: p.number, title: p.title, url: p.url, author: p.author, product: f.product.name, status: f.status, statusLabel: STATUS_WORDS[f.status], yours: mine, needsYou };
|
|
10506
10623
|
})
|
|
10507
10624
|
);
|
|
@@ -10673,6 +10790,7 @@ async function listComments(ctx, a) {
|
|
|
10673
10790
|
howToReply: "bridge_comment with replyTo: the thread id."
|
|
10674
10791
|
};
|
|
10675
10792
|
}
|
|
10793
|
+
var appliedLine = (ns) => ns.length ? ` ${ns.length === 1 ? "Suggestion" : "Suggestions"} ${ns.map((x) => `#${x}`).join(", ")} ${ns.length === 1 ? "is" : "are"} marked applied.` : "";
|
|
10676
10794
|
async function mergeBranch(ctx, a) {
|
|
10677
10795
|
const n = branchNumber(a.pr);
|
|
10678
10796
|
const gh = await writer(ctx, "Merging a branch", RIGHTS.merge);
|
|
@@ -10684,7 +10802,8 @@ async function mergeBranch(ctx, a) {
|
|
|
10684
10802
|
if (f.gate.allowed) {
|
|
10685
10803
|
if (a.dryRun) return { pr: n, status: "would-merge", said: "Every rule passes: it can be merged." };
|
|
10686
10804
|
const r = await mergeOnGitHub(gh, { number: n, headSha: f.pr.headSha, method: "squash" });
|
|
10687
|
-
|
|
10805
|
+
const s = await markSuggestionsAppliedOnGitHub(gh, f.pr);
|
|
10806
|
+
return { pr: n, status: "merged", sha: r.sha, said: `Merged. It goes out with the next library release.${appliedLine(s.applied)}`, ...s.warnings.length ? { warnings: s.warnings } : {} };
|
|
10688
10807
|
}
|
|
10689
10808
|
if (!reason) {
|
|
10690
10809
|
const hint = f.gate.overridable ? ` Only approvals are missing and @${viewer} may use the owner override: pass overrideReason (at least ${policy.minReasonLength} characters), it is posted on the branch.` : "";
|
|
@@ -10698,7 +10817,8 @@ async function mergeBranch(ctx, a) {
|
|
|
10698
10817
|
const method = allowedMergeMethods(f.repo)[0] ?? "squash";
|
|
10699
10818
|
try {
|
|
10700
10819
|
const r = await overrideMergeOnGitHub(gh, { number: n, headSha: f.pr.headSha, login: viewer, reason, missing: f.gate.missing, method });
|
|
10701
|
-
|
|
10820
|
+
const s = await markSuggestionsAppliedOnGitHub(gh, f.pr);
|
|
10821
|
+
return { pr: n, status: "merged", override: true, sha: r.sha, reasonUrl: r.commentUrl, said: `Merged as owner override. The reason is on #${n}.${appliedLine(s.applied)}`, ...s.warnings.length ? { warnings: s.warnings } : {} };
|
|
10702
10822
|
} catch (err) {
|
|
10703
10823
|
if (err instanceof OverrideMergeError) throw new Error(err.message);
|
|
10704
10824
|
throw err;
|
|
@@ -11067,11 +11187,33 @@ async function suggestionGovernance(ctx, gh) {
|
|
|
11067
11187
|
return { config, gov: { owners, rights: rights ?? null, ...teams ? { teams } : {}, ...config.governance.soloOwnerCanApproveBoth ? { soloOwnerCanApproveBoth: true } : {} } };
|
|
11068
11188
|
}
|
|
11069
11189
|
var OPEN_STATUSES = ["needs-review", "changes-requested", "approved", "applying", "outdated"];
|
|
11190
|
+
var REVIEW_STATUSES = ["needs-review", "changes-requested", "approved"];
|
|
11191
|
+
var MAX_STALENESS_READS = 30;
|
|
11192
|
+
var mainRef = (ctx, config) => ctx.ref ?? baseBranchOf(config);
|
|
11193
|
+
async function stalenessOf(ctx, gh, d, config, gov) {
|
|
11194
|
+
if (d.issue.state !== "open" || !REVIEW_STATUSES.includes(d.status)) return null;
|
|
11195
|
+
return suggestionStalenessOnGitHub(gh, d, { config, governance: gov, ref: mainRef(ctx, config) }).catch(() => null);
|
|
11196
|
+
}
|
|
11070
11197
|
async function listSuggestionsOp(ctx, a = {}) {
|
|
11071
11198
|
const gh = await reader(ctx);
|
|
11072
11199
|
const status = a.status ?? "all";
|
|
11073
11200
|
const state = a.state ?? (status === "all" ? "open" : OPEN_STATUSES.includes(status) ? "open" : "closed");
|
|
11074
|
-
|
|
11201
|
+
const pulls = await gh.listPullRequests("all").catch(() => []);
|
|
11202
|
+
let rows = await listSuggestionsOnGitHub(gh, { state, perPage: 100, pulls });
|
|
11203
|
+
const open = rows.filter((r) => r.state === "open" && REVIEW_STATUSES.includes(r.status)).slice(0, MAX_STALENESS_READS);
|
|
11204
|
+
if (open.length) {
|
|
11205
|
+
const { config, gov } = await suggestionGovernance(ctx, gh);
|
|
11206
|
+
const outdated = /* @__PURE__ */ new Set();
|
|
11207
|
+
await Promise.all(
|
|
11208
|
+
open.map(async (r) => {
|
|
11209
|
+
const d = await getSuggestionOnGitHub(gh, r.number, gov, { pulls }).catch(() => null);
|
|
11210
|
+
if (!d?.parsed.ok) return;
|
|
11211
|
+
const st = await suggestionStalenessOnGitHub(gh, d, { config, governance: gov, ref: mainRef(ctx, config) }).catch(() => null);
|
|
11212
|
+
if (st?.outdated) outdated.add(r.number);
|
|
11213
|
+
})
|
|
11214
|
+
);
|
|
11215
|
+
rows = rows.map((r) => outdated.has(r.number) ? { ...r, status: stalenessStatus(r.status, { outdated: true }) } : r);
|
|
11216
|
+
}
|
|
11075
11217
|
if (status !== "all") rows = rows.filter((r) => r.status === status);
|
|
11076
11218
|
if (a.kind) rows = rows.filter((r) => r.kind === a.kind);
|
|
11077
11219
|
const total = rows.length;
|
|
@@ -11101,8 +11243,10 @@ var roleText = (m) => m === "approval" ? "one approval" : `${m} approval`;
|
|
|
11101
11243
|
async function getSuggestionOp(ctx, a) {
|
|
11102
11244
|
const n = suggestionNumber(a.number);
|
|
11103
11245
|
const gh = await reader(ctx);
|
|
11104
|
-
const { gov } = await suggestionGovernance(ctx, gh);
|
|
11246
|
+
const { config, gov } = await suggestionGovernance(ctx, gh);
|
|
11105
11247
|
const d = await getSuggestionOnGitHub(gh, n, gov);
|
|
11248
|
+
const stale = d.parsed.ok ? await stalenessOf(ctx, gh, d, config, gov) : null;
|
|
11249
|
+
d.status = stalenessStatus(d.status, stale);
|
|
11106
11250
|
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 };
|
|
11107
11251
|
if (!d.parsed.ok) {
|
|
11108
11252
|
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.`);
|
|
@@ -11143,8 +11287,9 @@ async function getSuggestionOp(ctx, a) {
|
|
|
11143
11287
|
...solo && p.tier.tier > 1 ? { soloOwner: soloOwnerSentence(solo) } : {}
|
|
11144
11288
|
},
|
|
11145
11289
|
applied: d.applied,
|
|
11290
|
+
...stale?.outdated ? { outdated: { mainSha: stale.mainSha, conflicts: stale.conflicts.map((c) => ({ path: c.path, detail: c.detail })) } } : {},
|
|
11146
11291
|
...p.componentSpec ? { brief: p.componentSpec.brief } : {},
|
|
11147
|
-
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" ?
|
|
11292
|
+
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: ${stale?.conflicts[0] ? stale.conflicts[0].detail.replace(/\.$/, "") : "main changed what it changes since it was suggested"}. Ask the author to send it again on today's code.` : `${SUGGESTION_STATUS_LABEL[d.status]}: nothing to do.`
|
|
11148
11293
|
};
|
|
11149
11294
|
}
|
|
11150
11295
|
async function reviewSuggestionOp(ctx, a) {
|
|
@@ -11187,12 +11332,7 @@ async function reviewSuggestionOp(ctx, a) {
|
|
|
11187
11332
|
};
|
|
11188
11333
|
}
|
|
11189
11334
|
function githubWorkspace(gh, ref) {
|
|
11190
|
-
|
|
11191
|
-
return {
|
|
11192
|
-
read: async (path) => await gh.getFile(path, ref) ?? null,
|
|
11193
|
-
readAt: async (sha, path) => await gh.readFile(path, sha).catch(() => void 0) ?? null,
|
|
11194
|
-
list: () => files ??= gh.listFiles(ref)
|
|
11195
|
-
};
|
|
11335
|
+
return githubSuggestionWorkspace(gh, ref);
|
|
11196
11336
|
}
|
|
11197
11337
|
var MAX_DIFF = 12e3;
|
|
11198
11338
|
var cut = (s) => s && s.length > MAX_DIFF ? `${s.slice(0, MAX_DIFF)}
|
|
@@ -11228,6 +11368,14 @@ async function applySuggestionOp(ctx, a) {
|
|
|
11228
11368
|
return { number: n, status: "refused", outcome: "refused", exitCode: 1, message: `Not applied: ${d.parsed.message}` };
|
|
11229
11369
|
}
|
|
11230
11370
|
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.` };
|
|
11371
|
+
if (d.status === "applied") {
|
|
11372
|
+
const warnings2 = [];
|
|
11373
|
+
if (ctx.canWriteRemote) {
|
|
11374
|
+
const w = await syncSuggestionStateLabel(gh, n, d.issue.labels, "applied").catch((e) => `The status label on #${n} could not be updated (${e.message}).`);
|
|
11375
|
+
if (w) warnings2.push(w);
|
|
11376
|
+
}
|
|
11377
|
+
return { number: n, status: "already-applied", outcome: "already-applied", exitCode: 0, message: `#${n} is already applied and closed: nothing to write.`, ...warnings2.length ? { warnings: warnings2 } : {} };
|
|
11378
|
+
}
|
|
11231
11379
|
const parsed = d.parsed;
|
|
11232
11380
|
const dryRun = a.dryRun || a.mode === "dry-run";
|
|
11233
11381
|
const mode = a.mode && a.mode !== "dry-run" ? a.mode : "branch";
|
|
@@ -11297,15 +11445,18 @@ async function linkSuggestionOp(ctx, a) {
|
|
|
11297
11445
|
if (!d.parsed.ok) throw new Error(`#${n} cannot be linked: ${d.parsed.message}`);
|
|
11298
11446
|
const warnings = [];
|
|
11299
11447
|
let branch = a.branch;
|
|
11448
|
+
let merged = false;
|
|
11300
11449
|
if (a.pr) {
|
|
11301
11450
|
const pr = await gh.getPullRequest(a.pr);
|
|
11302
11451
|
branch ??= pr.headRef;
|
|
11303
|
-
|
|
11452
|
+
merged = pr.state === "merged";
|
|
11453
|
+
if (!merged && !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.`);
|
|
11304
11454
|
}
|
|
11305
11455
|
const viewer = await viewerOf(ctx, gh);
|
|
11306
|
-
const r = await linkSuggestionPrOnGitHub(gh, n, { pr: a.pr, branch, commit: a.commit, by: viewer ?? void 0, payloadSha: d.parsed.sha, forced: a.forced });
|
|
11456
|
+
const r = await linkSuggestionPrOnGitHub(gh, n, { pr: a.pr, branch, commit: a.commit, by: viewer ?? void 0, payloadSha: d.parsed.sha, forced: a.forced, merged });
|
|
11307
11457
|
warnings.push(...r.warnings);
|
|
11308
|
-
|
|
11458
|
+
const status = r.status;
|
|
11459
|
+
return { number: n, status, said: SUGGESTION_STATUS_LABEL[status], ...a.pr ? { pr: a.pr } : {}, ...branch ? { branch } : {}, ...warnings.length ? { warnings } : {}, next: status === "applied" ? "Applied: nothing left to do." : "It closes when the pull request merges." };
|
|
11309
11460
|
}
|
|
11310
11461
|
async function createSuggestionOp(ctx, a) {
|
|
11311
11462
|
const { config } = await ctx.config();
|
|
@@ -11935,7 +12086,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
11935
12086
|
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.");
|
|
11936
12087
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
11937
12088
|
const out = a.out ? resolve3(ctx.root, a.out) : join5(ctx.root, "visual-impact");
|
|
11938
|
-
const runImpact = run2 ?? (await import("./src-
|
|
12089
|
+
const runImpact = run2 ?? (await import("./src-DRJFSLRV.js")).runImpact;
|
|
11939
12090
|
const r = await runImpact({
|
|
11940
12091
|
root: ctx.root,
|
|
11941
12092
|
base: a.base ?? "main",
|
|
@@ -13003,7 +13154,7 @@ async function pushFigmaComponentOp(ctx, a) {
|
|
|
13003
13154
|
}
|
|
13004
13155
|
|
|
13005
13156
|
// src/server.ts
|
|
13006
|
-
var VERSION = true ? "0.8.
|
|
13157
|
+
var VERSION = true ? "0.8.4" : "dev";
|
|
13007
13158
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
13008
13159
|
var fail2 = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
13009
13160
|
var run = (fn) => async (a) => {
|
|
@@ -13847,7 +13998,7 @@ function createServer(ctx) {
|
|
|
13847
13998
|
"bridge_list_suggestions",
|
|
13848
13999
|
{
|
|
13849
14000
|
title: "List suggestions",
|
|
13850
|
-
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.",
|
|
14001
|
+
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, except that an open one whose change no longer applies to main reads outdated; bridge_get_suggestion counts the approvals.",
|
|
13851
14002
|
inputSchema: {
|
|
13852
14003
|
status: z.enum(["needs-review", "changes-requested", "approved", "applying", "applied", "declined", "outdated", "all"]).optional().describe("Default all open ones."),
|
|
13853
14004
|
state: z.enum(["open", "closed", "all"]).optional().describe("Issue state. Default: open, or closed for applied and declined."),
|