@bridge-ds/mcp 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -2
- package/dist/audit.d.ts +15 -0
- package/dist/brand.d.ts +263 -0
- package/dist/chunk-GABRGCKY.js +7921 -0
- package/dist/chunk-X455XY4E.js +1718 -0
- package/dist/components.d.ts +55 -0
- package/dist/config.d.ts +13 -0
- package/dist/context.d.ts +42 -0
- package/dist/core-adapter.d.ts +48 -0
- package/dist/drafts.d.ts +44 -0
- package/dist/explorations.d.ts +241 -0
- package/dist/history.d.ts +431 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/lib.d.ts +20 -0
- package/dist/lib.js +60 -0
- package/dist/markdown.d.ts +16 -0
- package/dist/ops.d.ts +739 -0
- package/dist/requests.d.ts +89 -0
- package/dist/server.d.ts +5 -0
- package/dist/source.d.ts +39 -0
- package/dist/src-XSBCZV4W.js +1125 -0
- package/package.json +65 -4
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../core/src/previews.ts
|
|
8
|
+
var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
|
|
9
|
+
function branchSlug(branch) {
|
|
10
|
+
return branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
11
|
+
}
|
|
12
|
+
function previewUrl(template, ctx) {
|
|
13
|
+
if (!template) return null;
|
|
14
|
+
const values = {
|
|
15
|
+
branch: branchSlug(ctx.branch),
|
|
16
|
+
rawBranch: encodeURIComponent(ctx.branch),
|
|
17
|
+
pr: ctx.pr === void 0 ? void 0 : String(ctx.pr),
|
|
18
|
+
sha: ctx.sha,
|
|
19
|
+
sha7: ctx.sha?.slice(0, 7)
|
|
20
|
+
};
|
|
21
|
+
let missing = false;
|
|
22
|
+
const filled = template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
23
|
+
const v = values[key];
|
|
24
|
+
if (!v) missing = true;
|
|
25
|
+
return v ?? "";
|
|
26
|
+
});
|
|
27
|
+
if (missing) return null;
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(filled);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
35
|
+
if (url.hostname.split(".").some((label) => label.length > 63)) return null;
|
|
36
|
+
return url.href;
|
|
37
|
+
}
|
|
38
|
+
function previewLinks(apps, ctx) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const app of Object.keys(PREVIEW_APP_LABELS)) {
|
|
41
|
+
const url = previewUrl(apps[app], ctx);
|
|
42
|
+
if (url) out.push({ app, label: PREVIEW_APP_LABELS[app], url });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
var PREVIEW_COMMENT_MARKER = "<!-- bridge-branch-preview -->";
|
|
47
|
+
function installCommandFromPreviewComment(body) {
|
|
48
|
+
if (!body || !body.trimStart().startsWith(PREVIEW_COMMENT_MARKER)) return null;
|
|
49
|
+
const m = /```(?:sh|bash|shell)[^\n]*\n([\s\S]*?)```/.exec(body);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const command = m[1].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).join(" ").replace(/\s*\\\s+/g, " ").replace(/\s+/g, " ").trim();
|
|
52
|
+
return command || null;
|
|
53
|
+
}
|
|
54
|
+
function readPreviewComment(body) {
|
|
55
|
+
if (!body || !body.trimStart().startsWith(PREVIEW_COMMENT_MARKER)) return null;
|
|
56
|
+
const command = installCommandFromPreviewComment(body);
|
|
57
|
+
const sha7 = /Snapshot of `([0-9a-f]{7,40})`/.exec(body)?.[1]?.slice(0, 7) ?? null;
|
|
58
|
+
const source = !command ? "none" : command.includes("https://pkg.pr.new/") ? "pkg.pr.new" : /\/releases\/download\/preview-pr-\d+\//.test(command) ? "release" : "other";
|
|
59
|
+
return { command, sha7, source };
|
|
60
|
+
}
|
|
61
|
+
function findPreviewComment(comments) {
|
|
62
|
+
return [...comments].reverse().find((c) => !!c.body && c.body.trimStart().startsWith(PREVIEW_COMMENT_MARKER));
|
|
63
|
+
}
|
|
64
|
+
var httpsUrl = (raw) => {
|
|
65
|
+
if (!raw || typeof raw !== "string") return null;
|
|
66
|
+
try {
|
|
67
|
+
const u = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`);
|
|
68
|
+
return u.protocol === "https:" || u.protocol === "http:" ? u.href : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var decodeBase64 = (b64) => {
|
|
74
|
+
const bin = atob(b64.replace(/\s/g, ""));
|
|
75
|
+
const bytes = new Uint8Array(bin.length);
|
|
76
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
77
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
78
|
+
};
|
|
79
|
+
var isVercelComment = (body) => !!body && /^\[vc\]: #[^:\s]*:[A-Za-z0-9+/=]+/m.test(body);
|
|
80
|
+
function parseVercelComment(body) {
|
|
81
|
+
if (!body) return null;
|
|
82
|
+
const m = /^\[vc\]: #[^:\s]*:([A-Za-z0-9+/=]+)/m.exec(body);
|
|
83
|
+
if (!m) return null;
|
|
84
|
+
let data;
|
|
85
|
+
try {
|
|
86
|
+
data = JSON.parse(decodeBase64(m[1]));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!data || !Array.isArray(data.projects)) return null;
|
|
91
|
+
return data.projects.filter((p) => p && typeof p.name === "string").map((p) => ({ name: p.name, previewUrl: httpsUrl(typeof p.previewUrl === "string" ? p.previewUrl : null), status: typeof p.nextCommitStatus === "string" ? p.nextCommitStatus : null }));
|
|
92
|
+
}
|
|
93
|
+
function findVercelComment(comments) {
|
|
94
|
+
return [...comments].reverse().find((c) => isVercelComment(c.body));
|
|
95
|
+
}
|
|
96
|
+
function templateHostPrefix(template) {
|
|
97
|
+
if (!template) return null;
|
|
98
|
+
const host = /^https?:\/\/([^/?#]+)/i.exec(template)?.[1];
|
|
99
|
+
if (!host) return null;
|
|
100
|
+
return host.split(".")[0].split("{")[0].toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function appForProject(project, apps) {
|
|
103
|
+
const name = project.trim().toLowerCase();
|
|
104
|
+
if (!name) return null;
|
|
105
|
+
const keys = Object.keys(PREVIEW_APP_LABELS);
|
|
106
|
+
const prefixes = keys.map((app) => [app, templateHostPrefix(apps[app])]);
|
|
107
|
+
const exact = prefixes.find(([, p]) => p === `${name}-git-` || p === name);
|
|
108
|
+
if (exact) return exact[0];
|
|
109
|
+
const loose = prefixes.filter(([, p]) => p !== null && p.startsWith(`${name}-`));
|
|
110
|
+
return loose.length === 1 ? loose[0][0] : null;
|
|
111
|
+
}
|
|
112
|
+
function projectOfLabel(label) {
|
|
113
|
+
const m = /^\s*(?:preview|vercel)\s*[\u2013\u2014-]\s*(.+?)\s*$/i.exec(label);
|
|
114
|
+
return m ? m[1] : null;
|
|
115
|
+
}
|
|
116
|
+
var STATE_OF = {
|
|
117
|
+
success: "ready",
|
|
118
|
+
inactive: "ready",
|
|
119
|
+
deployed: "ready",
|
|
120
|
+
ready: "ready",
|
|
121
|
+
ignored: "ready",
|
|
122
|
+
// Vercel skipped the build (nothing changed for this app): the branch alias serves the last one.
|
|
123
|
+
pending: "building",
|
|
124
|
+
queued: "building",
|
|
125
|
+
in_progress: "building",
|
|
126
|
+
building: "building",
|
|
127
|
+
initializing: "building",
|
|
128
|
+
failure: "failed",
|
|
129
|
+
error: "failed",
|
|
130
|
+
failed: "failed",
|
|
131
|
+
canceled: "failed"
|
|
132
|
+
};
|
|
133
|
+
var stateOf = (s) => STATE_OF[(s ?? "").toLowerCase()] ?? "unknown";
|
|
134
|
+
var isInspectorUrl = (u) => /^https:\/\/(?:www\.)?vercel\.com\//i.test(u);
|
|
135
|
+
function resolveBranchPreviews(apps, ctx, sources = {}) {
|
|
136
|
+
const seen = /* @__PURE__ */ new Map();
|
|
137
|
+
const at = (app) => seen.get(app) ?? (seen.set(app, {}), seen.get(app));
|
|
138
|
+
const offer = (app, url, urlSource, state, stateRank) => {
|
|
139
|
+
if (!app) return;
|
|
140
|
+
const s = at(app);
|
|
141
|
+
if (url && !s.url) Object.assign(s, { url, urlSource });
|
|
142
|
+
if (state !== "unknown" && (s.stateRank === void 0 || stateRank < s.stateRank)) Object.assign(s, { state, stateRank });
|
|
143
|
+
};
|
|
144
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
145
|
+
for (const st of sources.statuses ?? []) {
|
|
146
|
+
if (contexts.has(st.context)) continue;
|
|
147
|
+
contexts.add(st.context);
|
|
148
|
+
const project = projectOfLabel(st.context);
|
|
149
|
+
if (!project) continue;
|
|
150
|
+
const url = httpsUrl(st.target_url);
|
|
151
|
+
offer(appForProject(project, apps), url && !isInspectorUrl(url) ? url : null, "status", stateOf(st.state), 0);
|
|
152
|
+
}
|
|
153
|
+
for (const d of sources.deployments ?? []) {
|
|
154
|
+
const project = projectOfLabel(d.environment);
|
|
155
|
+
const app = project ? appForProject(project, apps) : null;
|
|
156
|
+
if (!app) continue;
|
|
157
|
+
const url = httpsUrl(d.environmentUrl);
|
|
158
|
+
const s = at(app);
|
|
159
|
+
if (url && s.urlSource !== "deployment") Object.assign(s, { url, urlSource: "deployment" });
|
|
160
|
+
const state = stateOf(d.state);
|
|
161
|
+
if (state !== "unknown" && (s.stateRank === void 0 || 1 < s.stateRank)) Object.assign(s, { state, stateRank: 1 });
|
|
162
|
+
}
|
|
163
|
+
for (const p of parseVercelComment(findVercelComment(sources.comments ?? [])?.body) ?? []) offer(appForProject(p.name, apps), p.previewUrl, "vercel-comment", stateOf(p.status), 2);
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const app of Object.keys(PREVIEW_APP_LABELS)) {
|
|
166
|
+
if (!apps[app]) continue;
|
|
167
|
+
const s = seen.get(app) ?? {};
|
|
168
|
+
const url = s.url ?? previewUrl(apps[app], ctx);
|
|
169
|
+
const source = s.url ? s.urlSource : url ? "template" : null;
|
|
170
|
+
out.push({ app, label: PREVIEW_APP_LABELS[app], url, state: s.state ?? "unknown", source });
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
var previewsSettled = (previews) => previews.every((p) => !!p.url && (p.state === "ready" || p.state === "failed"));
|
|
175
|
+
|
|
176
|
+
// ../core/src/teams.ts
|
|
177
|
+
var LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
178
|
+
var SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
179
|
+
function parseOwnerEntry(raw) {
|
|
180
|
+
if (typeof raw !== "string") return null;
|
|
181
|
+
const s = raw.trim().replace(/^@/, "");
|
|
182
|
+
const slash = s.indexOf("/");
|
|
183
|
+
if (slash < 0) return LOGIN.test(s) ? { kind: "person", login: s } : null;
|
|
184
|
+
const org = s.slice(0, slash);
|
|
185
|
+
const slug = s.slice(slash + 1);
|
|
186
|
+
return LOGIN.test(org) && SLUG.test(slug) ? { kind: "team", org, slug } : null;
|
|
187
|
+
}
|
|
188
|
+
var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
|
|
189
|
+
function ownerEntryProblems(raw) {
|
|
190
|
+
if (raw === void 0) return [];
|
|
191
|
+
const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
192
|
+
if (!isObj2(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
195
|
+
if (k !== "design" && k !== "tech") {
|
|
196
|
+
out.push(`"governance.owners.${k}" is not an owner group (design, tech)`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!Array.isArray(v)) {
|
|
200
|
+
out.push(`"governance.owners.${k}" must be a list of people or teams`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
v.forEach((x, i) => {
|
|
204
|
+
if (typeof x !== "string" || !parseOwnerEntry(x))
|
|
205
|
+
out.push(`"governance.owners.${k}[${i}]" (${JSON.stringify(x)}) is not a GitHub login or team. Write a person as "peterbono" or "@peterbono", a team as "@org/team-slug".`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function teamUnreadableMessage(team, reason) {
|
|
211
|
+
const name = `@${team.org}/${team.slug}`;
|
|
212
|
+
switch (reason) {
|
|
213
|
+
case "personal-account":
|
|
214
|
+
return `${name} cannot be used: ${team.org} is a personal GitHub account, and teams only exist in organizations. List people instead, or move the repository to an organization.`;
|
|
215
|
+
case "missing-permission":
|
|
216
|
+
return `The members of ${name} cannot be read: the GitHub token needs read:org (classic token) or Members: Read on the ${team.org} organization (fine-grained token).`;
|
|
217
|
+
case "no-token":
|
|
218
|
+
return `The members of ${name} cannot be read without a working GitHub token. Connect GitHub with a token that has read:org.`;
|
|
219
|
+
case "not-found":
|
|
220
|
+
return `${name} was not found. Check the team slug on GitHub, or give the token read:org so it can see the team.`;
|
|
221
|
+
case "unreachable":
|
|
222
|
+
return `The members of ${name} cannot be read right now: GitHub did not answer. Try again in a moment.`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function teamsIn(owners) {
|
|
226
|
+
const all = Array.isArray(owners) ? owners : [...owners.design ?? [], ...owners.tech ?? []];
|
|
227
|
+
const out = /* @__PURE__ */ new Map();
|
|
228
|
+
for (const raw of all) {
|
|
229
|
+
const e = parseOwnerEntry(raw);
|
|
230
|
+
if (e?.kind === "team") out.set(teamKey(e.org, e.slug), { key: teamKey(e.org, e.slug), org: e.org, slug: e.slug });
|
|
231
|
+
}
|
|
232
|
+
return [...out.values()];
|
|
233
|
+
}
|
|
234
|
+
var norm = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
235
|
+
function expandEntries(entries, teams) {
|
|
236
|
+
const out = [];
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
const add = (x) => {
|
|
239
|
+
const k = norm(x);
|
|
240
|
+
if (k && !seen.has(k)) {
|
|
241
|
+
seen.add(k);
|
|
242
|
+
out.push(x.replace(/^@(?=[^/]+$)/, ""));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
for (const raw of entries) {
|
|
246
|
+
const e = parseOwnerEntry(raw);
|
|
247
|
+
if (!e) continue;
|
|
248
|
+
if (e.kind === "person") add(e.login);
|
|
249
|
+
else {
|
|
250
|
+
const state = teams?.[teamKey(e.org, e.slug)];
|
|
251
|
+
if (state?.status === "ok") state.members.forEach(add);
|
|
252
|
+
else add(teamKey(e.org, e.slug));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
function expandOwners(owners, teams) {
|
|
258
|
+
return { design: expandEntries(owners.design, teams), tech: expandEntries(owners.tech, teams) };
|
|
259
|
+
}
|
|
260
|
+
var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
|
|
261
|
+
function entryLabel(raw, teams) {
|
|
262
|
+
const e = parseOwnerEntry(raw);
|
|
263
|
+
if (!e) return raw;
|
|
264
|
+
if (e.kind === "person") return `@${e.login}`;
|
|
265
|
+
const state = teams?.[teamKey(e.org, e.slug)];
|
|
266
|
+
const name = `@${e.org}/${e.slug}`;
|
|
267
|
+
return state?.status === "ok" ? `${name} (${people(state.members.length)})` : name;
|
|
268
|
+
}
|
|
269
|
+
function unreadableTeams(teams) {
|
|
270
|
+
return Object.values(teams ?? {}).filter((t) => t.status === "unreadable");
|
|
271
|
+
}
|
|
272
|
+
var TeamDirectory = class {
|
|
273
|
+
constructor(reader, opts = {}) {
|
|
274
|
+
this.reader = reader;
|
|
275
|
+
this.ttl = opts.ttlMs ?? 5 * 6e4;
|
|
276
|
+
this.now = opts.now ?? Date.now;
|
|
277
|
+
}
|
|
278
|
+
reader;
|
|
279
|
+
teams = /* @__PURE__ */ new Map();
|
|
280
|
+
accounts = /* @__PURE__ */ new Map();
|
|
281
|
+
memberships = /* @__PURE__ */ new Map();
|
|
282
|
+
ttl;
|
|
283
|
+
now;
|
|
284
|
+
accountType(org) {
|
|
285
|
+
const k = org.toLowerCase();
|
|
286
|
+
let p = this.accounts.get(k);
|
|
287
|
+
if (!p) {
|
|
288
|
+
p = this.reader.getAccountType(org).catch(() => null);
|
|
289
|
+
this.accounts.set(k, p);
|
|
290
|
+
}
|
|
291
|
+
return p;
|
|
292
|
+
}
|
|
293
|
+
/** The reason a read of `org` failed, as a state. A 404 on a personal account says so; a 403 is a missing permission. */
|
|
294
|
+
async failure(ref, err) {
|
|
295
|
+
const e = err ?? {};
|
|
296
|
+
let reason;
|
|
297
|
+
if (!this.reader.hasToken) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "no-token";
|
|
298
|
+
else if (e.status === 401) reason = "no-token";
|
|
299
|
+
else if (e.status === 403 || e.auth === "missing-permission") reason = "missing-permission";
|
|
300
|
+
else if (e.status === 404) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "not-found";
|
|
301
|
+
else reason = "unreachable";
|
|
302
|
+
return { ...ref, status: "unreadable", reason, message: teamUnreadableMessage(ref, reason) };
|
|
303
|
+
}
|
|
304
|
+
/** Members of one team, or why they cannot be read. Cached. */
|
|
305
|
+
team(org, slug) {
|
|
306
|
+
const ref = { key: teamKey(org, slug), org, slug };
|
|
307
|
+
const hit = this.teams.get(ref.key);
|
|
308
|
+
if (hit && this.now() - hit.at < this.ttl) return hit.value;
|
|
309
|
+
const value = (async () => {
|
|
310
|
+
if (!this.reader.hasToken) return this.failure(ref, void 0);
|
|
311
|
+
try {
|
|
312
|
+
const members = await this.reader.listTeamMembers(org, slug);
|
|
313
|
+
return { ...ref, status: "ok", members };
|
|
314
|
+
} catch (err) {
|
|
315
|
+
return this.failure(ref, err);
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
318
|
+
this.teams.set(ref.key, { at: this.now(), value });
|
|
319
|
+
void value.then((s) => {
|
|
320
|
+
if (s.status === "unreadable" && s.reason === "unreachable") this.teams.delete(ref.key);
|
|
321
|
+
});
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
/** Every team named in the owner lists, read in parallel. Empty object when no team is named (no request). */
|
|
325
|
+
async resolve(owners) {
|
|
326
|
+
const refs = teamsIn(owners);
|
|
327
|
+
const states = await Promise.all(refs.map((r) => this.team(r.org, r.slug)));
|
|
328
|
+
return Object.fromEntries(states.map((s) => [s.key, s]));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Whether `login` is an active member of one team: from the cached member list when there is one, else one
|
|
332
|
+
* membership read (GET /orgs/{org}/teams/{slug}/memberships/{user}). An unreadable team returns its state.
|
|
333
|
+
*/
|
|
334
|
+
async isMember(org, slug, login) {
|
|
335
|
+
const ref = { key: teamKey(org, slug), org, slug };
|
|
336
|
+
const cached = this.teams.get(ref.key);
|
|
337
|
+
if (cached && this.now() - cached.at < this.ttl) {
|
|
338
|
+
const s = await cached.value;
|
|
339
|
+
if (s.status === "ok") return s.members.some((m) => norm(m) === norm(login));
|
|
340
|
+
}
|
|
341
|
+
const k = `${ref.key}#${norm(login)}`;
|
|
342
|
+
const hit = this.memberships.get(k);
|
|
343
|
+
if (hit && this.now() - hit.at < this.ttl) return hit.value;
|
|
344
|
+
const value = (async () => {
|
|
345
|
+
if (!this.reader.hasToken) return this.failure(ref, void 0);
|
|
346
|
+
try {
|
|
347
|
+
return await this.reader.getTeamMembership(org, slug, login) === "active";
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return this.failure(ref, err);
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
this.memberships.set(k, { at: this.now(), value });
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Whether `login` holds a right given to `entries` (people and teams): a person entry matches directly, a team
|
|
357
|
+
* through its membership. Teams are only read when no person entry matches.
|
|
358
|
+
*/
|
|
359
|
+
async memberOfAny(entries, login) {
|
|
360
|
+
const unreadable = [];
|
|
361
|
+
const parsed = entries.map(parseOwnerEntry).filter((e) => Boolean(e));
|
|
362
|
+
if (parsed.some((e) => e.kind === "person" && norm(e.login) === norm(login))) return { member: true, unreadable };
|
|
363
|
+
for (const e of parsed) {
|
|
364
|
+
if (e.kind !== "team") continue;
|
|
365
|
+
const r = await this.isMember(e.org, e.slug, login);
|
|
366
|
+
if (r === true) return { member: true, unreadable };
|
|
367
|
+
if (typeof r === "object" && r.status === "unreadable") unreadable.push(r);
|
|
368
|
+
}
|
|
369
|
+
return { member: false, unreadable };
|
|
370
|
+
}
|
|
371
|
+
/** The organization's teams for a picker, or why they cannot be listed (a personal account has none). */
|
|
372
|
+
async orgTeams(org) {
|
|
373
|
+
if (await this.accountType(org) === "User") return { status: "unreadable", reason: "personal-account" };
|
|
374
|
+
if (!this.reader.hasToken) return { status: "unreadable", reason: "no-token" };
|
|
375
|
+
try {
|
|
376
|
+
return { status: "ok", teams: await this.reader.listOrgTeams(org) };
|
|
377
|
+
} catch (err) {
|
|
378
|
+
const s = await this.failure({ key: teamKey(org, "*"), org, slug: "*" }, err);
|
|
379
|
+
return { status: "unreadable", reason: s.status === "unreadable" ? s.reason : "unreachable" };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** Drops every cached read (after the token changes). */
|
|
383
|
+
clear() {
|
|
384
|
+
this.teams.clear();
|
|
385
|
+
this.accounts.clear();
|
|
386
|
+
this.memberships.clear();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ../core/src/rights.ts
|
|
391
|
+
var OWNER_GROUPS = ["design", "tech"];
|
|
392
|
+
var RIGHTS = ["approveDesign", "approveTech", "ship"];
|
|
393
|
+
var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
|
|
394
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
395
|
+
function normalizeRights(input) {
|
|
396
|
+
const pick = (r) => {
|
|
397
|
+
const v = input?.[r];
|
|
398
|
+
if (!Array.isArray(v)) return [...DEFAULT_RIGHTS[r]];
|
|
399
|
+
return OWNER_GROUPS.filter((g) => v.includes(g));
|
|
400
|
+
};
|
|
401
|
+
return { approveDesign: pick("approveDesign"), approveTech: pick("approveTech"), ship: pick("ship") };
|
|
402
|
+
}
|
|
403
|
+
function rightsProblems(raw) {
|
|
404
|
+
if (raw === void 0) return [];
|
|
405
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"governance.rights" must be { "approveDesign": [groups], "approveTech": [groups], "ship": [groups] }'];
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
408
|
+
if (!RIGHTS.includes(k)) out.push(`"governance.rights.${k}" is not a right (${RIGHTS.join(", ")})`);
|
|
409
|
+
else if (!Array.isArray(v) || v.length === 0 || !v.every((g) => OWNER_GROUPS.includes(g)))
|
|
410
|
+
out.push(`"governance.rights.${k}" must list one or more of "design", "tech"`);
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
function holdersOf(right, owners, rights) {
|
|
415
|
+
const groups = (rights ?? DEFAULT_RIGHTS)[right];
|
|
416
|
+
return uniq(groups.flatMap((g) => owners[g] ?? []));
|
|
417
|
+
}
|
|
418
|
+
function approverEntries(owners, rights) {
|
|
419
|
+
if (!rights) return { design: [...owners.design], tech: [...owners.tech] };
|
|
420
|
+
return { design: holdersOf("approveDesign", owners, rights), tech: holdersOf("approveTech", owners, rights) };
|
|
421
|
+
}
|
|
422
|
+
function shipPolicy(owners, rights, teams) {
|
|
423
|
+
if (!rights) return null;
|
|
424
|
+
const entries = holdersOf("ship", owners, rights);
|
|
425
|
+
if (!entries.length) return null;
|
|
426
|
+
const logins = expandEntries(entries, teams);
|
|
427
|
+
return { logins, groups: [...rights.ship], entries, ...teams ? { teams } : {} };
|
|
428
|
+
}
|
|
429
|
+
function groupsPhrase(groups) {
|
|
430
|
+
const g = OWNER_GROUPS.filter((x) => groups.includes(x));
|
|
431
|
+
if (g.length === 1) return `the ${g[0]} owner`;
|
|
432
|
+
return `a ${g.join(" or ")} owner`;
|
|
433
|
+
}
|
|
434
|
+
function whoCanShipSentence(policy) {
|
|
435
|
+
const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
|
|
436
|
+
return `Only ${groupsPhrase(policy.groups)} can ship this${names ? ` (${names})` : ""}.`;
|
|
437
|
+
}
|
|
438
|
+
function visualImpactProblems(raw) {
|
|
439
|
+
if (raw === void 0) return [];
|
|
440
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"visualImpact" must be { "mode": "auto" | "on-demand" }'];
|
|
441
|
+
const out = [];
|
|
442
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
443
|
+
if (k !== "mode") out.push(`"visualImpact.${k}" is not a setting (mode)`);
|
|
444
|
+
else if (v !== "auto" && v !== "on-demand") out.push('"visualImpact.mode" must be "auto" or "on-demand"');
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ../core/src/config.ts
|
|
450
|
+
var CONFIG_FILE = "bridge.config.json";
|
|
451
|
+
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
452
|
+
var DEFAULT_TOKEN_PATHS = {
|
|
453
|
+
root: "tokens",
|
|
454
|
+
primitives: "primitives",
|
|
455
|
+
brands: "brands",
|
|
456
|
+
semantic: "semantic",
|
|
457
|
+
base: "base",
|
|
458
|
+
density: "density",
|
|
459
|
+
components: "components",
|
|
460
|
+
contrastChecks: "contrast.checks.json"
|
|
461
|
+
};
|
|
462
|
+
var DEFAULT_GOVERNANCE = {
|
|
463
|
+
protectedBranches: ["main", "master"],
|
|
464
|
+
branchPrefix: "bridge/",
|
|
465
|
+
labels: ["source: mcp"],
|
|
466
|
+
guardrails: ["AGENTS.md", "CLAUDE.md", "docs/GOVERNANCE.md", ".stylelintrc.json", ".github/", CONFIG_FILE],
|
|
467
|
+
owners: { design: [], tech: [] }
|
|
468
|
+
};
|
|
469
|
+
var FLAT_TOKEN_KEYS = ["primitives", "brands", "semantic", "base", "density", "components", "contrastChecks"];
|
|
470
|
+
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers"];
|
|
471
|
+
var FLAT_MODE_KEYS = ["brands", "themes", "densities", "defaults"];
|
|
472
|
+
var AXIS_OF = { brands: "brand", themes: "theme", densities: "density" };
|
|
473
|
+
var ConfigError = class extends Error {
|
|
474
|
+
constructor(problems) {
|
|
475
|
+
super(`${CONFIG_FILE} is invalid: ${problems.join("; ")}`);
|
|
476
|
+
this.problems = problems;
|
|
477
|
+
this.name = "ConfigError";
|
|
478
|
+
}
|
|
479
|
+
problems;
|
|
480
|
+
};
|
|
481
|
+
var trimSlashes = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
482
|
+
var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
|
|
483
|
+
var isFlatModes = (m) => Object.keys(m).some((k) => FLAT_MODE_KEYS.includes(k)) || Object.keys(m).length === 0;
|
|
484
|
+
function parseRepoInput(repo, branch) {
|
|
485
|
+
if (!repo) return void 0;
|
|
486
|
+
if (typeof repo === "string") {
|
|
487
|
+
const [owner, name] = repo.split("/");
|
|
488
|
+
return { owner, name, defaultBranch: branch ?? "main" };
|
|
489
|
+
}
|
|
490
|
+
return { owner: repo.owner, name: repo.name, defaultBranch: repo.defaultBranch ?? branch ?? "main" };
|
|
491
|
+
}
|
|
492
|
+
function conventionalLayers(p) {
|
|
493
|
+
return [
|
|
494
|
+
{ files: `${p.primitives}/*.tokens.json`, optional: false },
|
|
495
|
+
{ files: `${p.semantic}/${p.base}.tokens.json`, optional: true },
|
|
496
|
+
{ files: `${p.brands}/{brand}.tokens.json`, optional: false },
|
|
497
|
+
{ files: `${p.semantic}/{theme}.tokens.json`, optional: false },
|
|
498
|
+
{ files: `${p.brands}/{brand}.{theme}.tokens.json`, optional: true },
|
|
499
|
+
{ files: `${p.density}/{density}.tokens.json`, optional: false },
|
|
500
|
+
{ files: `${p.components}/*.tokens.json`, optional: false }
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
function defineConfig(input) {
|
|
504
|
+
const t = input.tokens ?? {};
|
|
505
|
+
const root = trimSlashes(t.root ?? DEFAULT_TOKEN_PATHS.root) || ".";
|
|
506
|
+
const tiersIn = t.tiers ? Object.fromEntries(Object.entries(t.tiers).map(([k, v]) => [k, list(v).map(trimSlashes)])) : void 0;
|
|
507
|
+
const firstDir = (tier) => tiersIn?.[tier]?.find((d) => !d.endsWith(".json"));
|
|
508
|
+
const baseFile = tiersIn?.base?.find((d) => d.endsWith(".json"));
|
|
509
|
+
const contrastPath = input.contrast?.path ? trimSlashes(input.contrast.path) : void 0;
|
|
510
|
+
const underRoot = (p) => root === "." ? p : p.startsWith(`${root}/`) ? p.slice(root.length + 1) : void 0;
|
|
511
|
+
const paths = {
|
|
512
|
+
root,
|
|
513
|
+
primitives: trimSlashes(t.primitives ?? firstDir("primitive") ?? DEFAULT_TOKEN_PATHS.primitives),
|
|
514
|
+
brands: trimSlashes(t.brands ?? firstDir("brand") ?? DEFAULT_TOKEN_PATHS.brands),
|
|
515
|
+
semantic: trimSlashes(t.semantic ?? firstDir("semantic") ?? DEFAULT_TOKEN_PATHS.semantic),
|
|
516
|
+
base: trimSlashes(t.base ?? (baseFile ? baseFile.split("/").pop().replace(/(\.tokens)?\.json$/, "") : DEFAULT_TOKEN_PATHS.base)),
|
|
517
|
+
density: trimSlashes(t.density ?? firstDir("density") ?? DEFAULT_TOKEN_PATHS.density),
|
|
518
|
+
components: trimSlashes(t.components ?? firstDir("component") ?? DEFAULT_TOKEN_PATHS.components),
|
|
519
|
+
contrastChecks: trimSlashes(t.contrastChecks ?? (contrastPath ? underRoot(contrastPath) ?? contrastPath : DEFAULT_TOKEN_PATHS.contrastChecks))
|
|
520
|
+
};
|
|
521
|
+
const tiers = tiersIn ?? {
|
|
522
|
+
primitive: [paths.primitives],
|
|
523
|
+
brand: [paths.brands],
|
|
524
|
+
base: [`${paths.semantic}/${paths.base}.tokens.json`],
|
|
525
|
+
semantic: [paths.semantic],
|
|
526
|
+
density: [paths.density],
|
|
527
|
+
component: [paths.components]
|
|
528
|
+
};
|
|
529
|
+
const layers = t.layers?.length ? t.layers.map((l) => typeof l === "string" ? { files: l, optional: false } : { files: l.files, optional: l.optional ?? false }) : conventionalLayers(paths);
|
|
530
|
+
const axes = {};
|
|
531
|
+
const m = input.modes ?? {};
|
|
532
|
+
if (isFlatModes(m)) {
|
|
533
|
+
for (const key of ["brands", "themes", "densities"]) {
|
|
534
|
+
const values = m[key] ?? [];
|
|
535
|
+
if (!values.length) continue;
|
|
536
|
+
const axis = AXIS_OF[key];
|
|
537
|
+
const def = m.defaults?.[axis] ?? values[0];
|
|
538
|
+
axes[axis] = { values, default: def, parity: axis === "theme" };
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
for (const [name, a] of Object.entries(m)) axes[name] = { values: a.values, default: a.default ?? a.values[0], parity: a.parity ?? false };
|
|
542
|
+
}
|
|
543
|
+
const ordered = (axis) => {
|
|
544
|
+
const a = axes[axis];
|
|
545
|
+
return a ? [a.default, ...a.values.filter((v) => v !== a.default)] : [];
|
|
546
|
+
};
|
|
547
|
+
const modes = {
|
|
548
|
+
brands: ordered("brand"),
|
|
549
|
+
themes: ordered("theme"),
|
|
550
|
+
densities: ordered("density"),
|
|
551
|
+
defaults: { brand: axes.brand?.default, theme: axes.theme?.default, density: axes.density?.default }
|
|
552
|
+
};
|
|
553
|
+
const pkg = input.changesets?.package ?? input.changeset?.package;
|
|
554
|
+
const g = input.governance ?? {};
|
|
555
|
+
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
556
|
+
return {
|
|
557
|
+
repo: parseRepoInput(input.repo, input.branch),
|
|
558
|
+
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers },
|
|
559
|
+
axes,
|
|
560
|
+
modes,
|
|
561
|
+
contrast: { path: contrastFile, modes: input.contrast?.modes },
|
|
562
|
+
components: input.components ? {
|
|
563
|
+
dir: trimSlashes(input.components.dir),
|
|
564
|
+
docsSuffix: input.components.docsSuffix ?? ".docs.json",
|
|
565
|
+
styleSuffix: input.components.styleSuffix ?? ".module.css",
|
|
566
|
+
reference: input.components.reference,
|
|
567
|
+
index: input.components.index,
|
|
568
|
+
specs: input.components.specs
|
|
569
|
+
} : void 0,
|
|
570
|
+
docs: { ...input.docs },
|
|
571
|
+
changesets: pkg ? { dir: trimSlashes(input.changesets?.dir ?? ".changeset"), package: pkg } : void 0,
|
|
572
|
+
changeset: pkg ? { package: pkg } : void 0,
|
|
573
|
+
cssVar: { prefix: input.cssVar?.prefix ?? "--", separator: input.cssVar?.separator ?? "-" },
|
|
574
|
+
governance: {
|
|
575
|
+
protectedBranches: g.protectedBranches ?? DEFAULT_GOVERNANCE.protectedBranches,
|
|
576
|
+
branchPrefix: g.branchPrefix ?? DEFAULT_GOVERNANCE.branchPrefix,
|
|
577
|
+
labels: g.labels ?? DEFAULT_GOVERNANCE.labels,
|
|
578
|
+
guardrails: g.guardrails ?? [...DEFAULT_GOVERNANCE.guardrails, contrastFile],
|
|
579
|
+
owners: { design: g.owners?.design ?? [], tech: g.owners?.tech ?? [] },
|
|
580
|
+
...g.override ? {
|
|
581
|
+
override: {
|
|
582
|
+
allowed: g.override.allowed ?? DEFAULT_OVERRIDE.allowed,
|
|
583
|
+
requireReason: g.override.requireReason ?? DEFAULT_OVERRIDE.requireReason,
|
|
584
|
+
minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
|
|
585
|
+
}
|
|
586
|
+
} : {},
|
|
587
|
+
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
588
|
+
},
|
|
589
|
+
...input.audit ? { audit: { ...input.audit } } : {},
|
|
590
|
+
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
591
|
+
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
592
|
+
...input.previews ? {
|
|
593
|
+
previews: {
|
|
594
|
+
packages: input.previews.packages ?? [],
|
|
595
|
+
apps: { ...input.previews.apps },
|
|
596
|
+
...input.previews.production ? { production: { ...input.previews.production } } : {}
|
|
597
|
+
}
|
|
598
|
+
} : {}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
var isStr = (x) => typeof x === "string" && x.trim().length > 0;
|
|
602
|
+
var isStrList = (x) => Array.isArray(x) && x.every(isStr);
|
|
603
|
+
var isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
604
|
+
function loadConfig(source) {
|
|
605
|
+
let data = source;
|
|
606
|
+
if (typeof source === "string") {
|
|
607
|
+
try {
|
|
608
|
+
data = JSON.parse(source);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
throw new ConfigError([`not valid JSON (${err.message})`]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (!isObj(data)) throw new ConfigError(["top level must be an object"]);
|
|
614
|
+
const problems = [];
|
|
615
|
+
const d = data;
|
|
616
|
+
const repo = d.repo;
|
|
617
|
+
if (typeof repo === "string") {
|
|
618
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) problems.push('"repo" must be "owner/name" or { "owner", "name" }');
|
|
619
|
+
} else if (repo !== void 0) {
|
|
620
|
+
if (!isObj(repo)) problems.push('"repo" must be "owner/name" or { "owner", "name" }');
|
|
621
|
+
else {
|
|
622
|
+
if (!isStr(repo.owner)) problems.push('"repo.owner" must be a non-empty string');
|
|
623
|
+
if (!isStr(repo.name)) problems.push('"repo.name" must be a non-empty string');
|
|
624
|
+
if (repo.defaultBranch !== void 0 && !isStr(repo.defaultBranch)) problems.push('"repo.defaultBranch" must be a non-empty string');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (d.branch !== void 0 && !isStr(d.branch)) problems.push('"branch" must be a non-empty string');
|
|
628
|
+
const tokens = d.tokens;
|
|
629
|
+
if (tokens !== void 0) {
|
|
630
|
+
if (!isObj(tokens)) problems.push('"tokens" must be an object');
|
|
631
|
+
else
|
|
632
|
+
for (const [k, v] of Object.entries(tokens)) {
|
|
633
|
+
if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
|
|
634
|
+
if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
|
|
635
|
+
} else if (k === "include" || k === "exclude") {
|
|
636
|
+
if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
|
|
637
|
+
} else if (k === "tiers") {
|
|
638
|
+
if (!isObj(v) || !Object.values(v).every((x) => isStr(x) || isStrList(x))) problems.push('"tokens.tiers" must map tier names to directories');
|
|
639
|
+
} else if (k === "layers") {
|
|
640
|
+
if (!Array.isArray(v) || !v.length || !v.every((l) => isStr(l) || isObj(l) && isStr(l.files) && (l.optional === void 0 || typeof l.optional === "boolean")))
|
|
641
|
+
problems.push('"tokens.layers" must be a non-empty list of { "files", "optional"? }');
|
|
642
|
+
} else if (!CANONICAL_TOKEN_KEYS.includes(k)) problems.push(`"tokens.${k}" is not a known key`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const modes = d.modes;
|
|
646
|
+
let axisNames = [];
|
|
647
|
+
if (modes !== void 0) {
|
|
648
|
+
if (!isObj(modes)) problems.push('"modes" must be an object');
|
|
649
|
+
else if (isFlatModes(modes)) {
|
|
650
|
+
for (const k of ["brands", "themes", "densities"]) {
|
|
651
|
+
if (modes[k] !== void 0 && !isStrList(modes[k])) problems.push(`"modes.${k}" must be a list of names`);
|
|
652
|
+
}
|
|
653
|
+
const defs = modes.defaults;
|
|
654
|
+
const lists = { brand: "brands", theme: "themes", density: "densities" };
|
|
655
|
+
for (const [k, l] of Object.entries(lists)) {
|
|
656
|
+
const v = defs?.[k];
|
|
657
|
+
if (v === void 0) continue;
|
|
658
|
+
if (!isStr(v)) problems.push(`"modes.defaults.${k}" must be a string`);
|
|
659
|
+
else if (isStrList(modes[l]) && !modes[l].includes(v)) problems.push(`"modes.defaults.${k}" ("${v}") is not in "modes.${l}"`);
|
|
660
|
+
}
|
|
661
|
+
axisNames = Object.entries(lists).filter(([, l]) => isStrList(modes[l]) && modes[l].length).map(([k]) => k);
|
|
662
|
+
} else {
|
|
663
|
+
for (const [name, a] of Object.entries(modes)) {
|
|
664
|
+
if (!isObj(a) || !isStrList(a.values) || !a.values.length) {
|
|
665
|
+
problems.push(`"modes.${name}.values" must be a non-empty list of names`);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (a.default !== void 0 && (!isStr(a.default) || !a.values.includes(a.default))) problems.push(`"modes.${name}.default" ("${String(a.default)}") is not in "modes.${name}.values"`);
|
|
669
|
+
if (a.parity !== void 0 && typeof a.parity !== "boolean") problems.push(`"modes.${name}.parity" must be true or false`);
|
|
670
|
+
axisNames.push(name);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (isObj(tokens) && Array.isArray(tokens.layers)) {
|
|
675
|
+
for (const l of tokens.layers) {
|
|
676
|
+
const files = typeof l === "string" ? l : isObj(l) ? String(l.files) : "";
|
|
677
|
+
for (const ph of files.match(/\{([^}]+)\}/g) ?? []) {
|
|
678
|
+
if (!axisNames.includes(ph.slice(1, -1))) problems.push(`layer "${files}" uses ${ph} but "modes.${ph.slice(1, -1)}" is not defined`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const contrast = d.contrast;
|
|
683
|
+
if (contrast !== void 0 && (!isObj(contrast) || !isStr(contrast.path) || contrast.modes !== void 0 && !isStrList(contrast.modes)))
|
|
684
|
+
problems.push('"contrast" must be { "path", "modes"? }');
|
|
685
|
+
const comps = d.components;
|
|
686
|
+
if (comps !== void 0 && (!isObj(comps) || !isStr(comps.dir))) problems.push('"components.dir" must be a non-empty string');
|
|
687
|
+
const cs = d.changeset;
|
|
688
|
+
if (cs !== void 0 && (!isObj(cs) || !isStr(cs.package))) problems.push('"changeset.package" must be a non-empty string');
|
|
689
|
+
const css = d.changesets;
|
|
690
|
+
if (css !== void 0 && (!isObj(css) || !isStr(css.package))) problems.push('"changesets.package" must be a non-empty string');
|
|
691
|
+
const gov = d.governance;
|
|
692
|
+
if (gov !== void 0) {
|
|
693
|
+
if (!isObj(gov)) problems.push('"governance" must be an object');
|
|
694
|
+
else {
|
|
695
|
+
for (const k of ["protectedBranches", "labels", "guardrails"]) if (gov[k] !== void 0 && !(Array.isArray(gov[k]) && gov[k].every((x) => typeof x === "string"))) problems.push(`"governance.${k}" must be a list of strings`);
|
|
696
|
+
if (gov.branchPrefix !== void 0 && !isStr(gov.branchPrefix)) problems.push('"governance.branchPrefix" must be a non-empty string');
|
|
697
|
+
problems.push(...ownerEntryProblems(gov.owners));
|
|
698
|
+
const ov = gov.override;
|
|
699
|
+
if (ov !== void 0) {
|
|
700
|
+
if (!isObj(ov)) problems.push('"governance.override" must be an object');
|
|
701
|
+
else {
|
|
702
|
+
if (ov.allowed !== void 0 && !isStrList(ov.allowed)) problems.push('"governance.override.allowed" must be a list of GitHub logins');
|
|
703
|
+
if (ov.requireReason !== void 0 && typeof ov.requireReason !== "boolean") problems.push('"governance.override.requireReason" must be true or false');
|
|
704
|
+
if (ov.minReasonLength !== void 0 && !(Number.isInteger(ov.minReasonLength) && ov.minReasonLength >= 0)) problems.push('"governance.override.minReasonLength" must be a whole number, 0 or more');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
problems.push(...rightsProblems(gov.rights));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
problems.push(...visualImpactProblems(d.visualImpact));
|
|
711
|
+
const audit = d.audit;
|
|
712
|
+
if (audit !== void 0) {
|
|
713
|
+
if (!isObj(audit)) problems.push('"audit" must be an object');
|
|
714
|
+
else for (const k of ["packages", "foreignLibraries", "nativeElements", "exclude"]) if (audit[k] !== void 0 && !isStrList(audit[k])) problems.push(`"audit.${k}" must be a list of strings`);
|
|
715
|
+
}
|
|
716
|
+
const pv = d.previews;
|
|
717
|
+
if (pv !== void 0) {
|
|
718
|
+
if (!isObj(pv)) problems.push('"previews" must be an object');
|
|
719
|
+
else {
|
|
720
|
+
if (pv.packages !== void 0 && !isStrList(pv.packages)) problems.push('"previews.packages" must be a list of package names');
|
|
721
|
+
for (const field of ["apps", "production"]) {
|
|
722
|
+
const apps = pv[field];
|
|
723
|
+
if (apps === void 0) continue;
|
|
724
|
+
if (!isObj(apps)) {
|
|
725
|
+
problems.push(`"previews.${field}" must be an object`);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
for (const [k, v] of Object.entries(apps)) {
|
|
729
|
+
if (!(k in PREVIEW_APP_LABELS)) problems.push(`"previews.${field}.${k}" is not a known app (${Object.keys(PREVIEW_APP_LABELS).join(", ")})`);
|
|
730
|
+
else if (!isStr(v) || !/^https?:\/\//.test(v)) problems.push(`"previews.${field}.${k}" must be an http(s) URL${field === "apps" ? " template" : ""}`);
|
|
731
|
+
else if (field === "production" && /\{\w+\}/.test(v)) problems.push(`"previews.production.${k}" is a fixed URL: placeholders such as {branch} belong in "previews.apps"`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (problems.length) throw new ConfigError(problems);
|
|
737
|
+
return defineConfig(d);
|
|
738
|
+
}
|
|
739
|
+
var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
740
|
+
var protectedBranchesOf = (config) => [.../* @__PURE__ */ new Set([...config.governance.protectedBranches, ...config.repo ? [config.repo.defaultBranch] : []])];
|
|
741
|
+
var baseBranchOf = (config) => config.repo?.defaultBranch ?? "main";
|
|
742
|
+
function toConfigJson(config, schema = "./packages/core/bridge.config.schema.json") {
|
|
743
|
+
return {
|
|
744
|
+
$schema: schema,
|
|
745
|
+
version: 1,
|
|
746
|
+
...config.repo ? { repo: config.repo } : {},
|
|
747
|
+
tokens: { root: config.tokens.root, include: config.tokens.include, ...config.tokens.exclude.length ? { exclude: config.tokens.exclude } : {}, tiers: config.tokens.tiers, layers: config.tokens.layers.map((l) => l.optional ? l : { files: l.files }) },
|
|
748
|
+
modes: Object.fromEntries(Object.entries(config.axes).map(([k, a]) => [k, a.parity ? a : { values: a.values, default: a.default }])),
|
|
749
|
+
...config.contrast ? { contrast: config.contrast.modes ? config.contrast : { path: config.contrast.path } } : {},
|
|
750
|
+
...config.components ? { components: Object.fromEntries(Object.entries(config.components).filter(([, v]) => v !== void 0)) } : {},
|
|
751
|
+
docs: Object.fromEntries(Object.entries(config.docs).filter(([, v]) => v !== void 0)),
|
|
752
|
+
...config.changesets ? { changesets: config.changesets } : {},
|
|
753
|
+
cssVar: config.cssVar,
|
|
754
|
+
governance: config.governance,
|
|
755
|
+
...config.audit ? { audit: config.audit } : {},
|
|
756
|
+
...config.previews ? { previews: config.previews } : {},
|
|
757
|
+
...config.visualImpact ? { visualImpact: config.visualImpact } : {}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ../core/src/values.ts
|
|
762
|
+
var ALIAS_RE = /^\{([^{}]+)\}$/;
|
|
763
|
+
var isAlias = (v) => typeof v === "string" && ALIAS_RE.test(v);
|
|
764
|
+
var aliasTarget = (v) => ALIAS_RE.exec(v)?.[1] ?? v;
|
|
765
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
766
|
+
var round = (n, digits) => {
|
|
767
|
+
const f = 10 ** digits;
|
|
768
|
+
return Math.round(n * f) / f;
|
|
769
|
+
};
|
|
770
|
+
function hexToRgba(hex) {
|
|
771
|
+
const m = /^#?([0-9a-f]{3,8})$/i.exec(hex.trim());
|
|
772
|
+
if (!m) return void 0;
|
|
773
|
+
let h = m[1];
|
|
774
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
|
|
775
|
+
if (h.length !== 6 && h.length !== 8) return void 0;
|
|
776
|
+
const n = (idx) => parseInt(h.slice(idx, idx + 2), 16) / 255;
|
|
777
|
+
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
|
|
778
|
+
}
|
|
779
|
+
var to255 = (n) => Math.round(clamp01(n) * 255);
|
|
780
|
+
var rgbaToHex = (c) => "#" + [c.r, c.g, c.b].map((x) => to255(x).toString(16).padStart(2, "0")).join("");
|
|
781
|
+
function parseColor(value) {
|
|
782
|
+
if (typeof value === "string") return isAlias(value) ? void 0 : hexToRgba(value);
|
|
783
|
+
if (!value || typeof value !== "object") return void 0;
|
|
784
|
+
const v = value;
|
|
785
|
+
const alpha = typeof v.alpha === "number" ? v.alpha : 1;
|
|
786
|
+
if (v.colorSpace === "srgb" && Array.isArray(v.components) && v.components.length === 3) {
|
|
787
|
+
const [r, g, b] = v.components;
|
|
788
|
+
if ([r, g, b].every((x) => typeof x === "number")) return { r, g, b, a: alpha };
|
|
789
|
+
}
|
|
790
|
+
if (typeof v.hex === "string") {
|
|
791
|
+
const c = hexToRgba(v.hex);
|
|
792
|
+
if (c) return { ...c, a: alpha };
|
|
793
|
+
}
|
|
794
|
+
return void 0;
|
|
795
|
+
}
|
|
796
|
+
function rgbaToDtcg(c) {
|
|
797
|
+
const out = {
|
|
798
|
+
colorSpace: "srgb",
|
|
799
|
+
components: [round(clamp01(c.r), 3), round(clamp01(c.g), 3), round(clamp01(c.b), 3)]
|
|
800
|
+
};
|
|
801
|
+
if (c.a < 0.9995) out.alpha = round(clamp01(c.a), 3);
|
|
802
|
+
out.hex = rgbaToHex(c);
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function colorsEqual(a, b) {
|
|
806
|
+
return rgbaToHex(a) === rgbaToHex(b) && Math.abs(a.a - b.a) < 2e-3;
|
|
807
|
+
}
|
|
808
|
+
function parseDimension(value) {
|
|
809
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
810
|
+
const v = value;
|
|
811
|
+
if (typeof v.value === "number" && typeof v.unit === "string") return { value: v.value, unit: v.unit };
|
|
812
|
+
}
|
|
813
|
+
if (typeof value === "string") {
|
|
814
|
+
const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
|
|
815
|
+
if (m) return { value: Number(m[1]), unit: m[2] };
|
|
816
|
+
}
|
|
817
|
+
if (typeof value === "number") return { value, unit: "px" };
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
var numbersEqual = (a, b) => Math.abs(a - b) < 1e-4;
|
|
821
|
+
function deepEqual(a, b) {
|
|
822
|
+
if (a === b) return true;
|
|
823
|
+
if (typeof a === "number" && typeof b === "number") return numbersEqual(a, b);
|
|
824
|
+
if (typeof a !== typeof b || !a || !b || typeof a !== "object") return false;
|
|
825
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
826
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, idx) => deepEqual(v, b[idx]));
|
|
827
|
+
const ka = Object.keys(a).filter((k) => a[k] !== void 0);
|
|
828
|
+
const kb = Object.keys(b).filter((k) => b[k] !== void 0);
|
|
829
|
+
if (ka.length !== kb.length) return false;
|
|
830
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
831
|
+
}
|
|
832
|
+
function valuesEqual(type, a, b) {
|
|
833
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
834
|
+
if (isAlias(a) || isAlias(b)) return isAlias(a) && isAlias(b) && aliasTarget(a) === aliasTarget(b);
|
|
835
|
+
if (type === "color") {
|
|
836
|
+
const ca = parseColor(a);
|
|
837
|
+
const cb = parseColor(b);
|
|
838
|
+
if (ca && cb) return colorsEqual(ca, cb);
|
|
839
|
+
}
|
|
840
|
+
if (type === "dimension") {
|
|
841
|
+
const da = parseDimension(a);
|
|
842
|
+
const db = parseDimension(b);
|
|
843
|
+
if (da && db) return da.unit === db.unit && numbersEqual(da.value, db.value);
|
|
844
|
+
}
|
|
845
|
+
return deepEqual(a, b);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ../core/src/wcag.ts
|
|
849
|
+
var AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
|
|
850
|
+
var channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
851
|
+
function luminance(c) {
|
|
852
|
+
const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
|
|
853
|
+
return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
|
|
854
|
+
}
|
|
855
|
+
function composite(fg, bg) {
|
|
856
|
+
const a = fg.a;
|
|
857
|
+
return { r: fg.r * a + bg.r * (1 - a), g: fg.g * a + bg.g * (1 - a), b: fg.b * a + bg.b * (1 - a), a: 1 };
|
|
858
|
+
}
|
|
859
|
+
function contrastRatio(fg, bg) {
|
|
860
|
+
const f = typeof fg === "string" ? hexToRgba(fg) : fg;
|
|
861
|
+
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
862
|
+
if (!f || !b) throw new Error("Invalid color");
|
|
863
|
+
const opaqueBg = { ...b, a: 1 };
|
|
864
|
+
const l1 = luminance(composite(f, opaqueBg));
|
|
865
|
+
const l2 = luminance(opaqueBg);
|
|
866
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
867
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
868
|
+
}
|
|
869
|
+
function renderedColor(value) {
|
|
870
|
+
const c = parseColor(value);
|
|
871
|
+
const hex = value && typeof value === "object" ? value.hex : void 0;
|
|
872
|
+
const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
|
|
873
|
+
return c && fromHex ? { ...fromHex, a: c.a } : c;
|
|
874
|
+
}
|
|
875
|
+
var kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
876
|
+
var contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
877
|
+
function parseContrastChecks(text) {
|
|
878
|
+
if (!text) return [];
|
|
879
|
+
const data = JSON.parse(text);
|
|
880
|
+
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
881
|
+
const out = [];
|
|
882
|
+
for (const p of list2) {
|
|
883
|
+
let fg, bg, min;
|
|
884
|
+
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
885
|
+
else if (p && typeof p === "object") {
|
|
886
|
+
const o = p;
|
|
887
|
+
fg = o.fg ?? o.foreground;
|
|
888
|
+
bg = o.bg ?? o.background;
|
|
889
|
+
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
890
|
+
}
|
|
891
|
+
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
892
|
+
out.push(contrastPair(fg, bg, min));
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ../core/src/layered.ts
|
|
898
|
+
var DTCG_TYPES = /* @__PURE__ */ new Set(["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "number", "strokeStyle", "border", "transition", "shadow", "gradient", "typography", "fontStyle", "string", "boolean"]);
|
|
899
|
+
var aliasOf = (v) => isAlias(v) ? aliasTarget(v) : null;
|
|
900
|
+
function globToRegExp(glob) {
|
|
901
|
+
let re = "";
|
|
902
|
+
for (let i = 0; i < glob.length; i++) {
|
|
903
|
+
const c = glob[i];
|
|
904
|
+
if (c === "*" && glob[i + 1] === "*") {
|
|
905
|
+
if (glob[i + 2] === "/") {
|
|
906
|
+
re += "(?:.*/)?";
|
|
907
|
+
i += 2;
|
|
908
|
+
} else {
|
|
909
|
+
re += ".*";
|
|
910
|
+
i += 1;
|
|
911
|
+
}
|
|
912
|
+
} else if (c === "*") re += "[^/]*";
|
|
913
|
+
else if (c === "?") re += "[^/]";
|
|
914
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
915
|
+
}
|
|
916
|
+
return new RegExp(`^${re}$`);
|
|
917
|
+
}
|
|
918
|
+
var comboKey = (c) => Object.entries(c).map(([k, v]) => `${k}=${v}`).join(",") || "default";
|
|
919
|
+
function flattenLayerFile(json, file, issues) {
|
|
920
|
+
const out = [];
|
|
921
|
+
const walk = (node, path, inheritedType) => {
|
|
922
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
923
|
+
const n = node;
|
|
924
|
+
const type = typeof n.$type === "string" ? n.$type : inheritedType;
|
|
925
|
+
if ("$value" in n) {
|
|
926
|
+
out.push({
|
|
927
|
+
path: path.join("."),
|
|
928
|
+
...type ? { type } : {},
|
|
929
|
+
value: n.$value,
|
|
930
|
+
...typeof n.$description === "string" ? { description: n.$description } : {},
|
|
931
|
+
...n.$extensions && typeof n.$extensions === "object" ? { extensions: n.$extensions } : {},
|
|
932
|
+
...n.$deprecated !== void 0 ? { deprecated: n.$deprecated } : {},
|
|
933
|
+
file
|
|
934
|
+
});
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
for (const [k, v] of Object.entries(n)) {
|
|
938
|
+
if (k.startsWith("$")) continue;
|
|
939
|
+
const p = [...path, k].join(".");
|
|
940
|
+
if (/[{}.]/.test(k)) issues.push({ level: "error", code: "invalid-name", message: `Name "${k}" contains {, } or . which DTCG forbids.`, path: p, file });
|
|
941
|
+
if (v && typeof v === "object" && !Array.isArray(v)) walk(v, [...path, k], type);
|
|
942
|
+
else issues.push({ level: "error", code: "not-a-token", message: `"${p}" is neither a token (no $value) nor a group.`, path: p, file });
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
walk(json, []);
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
var toHex2 = (n) => Math.round(Math.max(0, Math.min(1, n)) * 255).toString(16).padStart(2, "0");
|
|
949
|
+
function compactJson(v) {
|
|
950
|
+
if (Array.isArray(v)) return `[${v.map(compactJson).join(", ")}]`;
|
|
951
|
+
if (v && typeof v === "object") {
|
|
952
|
+
const e = Object.entries(v);
|
|
953
|
+
return e.length ? `{ ${e.map(([k, x]) => `${JSON.stringify(k)}: ${compactJson(x)}`).join(", ")} }` : "{}";
|
|
954
|
+
}
|
|
955
|
+
return JSON.stringify(v);
|
|
956
|
+
}
|
|
957
|
+
function colorToCss(c) {
|
|
958
|
+
if (typeof c === "string") return c;
|
|
959
|
+
if (!c || typeof c !== "object") return String(c);
|
|
960
|
+
const o = c;
|
|
961
|
+
const [r, g, b] = (o.components ?? []).map((x) => typeof x === "number" ? x : 0);
|
|
962
|
+
const alpha = o.alpha ?? 1;
|
|
963
|
+
if (alpha < 1) return `rgb(${[r, g, b].map((x) => Math.round((x ?? 0) * 255)).join(" ")} / ${alpha})`;
|
|
964
|
+
return o.hex ?? `#${[r, g, b].map((x) => toHex2(x ?? 0)).join("")}`;
|
|
965
|
+
}
|
|
966
|
+
var dimToCss = (d) => d && typeof d === "object" && "value" in d ? `${d.value}${d.unit}` : String(d);
|
|
967
|
+
function valueToCss(type, v, ext) {
|
|
968
|
+
if (v === void 0 || v === null) return "";
|
|
969
|
+
switch (type) {
|
|
970
|
+
case "color":
|
|
971
|
+
return colorToCss(v);
|
|
972
|
+
case "dimension":
|
|
973
|
+
case "duration":
|
|
974
|
+
return dimToCss(v);
|
|
975
|
+
case "cubicBezier":
|
|
976
|
+
return Array.isArray(v) ? `cubic-bezier(${v.join(", ")})` : String(v);
|
|
977
|
+
case "fontFamily":
|
|
978
|
+
return Array.isArray(v) ? v.map((n) => /\s/.test(String(n)) ? `"${n}"` : String(n)).join(", ") : String(v);
|
|
979
|
+
case "shadow": {
|
|
980
|
+
if (typeof v === "string") return v;
|
|
981
|
+
const layers = Array.isArray(v) ? v : [v];
|
|
982
|
+
return layers.map((l) => [l.inset ? "inset" : "", dimToCss(l.offsetX), dimToCss(l.offsetY), dimToCss(l.blur), dimToCss(l.spread ?? { value: 0, unit: "px" }), colorToCss(l.color)].filter(Boolean).join(" ")).join(", ");
|
|
983
|
+
}
|
|
984
|
+
default: {
|
|
985
|
+
const unit = ext?.["dev.bridge.unit"];
|
|
986
|
+
if (typeof v === "number" && typeof unit === "string") return `${v}${unit}`;
|
|
987
|
+
return typeof v === "object" ? compactJson(v) : String(v);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
var LayeredTokenSet = class {
|
|
992
|
+
/** `files`: token file path relative to tokens.root -> file text. */
|
|
993
|
+
constructor(config, files) {
|
|
994
|
+
this.config = config;
|
|
995
|
+
this.files = files;
|
|
996
|
+
this.fileNames = [...files.keys()].sort();
|
|
997
|
+
for (const f of this.fileNames) {
|
|
998
|
+
let json;
|
|
999
|
+
try {
|
|
1000
|
+
json = JSON.parse(files.get(f));
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
this.issues.push({ level: "error", code: "invalid-json", message: `Invalid JSON: ${e.message}`, file: f });
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
this.defs.push(...flattenLayerFile(json, f, this.issues));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
config;
|
|
1009
|
+
files;
|
|
1010
|
+
defs = [];
|
|
1011
|
+
issues = [];
|
|
1012
|
+
fileNames;
|
|
1013
|
+
views = /* @__PURE__ */ new Map();
|
|
1014
|
+
get modeNames() {
|
|
1015
|
+
return Object.keys(this.config.axes);
|
|
1016
|
+
}
|
|
1017
|
+
get defaultCombo() {
|
|
1018
|
+
return Object.fromEntries(Object.entries(this.config.axes).map(([k, m]) => [k, m.default]));
|
|
1019
|
+
}
|
|
1020
|
+
/** Every combination of the given modes (all modes when omitted); other modes stay at default. */
|
|
1021
|
+
combos(only) {
|
|
1022
|
+
let out = [this.defaultCombo];
|
|
1023
|
+
for (const [name, m] of Object.entries(this.config.axes)) {
|
|
1024
|
+
if (only && !only.includes(name)) continue;
|
|
1025
|
+
out = out.flatMap((c) => m.values.map((v) => ({ ...c, [name]: v })));
|
|
1026
|
+
}
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
normalizeCombo(partial = {}) {
|
|
1030
|
+
const c = { ...this.defaultCombo };
|
|
1031
|
+
for (const [k, v] of Object.entries(partial)) {
|
|
1032
|
+
if (v === void 0) continue;
|
|
1033
|
+
const m = this.config.axes[k];
|
|
1034
|
+
if (!m) throw new Error(`Unknown mode "${k}". Modes: ${this.modeNames.join(", ") || "none"}.`);
|
|
1035
|
+
if (!m.values.includes(v)) throw new Error(`Unknown ${k} "${v}". Values: ${m.values.join(", ")}.`);
|
|
1036
|
+
c[k] = v;
|
|
1037
|
+
}
|
|
1038
|
+
return c;
|
|
1039
|
+
}
|
|
1040
|
+
tierOfFile(file) {
|
|
1041
|
+
for (const [tier, dirs] of Object.entries(this.config.tokens.tiers)) {
|
|
1042
|
+
if (dirs.some((d) => file === d || file.startsWith(`${d.replace(/\/$/, "")}/`) || globToRegExp(d).test(file))) return tier;
|
|
1043
|
+
}
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
/** Tier of a token: the lowest layer that defines it in the default mode (an override does not change its tier). */
|
|
1047
|
+
tierOf(path) {
|
|
1048
|
+
const d = this.view(this.defaultCombo).get(path)?.[0] ?? this.defs.find((x) => x.path === path);
|
|
1049
|
+
return d ? this.tierOfFile(d.file) : void 0;
|
|
1050
|
+
}
|
|
1051
|
+
/** Files of each layer for a combo, in resolution order. */
|
|
1052
|
+
layerFiles(combo) {
|
|
1053
|
+
const files = [];
|
|
1054
|
+
const missing = [];
|
|
1055
|
+
for (const l of this.config.tokens.layers) {
|
|
1056
|
+
const pattern = l.files.replace(/\{([^}]+)\}/g, (_, k) => combo[k] ?? `{${k}}`);
|
|
1057
|
+
const re = globToRegExp(pattern);
|
|
1058
|
+
const hit = this.fileNames.filter((f) => re.test(f));
|
|
1059
|
+
if (!hit.length && !l.optional) missing.push(pattern);
|
|
1060
|
+
for (const f of hit) if (!files.includes(f)) files.push(f);
|
|
1061
|
+
}
|
|
1062
|
+
return { files, missing };
|
|
1063
|
+
}
|
|
1064
|
+
/** path -> definitions in layer order (last one wins) for a combo. */
|
|
1065
|
+
view(combo) {
|
|
1066
|
+
const key = comboKey(combo);
|
|
1067
|
+
let v = this.views.get(key);
|
|
1068
|
+
if (!v) {
|
|
1069
|
+
const { files, missing } = this.layerFiles(combo);
|
|
1070
|
+
const map = /* @__PURE__ */ new Map();
|
|
1071
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1072
|
+
for (const d of this.defs) (byFile.get(d.file) ?? byFile.set(d.file, []).get(d.file)).push(d);
|
|
1073
|
+
for (const f of files) for (const d of byFile.get(f) ?? []) (map.get(d.path) ?? map.set(d.path, []).get(d.path)).push(d);
|
|
1074
|
+
v = { map, missing };
|
|
1075
|
+
this.views.set(key, v);
|
|
1076
|
+
}
|
|
1077
|
+
return v.map;
|
|
1078
|
+
}
|
|
1079
|
+
missingLayers(combo) {
|
|
1080
|
+
this.view(combo);
|
|
1081
|
+
return this.views.get(comboKey(combo)).missing;
|
|
1082
|
+
}
|
|
1083
|
+
/** Every token path defined in any combo. */
|
|
1084
|
+
allPaths() {
|
|
1085
|
+
return [...new Set(this.defs.map((d) => d.path))].sort();
|
|
1086
|
+
}
|
|
1087
|
+
defsOf(path) {
|
|
1088
|
+
return this.defs.filter((d) => d.path === path);
|
|
1089
|
+
}
|
|
1090
|
+
/** First combo whose layers include `file`, preferring the default. */
|
|
1091
|
+
comboForFile(file) {
|
|
1092
|
+
const all = [this.defaultCombo, ...this.combos()];
|
|
1093
|
+
return all.find((c) => this.layerFiles(c).files.includes(file)) ?? this.defaultCombo;
|
|
1094
|
+
}
|
|
1095
|
+
resolve(path, combo) {
|
|
1096
|
+
const view = this.view(combo);
|
|
1097
|
+
const chain = [path];
|
|
1098
|
+
const top = view.get(path);
|
|
1099
|
+
if (!top) return { path, value: void 0, css: "", chain, definedIn: [], error: `"${path}" is not defined for ${comboKey(combo)}` };
|
|
1100
|
+
const def = top[top.length - 1];
|
|
1101
|
+
let type = def.type;
|
|
1102
|
+
let error;
|
|
1103
|
+
const deref = (target, stack) => {
|
|
1104
|
+
if (stack.includes(target)) {
|
|
1105
|
+
error = `circular alias: ${[...stack, target].join(" -> ")}`;
|
|
1106
|
+
return void 0;
|
|
1107
|
+
}
|
|
1108
|
+
const t = view.get(target);
|
|
1109
|
+
if (!t) {
|
|
1110
|
+
error = `broken alias {${target}}`;
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
const td = t[t.length - 1];
|
|
1114
|
+
type ??= td.type;
|
|
1115
|
+
return walk(td.value, [...stack, target]);
|
|
1116
|
+
};
|
|
1117
|
+
const walk = (v, stack) => {
|
|
1118
|
+
const a = aliasOf(v);
|
|
1119
|
+
if (a) return deref(a, stack);
|
|
1120
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, stack));
|
|
1121
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, stack)]));
|
|
1122
|
+
return v;
|
|
1123
|
+
};
|
|
1124
|
+
let cur = def.value;
|
|
1125
|
+
const seen = /* @__PURE__ */ new Set([path]);
|
|
1126
|
+
for (let a = aliasOf(cur); a; a = aliasOf(cur)) {
|
|
1127
|
+
chain.push(a);
|
|
1128
|
+
if (seen.has(a)) break;
|
|
1129
|
+
seen.add(a);
|
|
1130
|
+
const t = view.get(a);
|
|
1131
|
+
if (!t) break;
|
|
1132
|
+
cur = t[t.length - 1].value;
|
|
1133
|
+
}
|
|
1134
|
+
const value = walk(def.value, [path]);
|
|
1135
|
+
return { path, ...type ? { type } : {}, value, css: error ? "" : valueToCss(type, value, def.extensions), chain, definedIn: top.map((d) => d.file), ...error ? { error } : {} };
|
|
1136
|
+
}
|
|
1137
|
+
/** Tokens whose own value aliases `path` directly, across all files. */
|
|
1138
|
+
referencedBy(path) {
|
|
1139
|
+
const needle = `{${path}}`;
|
|
1140
|
+
return this.defs.filter((d) => JSON.stringify(d.value).includes(needle)).map((d) => ({ path: d.path, file: d.file }));
|
|
1141
|
+
}
|
|
1142
|
+
cssVarName(path) {
|
|
1143
|
+
return `${this.config.cssVar.prefix}${path.split(".").join(this.config.cssVar.separator)}`;
|
|
1144
|
+
}
|
|
1145
|
+
/** Repo-relative path of a token file. */
|
|
1146
|
+
repoPath(file) {
|
|
1147
|
+
const root = this.config.tokens.root;
|
|
1148
|
+
return root === "." || !root ? file : `${root.replace(/\/$/, "")}/${file}`;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function selectTokenFiles(config, repoFiles) {
|
|
1152
|
+
const root = config.tokens.root.replace(/\/$/, "");
|
|
1153
|
+
const rel = (f) => root === "." || !root ? f : f.startsWith(`${root}/`) ? f.slice(root.length + 1) : null;
|
|
1154
|
+
const inc = config.tokens.include.map(globToRegExp);
|
|
1155
|
+
const exc = config.tokens.exclude.map(globToRegExp);
|
|
1156
|
+
return repoFiles.filter((f) => f !== config.contrast?.path).map(rel).filter((f) => f !== null && inc.some((r) => r.test(f)) && !exc.some((r) => r.test(f)));
|
|
1157
|
+
}
|
|
1158
|
+
function checkShape(type, v) {
|
|
1159
|
+
if (aliasOf(v)) return null;
|
|
1160
|
+
const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
1161
|
+
switch (type) {
|
|
1162
|
+
case "color":
|
|
1163
|
+
if (typeof v === "string") return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) ? "warn:color as a hex string: DTCG 2025.10 uses { colorSpace, components, hex }" : "color must be { colorSpace, components } (or a hex string)";
|
|
1164
|
+
if (!isObj2(v) || typeof v.colorSpace !== "string" || !Array.isArray(v.components)) return "color must be { colorSpace, components, alpha?, hex? }";
|
|
1165
|
+
if (v.components.length !== 3 || !v.components.every((c) => typeof c === "number" || c === "none" || aliasOf(c))) return "color components must be 3 numbers";
|
|
1166
|
+
if (v.alpha !== void 0 && (typeof v.alpha !== "number" || v.alpha < 0 || v.alpha > 1)) return "color alpha must be between 0 and 1";
|
|
1167
|
+
if (v.hex !== void 0 && !/^#[0-9a-f]{6}$/i.test(String(v.hex))) return "color hex must be #rrggbb";
|
|
1168
|
+
return null;
|
|
1169
|
+
case "dimension":
|
|
1170
|
+
if (typeof v === "string" || typeof v === "number") return "warn:dimension as a plain value: DTCG 2025.10 uses { value, unit }";
|
|
1171
|
+
return isObj2(v) && typeof v.value === "number" && ["px", "rem"].includes(String(v.unit)) ? null : 'dimension must be { value: number, unit: "px" | "rem" }';
|
|
1172
|
+
case "duration":
|
|
1173
|
+
if (typeof v === "string") return "warn:duration as a string: DTCG 2025.10 uses { value, unit }";
|
|
1174
|
+
return isObj2(v) && typeof v.value === "number" && ["ms", "s"].includes(String(v.unit)) ? null : 'duration must be { value: number, unit: "ms" | "s" }';
|
|
1175
|
+
case "cubicBezier":
|
|
1176
|
+
return Array.isArray(v) && v.length === 4 && v.every((x) => typeof x === "number") && v[0] >= 0 && v[0] <= 1 && v[2] >= 0 && v[2] <= 1 ? null : "cubicBezier must be [x1, y1, x2, y2] with x in 0..1";
|
|
1177
|
+
case "fontFamily":
|
|
1178
|
+
return typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string") ? null : "fontFamily must be a string or an array of strings";
|
|
1179
|
+
case "fontWeight":
|
|
1180
|
+
return typeof v === "number" && v >= 1 && v <= 1e3 || typeof v === "string" ? null : "fontWeight must be 1..1000 or a keyword";
|
|
1181
|
+
case "number":
|
|
1182
|
+
return typeof v === "number" ? null : "number must be a number";
|
|
1183
|
+
case "shadow": {
|
|
1184
|
+
const layers = Array.isArray(v) ? v : [v];
|
|
1185
|
+
return layers.every((l) => isObj2(l) && "color" in l && "offsetX" in l && "offsetY" in l && "blur" in l) ? null : "shadow must be { color, offsetX, offsetY, blur, spread? } or an array of them";
|
|
1186
|
+
}
|
|
1187
|
+
default:
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function validateLayered(set) {
|
|
1192
|
+
const issues = [...set.issues];
|
|
1193
|
+
for (const d of set.defs) {
|
|
1194
|
+
if (d.type && !DTCG_TYPES.has(d.type)) issues.push({ level: "error", code: "unknown-type", message: `Unknown $type "${d.type}".`, path: d.path, file: d.file });
|
|
1195
|
+
if (d.type) {
|
|
1196
|
+
const s = checkShape(d.type, d.value);
|
|
1197
|
+
if (s) issues.push({ level: s.startsWith("warn:") ? "warning" : "error", code: "invalid-value", message: s.replace(/^warn:/, ""), path: d.path, file: d.file });
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1201
|
+
const add = (i, combo) => {
|
|
1202
|
+
const k = `${i.code}|${i.path}|${i.message}`;
|
|
1203
|
+
const prev = merged.get(k);
|
|
1204
|
+
if (prev) prev.modes.push(comboKey(combo));
|
|
1205
|
+
else merged.set(k, { ...i, modes: [comboKey(combo)] });
|
|
1206
|
+
};
|
|
1207
|
+
for (const combo of set.combos()) {
|
|
1208
|
+
for (const miss of set.missingLayers(combo)) add({ level: "error", code: "missing-layer", message: `No file matches layer "${miss}".` }, combo);
|
|
1209
|
+
for (const [path, defs] of set.view(combo)) {
|
|
1210
|
+
const r = set.resolve(path, combo);
|
|
1211
|
+
const def = defs[defs.length - 1];
|
|
1212
|
+
if (r.error) add({ level: "error", code: r.error.startsWith("circular") ? "circular-alias" : "broken-alias", message: r.error, path, file: def.file }, combo);
|
|
1213
|
+
else if (!r.type) add({ level: "error", code: "missing-type", message: "No $type on the token, its groups or its alias target.", path, file: def.file }, combo);
|
|
1214
|
+
else if (def.type && aliasOf(def.value)) {
|
|
1215
|
+
const target = set.view(combo).get(aliasOf(def.value));
|
|
1216
|
+
const tt = target?.[target.length - 1]?.type;
|
|
1217
|
+
if (tt && tt !== def.type) add({ level: "error", code: "type-mismatch", message: `$type ${def.type} aliases a ${tt} token (${String(def.value)}).`, path, file: def.file }, combo);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const total = set.combos().length;
|
|
1222
|
+
for (const i of merged.values()) {
|
|
1223
|
+
if (i.modes.length === total) delete i.modes;
|
|
1224
|
+
issues.push(i);
|
|
1225
|
+
}
|
|
1226
|
+
issues.push(...layeredParityIssues(set));
|
|
1227
|
+
return issues;
|
|
1228
|
+
}
|
|
1229
|
+
function layeredParityIssues(set) {
|
|
1230
|
+
const out = [];
|
|
1231
|
+
const pathsOf = (file) => new Set(set.defs.filter((d) => d.file === file).map((d) => d.path));
|
|
1232
|
+
for (const [mode, m] of Object.entries(set.config.axes)) {
|
|
1233
|
+
if (!m.parity) continue;
|
|
1234
|
+
for (const layer of set.config.tokens.layers) {
|
|
1235
|
+
if (!layer.files.includes(`{${mode}}`) || layer.optional) continue;
|
|
1236
|
+
const others = (layer.files.match(/\{([^}]+)\}/g) ?? []).map((p) => p.slice(1, -1)).filter((k) => k !== mode);
|
|
1237
|
+
let fixed = [{}];
|
|
1238
|
+
for (const o of others) fixed = fixed.flatMap((c) => (set.config.axes[o]?.values ?? []).map((v) => ({ ...c, [o]: v })));
|
|
1239
|
+
for (const c of fixed) {
|
|
1240
|
+
const filesByValue = m.values.map((v) => {
|
|
1241
|
+
const re = globToRegExp(layer.files.replace(/\{([^}]+)\}/g, (_, k) => k === mode ? v : c[k]));
|
|
1242
|
+
return { v, files: set.fileNames.filter((f) => re.test(f)) };
|
|
1243
|
+
});
|
|
1244
|
+
const present = filesByValue.filter((x) => x.files.length);
|
|
1245
|
+
if (!present.length) continue;
|
|
1246
|
+
for (const x of filesByValue) {
|
|
1247
|
+
if (!x.files.length) out.push({ level: "error", code: "mode-parity", message: `${mode} "${x.v}" has no file for layer "${layer.files}" while ${present.map((p) => p.v).join(", ")} do.` });
|
|
1248
|
+
}
|
|
1249
|
+
const sets = present.map((x) => ({ v: x.v, files: x.files, paths: new Set(x.files.flatMap((f) => [...pathsOf(f)])) }));
|
|
1250
|
+
const union = new Set(sets.flatMap((s) => [...s.paths]));
|
|
1251
|
+
for (const p of union) {
|
|
1252
|
+
const lacking = sets.filter((s) => !s.paths.has(p));
|
|
1253
|
+
if (lacking.length) {
|
|
1254
|
+
const having = sets.find((s) => s.paths.has(p));
|
|
1255
|
+
out.push({ level: "error", code: "mode-parity", message: `Defined for ${mode} ${sets.filter((s) => s.paths.has(p)).map((s) => s.v).join(", ")} but not ${lacking.map((s) => s.v).join(", ")} (${lacking.map((s) => s.files.join(", ")).join("; ")}).`, path: p, file: having.files[0] });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ../core/src/scopes.ts
|
|
1265
|
+
var has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
1266
|
+
function defaultScopes(path, type, tier) {
|
|
1267
|
+
const s = path.split(".");
|
|
1268
|
+
switch (type) {
|
|
1269
|
+
case "color":
|
|
1270
|
+
if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
|
|
1271
|
+
if (has(s, "text")) return ["TEXT_FILL"];
|
|
1272
|
+
if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
|
|
1273
|
+
if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
|
|
1274
|
+
if (has(s, "shadow")) return ["EFFECT_COLOR"];
|
|
1275
|
+
return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
|
|
1276
|
+
case "dimension":
|
|
1277
|
+
if (has(s, "radius")) return ["CORNER_RADIUS"];
|
|
1278
|
+
if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
|
|
1279
|
+
if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
|
|
1280
|
+
if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
|
|
1281
|
+
if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
|
|
1282
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
1283
|
+
case "fontWeight":
|
|
1284
|
+
return ["FONT_WEIGHT"];
|
|
1285
|
+
case "fontFamily":
|
|
1286
|
+
return ["FONT_FAMILY"];
|
|
1287
|
+
case "number":
|
|
1288
|
+
if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
|
|
1289
|
+
if (has(s, "opacity")) return ["OPACITY"];
|
|
1290
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
1291
|
+
case "string":
|
|
1292
|
+
return ["TEXT_CONTENT"];
|
|
1293
|
+
default:
|
|
1294
|
+
return [];
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
var sameScopes = (a, b) => {
|
|
1298
|
+
const sa = [...new Set(a)].sort();
|
|
1299
|
+
const sb = [...new Set(b)].sort();
|
|
1300
|
+
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
1301
|
+
};
|
|
1302
|
+
var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
1303
|
+
|
|
1304
|
+
// ../core/src/diff-engine.ts
|
|
1305
|
+
var normDesc = (d) => (d ?? "").trim();
|
|
1306
|
+
function fieldChanges(a, b) {
|
|
1307
|
+
const out = [];
|
|
1308
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
1309
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
1310
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
1311
|
+
for (const mode of modes) {
|
|
1312
|
+
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
1313
|
+
}
|
|
1314
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
1315
|
+
const sa = effectiveScopes(a);
|
|
1316
|
+
const sb = effectiveScopes(b);
|
|
1317
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
1318
|
+
return out;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ../core/src/risk.ts
|
|
1322
|
+
var RISK_LABELS = {
|
|
1323
|
+
1: "Tier 1: low risk, one code owner",
|
|
1324
|
+
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
1325
|
+
3: "Tier 3: token change, needs design + tech approval"
|
|
1326
|
+
};
|
|
1327
|
+
var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
1328
|
+
var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
1329
|
+
var isSummary = (c) => "kind" in c;
|
|
1330
|
+
function summarizeTokenChange(c) {
|
|
1331
|
+
if (isSummary(c)) return c;
|
|
1332
|
+
const path = (c.target ?? c.current).path;
|
|
1333
|
+
if (!c.target) return { kind: "removed", path };
|
|
1334
|
+
if (!c.current) return { kind: "added", path };
|
|
1335
|
+
if (c.current.path !== c.target.path) return { kind: "renamed", path };
|
|
1336
|
+
const fields = fieldChanges(c.current, c.target);
|
|
1337
|
+
return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
|
|
1338
|
+
}
|
|
1339
|
+
function tokenBump(changes) {
|
|
1340
|
+
let bump;
|
|
1341
|
+
for (const c of changes.map(summarizeTokenChange)) {
|
|
1342
|
+
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
1343
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
1344
|
+
}
|
|
1345
|
+
return bump;
|
|
1346
|
+
}
|
|
1347
|
+
function isGuardrail(path, config) {
|
|
1348
|
+
const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
|
|
1349
|
+
return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
|
|
1350
|
+
}
|
|
1351
|
+
function classifyRisk(input) {
|
|
1352
|
+
const reasons = [];
|
|
1353
|
+
const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
|
|
1354
|
+
for (const c of tokenChanges) {
|
|
1355
|
+
if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
|
|
1356
|
+
else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
|
|
1357
|
+
}
|
|
1358
|
+
const bump = tokenBump(tokenChanges);
|
|
1359
|
+
const componentsDir = input.config?.components?.dir;
|
|
1360
|
+
for (const f of input.files ?? []) {
|
|
1361
|
+
const p = f.path.replace(/^\/+/, "");
|
|
1362
|
+
if (p.endsWith(".tokens.json")) {
|
|
1363
|
+
if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
|
|
1364
|
+
} else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
|
|
1365
|
+
else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
|
|
1366
|
+
else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
|
|
1367
|
+
else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
|
|
1368
|
+
}
|
|
1369
|
+
reasons.sort((a, b) => b.tier - a.tier);
|
|
1370
|
+
const tier = reasons[0]?.tier ?? 1;
|
|
1371
|
+
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ../core/src/layered-review.ts
|
|
1375
|
+
function checkLayeredPair(set, pair, combo) {
|
|
1376
|
+
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
1377
|
+
const f = set.resolve(pair.foreground, combo);
|
|
1378
|
+
const b = set.resolve(pair.background, combo);
|
|
1379
|
+
if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
|
|
1380
|
+
const fc = renderedColor(f.value);
|
|
1381
|
+
const bc = renderedColor(b.value);
|
|
1382
|
+
if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
|
|
1383
|
+
if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
|
|
1384
|
+
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
1385
|
+
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
1386
|
+
}
|
|
1387
|
+
function runLayeredContrast(set, pairs, modes) {
|
|
1388
|
+
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
1389
|
+
return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
|
|
1390
|
+
}
|
|
1391
|
+
function contrastDelta(before, after) {
|
|
1392
|
+
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
1393
|
+
const prev = new Map(before.map((r) => [key(r), r]));
|
|
1394
|
+
return after.map((a) => {
|
|
1395
|
+
const b = prev.get(key(a));
|
|
1396
|
+
return { fg: a.fg, bg: a.bg, min: a.min, modes: a.modes, before: b?.ratio ?? null, after: a.ratio, passBefore: b?.pass ?? true, passAfter: a.pass };
|
|
1397
|
+
}).filter((d) => d.before !== d.after || !d.passAfter);
|
|
1398
|
+
}
|
|
1399
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1400
|
+
function diffLayered(before, after) {
|
|
1401
|
+
const key = (d) => `${d.file}\0${d.path}`;
|
|
1402
|
+
const b = new Map(before.defs.map((d) => [key(d), d]));
|
|
1403
|
+
const a = new Map(after.defs.map((d) => [key(d), d]));
|
|
1404
|
+
const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
|
|
1405
|
+
const tierOf = (set, f) => {
|
|
1406
|
+
const t = set.tierOfFile(f);
|
|
1407
|
+
return t ? { tier: t } : {};
|
|
1408
|
+
};
|
|
1409
|
+
const changes = [];
|
|
1410
|
+
const removed = [];
|
|
1411
|
+
const added = [];
|
|
1412
|
+
for (const [k, d] of b) {
|
|
1413
|
+
const n = a.get(k);
|
|
1414
|
+
if (!n) {
|
|
1415
|
+
removed.push(d);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const fields = [];
|
|
1419
|
+
if (!same(d.value, n.value)) fields.push("value");
|
|
1420
|
+
if (d.type !== n.type) fields.push("type");
|
|
1421
|
+
if (d.description !== n.description) fields.push("description");
|
|
1422
|
+
if (!same(d.extensions, n.extensions)) fields.push("extensions");
|
|
1423
|
+
if (!fields.length) continue;
|
|
1424
|
+
changes.push({
|
|
1425
|
+
kind: "changed",
|
|
1426
|
+
path: d.path,
|
|
1427
|
+
file: d.file,
|
|
1428
|
+
...tierOf(after, d.file),
|
|
1429
|
+
...n.type ? { type: n.type } : {},
|
|
1430
|
+
fields,
|
|
1431
|
+
before: d.value,
|
|
1432
|
+
after: n.value,
|
|
1433
|
+
beforeCss: css(before, d),
|
|
1434
|
+
afterCss: css(after, n),
|
|
1435
|
+
...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
for (const [k, d] of a) if (!b.has(k)) added.push(d);
|
|
1439
|
+
for (const r of [...removed]) {
|
|
1440
|
+
const i = added.findIndex((x) => x.file === r.file && same(x.value, r.value) && x.type === r.type);
|
|
1441
|
+
if (i < 0) continue;
|
|
1442
|
+
const n = added.splice(i, 1)[0];
|
|
1443
|
+
removed.splice(removed.indexOf(r), 1);
|
|
1444
|
+
changes.push({ kind: "renamed", path: n.path, from: r.path, file: n.file, ...n.type ? { type: n.type } : {}, fields: [], before: r.value, after: n.value, afterCss: css(after, n), ...tierOf(after, n.file) });
|
|
1445
|
+
}
|
|
1446
|
+
for (const d of removed) changes.push({ kind: "removed", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], before: d.value, beforeCss: css(before, d), ...d.description ? { descriptionBefore: d.description } : {}, ...tierOf(before, d.file) });
|
|
1447
|
+
for (const d of added) changes.push({ kind: "added", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], after: d.value, afterCss: css(after, d), ...d.description ? { descriptionAfter: d.description } : {}, ...tierOf(after, d.file) });
|
|
1448
|
+
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
1449
|
+
}
|
|
1450
|
+
function layeredImpact(before, after, direct, limit = 60) {
|
|
1451
|
+
const skip = new Set(direct.map((c) => c.path));
|
|
1452
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1453
|
+
for (const combo of after.combos()) {
|
|
1454
|
+
const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
|
|
1455
|
+
for (const p of paths) {
|
|
1456
|
+
if (skip.has(p)) continue;
|
|
1457
|
+
const x = before.resolve(p, combo).css;
|
|
1458
|
+
const y = after.resolve(p, combo).css;
|
|
1459
|
+
if (x === y) continue;
|
|
1460
|
+
const e = byPath.get(p);
|
|
1461
|
+
if (e) e.modes.push(comboKey(combo));
|
|
1462
|
+
else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1466
|
+
return { total: all.length, entries: all.slice(0, limit) };
|
|
1467
|
+
}
|
|
1468
|
+
var toChangeSummary = (c) => ({
|
|
1469
|
+
kind: c.kind,
|
|
1470
|
+
path: c.path,
|
|
1471
|
+
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
1472
|
+
});
|
|
1473
|
+
function classifyLayered(changes, otherFiles = [], config) {
|
|
1474
|
+
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
1475
|
+
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
1476
|
+
}
|
|
1477
|
+
var short = (v) => {
|
|
1478
|
+
if (v === void 0) return "";
|
|
1479
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1480
|
+
return s.length > 60 ? `${s.slice(0, 57)}...` : s;
|
|
1481
|
+
};
|
|
1482
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1483
|
+
var swatch = (css) => css ? `\`${cell(css)}\`` : "";
|
|
1484
|
+
var aliasNote = (v) => typeof v === "string" && v.startsWith("{") ? ` (${cell(short(v))})` : "";
|
|
1485
|
+
function changeTable(changes) {
|
|
1486
|
+
if (!changes.length) return "_No token changes._";
|
|
1487
|
+
const rows = changes.map((c) => {
|
|
1488
|
+
const what = c.kind === "renamed" ? `renamed from \`${c.from}\`` : c.kind === "changed" ? c.fields.join(", ") : c.kind;
|
|
1489
|
+
const before = c.kind === "added" || c.kind === "renamed" ? "" : `${swatch(c.beforeCss)}${aliasNote(c.before)}`;
|
|
1490
|
+
const after = c.kind === "removed" ? "" : `${swatch(c.afterCss)}${aliasNote(c.after)}`;
|
|
1491
|
+
return `| \`${c.path}\` | ${what} | ${before} | ${after} | ${c.file} |`;
|
|
1492
|
+
});
|
|
1493
|
+
return ["| Token | Change | Before | After | File |", "| --- | --- | --- | --- | --- |", ...rows].join("\n");
|
|
1494
|
+
}
|
|
1495
|
+
function contrastDeltaTable(deltas, limit = 40) {
|
|
1496
|
+
if (!deltas.length) return "_No contrast pair moved._";
|
|
1497
|
+
const rows = deltas.slice(0, limit).map((d) => `| \`${d.fg}\` on \`${d.bg}\` | ${d.modes} | ${d.before ?? "n/a"} | ${d.after ?? "n/a"} | ${d.min} | ${d.passAfter ? "pass" : "**fail**"} |`);
|
|
1498
|
+
const more = deltas.length > limit ? `
|
|
1499
|
+
|
|
1500
|
+
_${deltas.length - limit} more pair(s) not shown._` : "";
|
|
1501
|
+
return ["| Pair | Mode | Before | After | Min | Result |", "| --- | --- | --- | --- | --- | --- |", ...rows].join("\n") + more;
|
|
1502
|
+
}
|
|
1503
|
+
function composeProposalBody(args) {
|
|
1504
|
+
const t = args.tier.tier;
|
|
1505
|
+
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
|
|
1506
|
+
const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
|
|
1507
|
+
return [
|
|
1508
|
+
"## What changed and why",
|
|
1509
|
+
"",
|
|
1510
|
+
args.summary.trim(),
|
|
1511
|
+
"",
|
|
1512
|
+
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
1513
|
+
"",
|
|
1514
|
+
"## Risk tier",
|
|
1515
|
+
"",
|
|
1516
|
+
box(1),
|
|
1517
|
+
box(2),
|
|
1518
|
+
box(3),
|
|
1519
|
+
"",
|
|
1520
|
+
...args.tier.reasons.map((r) => `- ${r}`),
|
|
1521
|
+
`- Semver bump: \`${args.tier.bump}\``,
|
|
1522
|
+
"",
|
|
1523
|
+
"## Token changes",
|
|
1524
|
+
"",
|
|
1525
|
+
changeTable(args.changes),
|
|
1526
|
+
"",
|
|
1527
|
+
"## Contrast before and after",
|
|
1528
|
+
"",
|
|
1529
|
+
contrastDeltaTable(args.contrast),
|
|
1530
|
+
"",
|
|
1531
|
+
"## Downstream impact",
|
|
1532
|
+
"",
|
|
1533
|
+
args.impact.total ? [`${args.impact.total} token(s) resolve to a new value because they alias an edited token.`, "", ...impactList, args.impact.total > 15 ? `- and ${args.impact.total - 15} more` : ""].filter(Boolean).join("\n") : "_No other token changes value._",
|
|
1534
|
+
"",
|
|
1535
|
+
"## Checks",
|
|
1536
|
+
"",
|
|
1537
|
+
`- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
|
|
1538
|
+
`- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
|
|
1539
|
+
"",
|
|
1540
|
+
"## Authored by",
|
|
1541
|
+
"",
|
|
1542
|
+
"- [ ] Human",
|
|
1543
|
+
"- [x] AI agent (Claude), reviewed by a human before merge",
|
|
1544
|
+
"",
|
|
1545
|
+
"## Design review checklist",
|
|
1546
|
+
"",
|
|
1547
|
+
"- [ ] Checked in light and dark themes and in every brand",
|
|
1548
|
+
"- [ ] Contrast table reviewed, no pair loosened",
|
|
1549
|
+
"- [ ] Changeset bump matches the tier",
|
|
1550
|
+
""
|
|
1551
|
+
].join("\n");
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// ../core/src/node/git-local.ts
|
|
1555
|
+
import { execFile } from "node:child_process";
|
|
1556
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
1557
|
+
import { tmpdir } from "node:os";
|
|
1558
|
+
import { join } from "node:path";
|
|
1559
|
+
|
|
1560
|
+
// ../core/src/governance.ts
|
|
1561
|
+
var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
1562
|
+
var ProtectedBranchError = class extends Error {
|
|
1563
|
+
constructor(message) {
|
|
1564
|
+
super(message);
|
|
1565
|
+
this.name = "ProtectedBranchError";
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
function assertSafeBranch(branch, rules = {}) {
|
|
1569
|
+
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
1570
|
+
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
1571
|
+
if (!name || blocked.has(name)) {
|
|
1572
|
+
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
1573
|
+
}
|
|
1574
|
+
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
1575
|
+
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// ../core/src/node/git-local.ts
|
|
1580
|
+
function git(root, args, opts = {}) {
|
|
1581
|
+
return new Promise((resolve, reject) => {
|
|
1582
|
+
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
1583
|
+
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
1584
|
+
else resolve(stdout);
|
|
1585
|
+
});
|
|
1586
|
+
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async function isGitRepo(root) {
|
|
1590
|
+
try {
|
|
1591
|
+
return (await git(root, ["rev-parse", "--is-inside-work-tree"])).trim() === "true";
|
|
1592
|
+
} catch {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function currentBranch(root) {
|
|
1597
|
+
return (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
1598
|
+
}
|
|
1599
|
+
async function branchExists(root, branch) {
|
|
1600
|
+
try {
|
|
1601
|
+
await git(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
1602
|
+
return true;
|
|
1603
|
+
} catch {
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async function showFile(root, ref, path) {
|
|
1608
|
+
try {
|
|
1609
|
+
return await git(root, ["show", `${ref}:${path}`]);
|
|
1610
|
+
} catch {
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async function listFilesAt(root, ref) {
|
|
1615
|
+
return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
|
|
1616
|
+
}
|
|
1617
|
+
async function originRepo(root) {
|
|
1618
|
+
try {
|
|
1619
|
+
const url = (await git(root, ["remote", "get-url", "origin"])).trim();
|
|
1620
|
+
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
|
|
1621
|
+
} catch {
|
|
1622
|
+
return void 0;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async function commitToNewLocalBranch(root, args) {
|
|
1626
|
+
try {
|
|
1627
|
+
assertSafeBranch(args.branch, args);
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
throw new Error(`Refusing to write branch "${args.branch}": ${err.message}`);
|
|
1630
|
+
}
|
|
1631
|
+
if (await branchExists(root, args.branch)) throw new Error(`Branch "${args.branch}" already exists. Pick another title or delete it first.`);
|
|
1632
|
+
const baseSha = (await git(root, ["rev-parse", "--verify", `${args.base}^{commit}`])).trim();
|
|
1633
|
+
const dir = await mkdtemp(join(tmpdir(), "bridge-index-"));
|
|
1634
|
+
const env = { GIT_INDEX_FILE: join(dir, "index") };
|
|
1635
|
+
try {
|
|
1636
|
+
await git(root, ["read-tree", baseSha], { env });
|
|
1637
|
+
for (const f of args.files) {
|
|
1638
|
+
if (f.content === null) {
|
|
1639
|
+
await git(root, ["update-index", "--force-remove", f.path], { env });
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const blob = (await git(root, ["hash-object", "-w", "--stdin"], { input: f.content })).trim();
|
|
1643
|
+
await git(root, ["update-index", "--add", "--cacheinfo", `100644,${blob},${f.path}`], { env });
|
|
1644
|
+
}
|
|
1645
|
+
const tree = (await git(root, ["write-tree"], { env })).trim();
|
|
1646
|
+
const ident = await identityEnv(root);
|
|
1647
|
+
const sha = (await git(root, ["commit-tree", tree, "-p", baseSha, "-F", "-"], { input: args.message, env: ident })).trim();
|
|
1648
|
+
await git(root, ["update-ref", `refs/heads/${args.branch}`, sha, ""]);
|
|
1649
|
+
const diff = await git(root, ["diff", baseSha, sha]);
|
|
1650
|
+
return { sha, diff };
|
|
1651
|
+
} finally {
|
|
1652
|
+
await rm(dir, { recursive: true, force: true });
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async function identityEnv(root) {
|
|
1656
|
+
const get = async (k) => (await git(root, ["config", k]).catch(() => "")).trim();
|
|
1657
|
+
const name = await get("user.name") || "Bridge";
|
|
1658
|
+
const email = await get("user.email") || "bridge@users.noreply.github.com";
|
|
1659
|
+
return { GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: name, GIT_COMMITTER_EMAIL: email };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
export {
|
|
1663
|
+
__export,
|
|
1664
|
+
previewLinks,
|
|
1665
|
+
readPreviewComment,
|
|
1666
|
+
findPreviewComment,
|
|
1667
|
+
resolveBranchPreviews,
|
|
1668
|
+
previewsSettled,
|
|
1669
|
+
expandOwners,
|
|
1670
|
+
entryLabel,
|
|
1671
|
+
unreadableTeams,
|
|
1672
|
+
TeamDirectory,
|
|
1673
|
+
DEFAULT_RIGHTS,
|
|
1674
|
+
approverEntries,
|
|
1675
|
+
shipPolicy,
|
|
1676
|
+
whoCanShipSentence,
|
|
1677
|
+
CONFIG_FILE,
|
|
1678
|
+
loadConfig,
|
|
1679
|
+
tokensPath,
|
|
1680
|
+
protectedBranchesOf,
|
|
1681
|
+
baseBranchOf,
|
|
1682
|
+
toConfigJson,
|
|
1683
|
+
hexToRgba,
|
|
1684
|
+
rgbaToHex,
|
|
1685
|
+
rgbaToDtcg,
|
|
1686
|
+
renderedColor,
|
|
1687
|
+
contrastPair,
|
|
1688
|
+
parseContrastChecks,
|
|
1689
|
+
RISK_LABELS,
|
|
1690
|
+
classifyRisk,
|
|
1691
|
+
aliasOf,
|
|
1692
|
+
globToRegExp,
|
|
1693
|
+
comboKey,
|
|
1694
|
+
LayeredTokenSet,
|
|
1695
|
+
selectTokenFiles,
|
|
1696
|
+
validateLayered,
|
|
1697
|
+
layeredParityIssues,
|
|
1698
|
+
checkLayeredPair,
|
|
1699
|
+
runLayeredContrast,
|
|
1700
|
+
contrastDelta,
|
|
1701
|
+
diffLayered,
|
|
1702
|
+
layeredImpact,
|
|
1703
|
+
classifyLayered,
|
|
1704
|
+
changeTable,
|
|
1705
|
+
contrastDeltaTable,
|
|
1706
|
+
composeProposalBody,
|
|
1707
|
+
DEFAULT_PROTECTED_BRANCHES,
|
|
1708
|
+
ProtectedBranchError,
|
|
1709
|
+
assertSafeBranch,
|
|
1710
|
+
git,
|
|
1711
|
+
isGitRepo,
|
|
1712
|
+
currentBranch,
|
|
1713
|
+
branchExists,
|
|
1714
|
+
showFile,
|
|
1715
|
+
listFilesAt,
|
|
1716
|
+
originRepo,
|
|
1717
|
+
commitToNewLocalBranch
|
|
1718
|
+
};
|