@bridge-ds/mcp 0.2.0 → 0.3.1

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.
@@ -43,18 +43,407 @@ function previewLinks(apps, ctx) {
43
43
  }
44
44
  return out;
45
45
  }
46
- function snapshotUrl(owner, repo, pkg, ref) {
47
- return `https://pkg.pr.new/${owner}/${repo}/${pkg}@${ref}`;
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;
48
53
  }
49
- function installCommand(urls, pm = "npm") {
50
- const verb = pm === "npm" ? "npm i" : pm === "pnpm" ? "pnpm add" : "yarn add";
51
- return `${verb} ${urls.join(" ")}`;
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 };
52
60
  }
53
- function tarballAssetName(pkg, sha) {
54
- return `${pkg.replace(/^@/, "").replace(/\//g, "-")}-${sha.slice(0, 7)}.tgz`;
61
+ function findPreviewComment(comments) {
62
+ return [...comments].reverse().find((c) => !!c.body && c.body.trimStart().startsWith(PREVIEW_COMMENT_MARKER));
55
63
  }
56
- function tarballUrl(owner, repo, pr, pkg, sha) {
57
- return `https://github.com/${owner}/${repo}/releases/download/preview-pr-${pr}/${tarballAssetName(pkg, sha)}`;
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;
58
447
  }
59
448
 
60
449
  // ../core/src/config.ts
@@ -194,9 +583,11 @@ function defineConfig(input) {
194
583
  requireReason: g.override.requireReason ?? DEFAULT_OVERRIDE.requireReason,
195
584
  minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
196
585
  }
197
- } : {}
586
+ } : {},
587
+ ...g.rights ? { rights: normalizeRights(g.rights) } : {}
198
588
  },
199
589
  ...input.audit ? { audit: { ...input.audit } } : {},
590
+ ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
200
591
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
201
592
  ...input.previews ? {
202
593
  previews: {
@@ -303,9 +694,7 @@ function loadConfig(source) {
303
694
  else {
304
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`);
305
696
  if (gov.branchPrefix !== void 0 && !isStr(gov.branchPrefix)) problems.push('"governance.branchPrefix" must be a non-empty string');
306
- const o = gov.owners;
307
- if (o !== void 0 && (!isObj(o) || ["design", "tech"].some((k) => o[k] !== void 0 && !(Array.isArray(o[k]) && o[k].every((x) => typeof x === "string")))))
308
- problems.push('"governance.owners" must be { "design": [logins], "tech": [logins] }');
697
+ problems.push(...ownerEntryProblems(gov.owners));
309
698
  const ov = gov.override;
310
699
  if (ov !== void 0) {
311
700
  if (!isObj(ov)) problems.push('"governance.override" must be an object');
@@ -315,8 +704,10 @@ function loadConfig(source) {
315
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');
316
705
  }
317
706
  }
707
+ problems.push(...rightsProblems(gov.rights));
318
708
  }
319
709
  }
710
+ problems.push(...visualImpactProblems(d.visualImpact));
320
711
  const audit = d.audit;
321
712
  if (audit !== void 0) {
322
713
  if (!isObj(audit)) problems.push('"audit" must be an object');
@@ -362,7 +753,8 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
362
753
  cssVar: config.cssVar,
363
754
  governance: config.governance,
364
755
  ...config.audit ? { audit: config.audit } : {},
365
- ...config.previews ? { previews: config.previews } : {}
756
+ ...config.previews ? { previews: config.previews } : {},
757
+ ...config.visualImpact ? { visualImpact: config.visualImpact } : {}
366
758
  };
367
759
  }
368
760
 
@@ -1270,9 +1662,18 @@ async function identityEnv(root) {
1270
1662
  export {
1271
1663
  __export,
1272
1664
  previewLinks,
1273
- snapshotUrl,
1274
- installCommand,
1275
- tarballUrl,
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,
1276
1677
  CONFIG_FILE,
1277
1678
  loadConfig,
1278
1679
  tokensPath,
package/dist/drafts.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type PendingDrafts } from '@bridge-ds/core';
1
+ import { type DraftResultInput, type PendingDrafts } from '@bridge-ds/core';
2
2
  import type { BridgeContext } from './context.js';
3
3
  /** Images larger than this are linked, not inlined (bytes of PNG). */
4
4
  export declare const IMAGE_INLINE_MAX = 750000;
@@ -8,6 +8,8 @@ export interface InlineImage {
8
8
  sessionId: string;
9
9
  itemId: string;
10
10
  label: 'before' | 'after';
11
+ /** The change line this image shows (push items with several components). */
12
+ line?: string | undefined;
11
13
  base64: string;
12
14
  }
13
15
  export interface PendingDraftsAnswer {
@@ -32,11 +34,7 @@ export declare function pendingDrafts(ctx: BridgeContext, a: {
32
34
  /** Writes Claude's texts into a session (sanitized, status "drafted" once every item has one), retrying on 409. */
33
35
  export declare function writeDrafts(ctx: BridgeContext, a: {
34
36
  sessionId: string;
35
- results: {
36
- id: string;
37
- title: string;
38
- summary: string;
39
- }[];
37
+ results: DraftResultInput[];
40
38
  }): Promise<{
41
39
  next: string;
42
40
  waiting?: string[] | undefined;
@@ -1,4 +1,4 @@
1
- import { explorationRules } from '@bridge-ds/core';
1
+ import { explorationRules, type ExplorationSource, type ExplorationTryIt } from '@bridge-ds/core';
2
2
  import { BridgeContext } from './context.js';
3
3
  /** `explore/<slug>` from a branch, a slug or a name; throws with how to fix it when it cannot be one. */
4
4
  export declare function toExplorationBranch(ref: string): string;
@@ -8,6 +8,9 @@ export interface StartExplorationInput {
8
8
  useCase?: string | undefined;
9
9
  experimental?: string[] | undefined;
10
10
  author?: string | undefined;
11
+ /** Where it is started from (the MCP tool says mcp, the CLI says cli), and the AI agent behind it when known. */
12
+ source?: ExplorationSource | undefined;
13
+ agent?: string | undefined;
11
14
  dryRun?: boolean | undefined;
12
15
  }
13
16
  /**
@@ -31,7 +34,11 @@ export declare function startExploration(ctx: BridgeContext, a: StartExploration
31
34
  pr: number;
32
35
  url: string;
33
36
  commit: string;
34
- install: import("@bridge-ds/core").ExplorationInstall;
37
+ install: {
38
+ state: ExplorationTryIt["state"];
39
+ command: string | null;
40
+ note: string;
41
+ };
35
42
  previews: {
36
43
  app: string;
37
44
  label: string;
@@ -64,6 +71,15 @@ export declare function startExploration(ctx: BridgeContext, a: StartExploration
64
71
  prBody: string;
65
72
  status: "branch-created";
66
73
  }>;
74
+ /**
75
+ * The install command of an exploration as tools show it: the one the preview workflow published (its sticky comment
76
+ * on the pull request), or why there is none yet. Never a guessed pkg.pr.new URL.
77
+ */
78
+ export declare function installOf(t: ExplorationTryIt): {
79
+ state: ExplorationTryIt['state'];
80
+ command: string | null;
81
+ note: string;
82
+ };
67
83
  /** bridge_list_explorations / `bridge explore list`. */
68
84
  export declare function listExplorations(ctx: BridgeContext, a?: {
69
85
  state?: 'open' | 'closed' | 'all';
@@ -75,7 +91,8 @@ export declare function listExplorations(ctx: BridgeContext, a?: {
75
91
  running: number;
76
92
  total: number;
77
93
  summary: string;
78
- explorations: {
94
+ explorations: ({
95
+ install: null;
79
96
  requests: {
80
97
  number: number;
81
98
  title: string;
@@ -92,14 +109,79 @@ export declare function listExplorations(ctx: BridgeContext, a?: {
92
109
  status: import("@bridge-ds/core").ExplorationStatus;
93
110
  draft: boolean;
94
111
  author: string;
112
+ source?: ExplorationSource | undefined;
113
+ agent?: string | undefined;
95
114
  updatedAt: string;
115
+ headSha: string;
96
116
  previews: {
97
117
  app: string;
98
118
  label: string;
99
119
  url: string;
100
120
  }[];
101
- install: import("@bridge-ds/core").ExplorationInstall;
102
- }[];
121
+ } | {
122
+ install: {
123
+ state: ExplorationTryIt["state"];
124
+ command: string | null;
125
+ note: string;
126
+ };
127
+ requests: {
128
+ number: number;
129
+ title: string;
130
+ url: string;
131
+ state: string;
132
+ }[];
133
+ number: number;
134
+ url: string;
135
+ branch: string;
136
+ slug: string;
137
+ name: string;
138
+ purpose: string;
139
+ useCase?: string | undefined;
140
+ status: import("@bridge-ds/core").ExplorationStatus;
141
+ draft: boolean;
142
+ author: string;
143
+ source?: ExplorationSource | undefined;
144
+ agent?: string | undefined;
145
+ updatedAt: string;
146
+ headSha: string;
147
+ previews: {
148
+ app: string;
149
+ label: string;
150
+ url: string;
151
+ }[];
152
+ } | {
153
+ previews: {
154
+ app: keyof import("@bridge-ds/core").PreviewApps;
155
+ label: string;
156
+ url: string | null;
157
+ state: import("@bridge-ds/core").PreviewState;
158
+ }[];
159
+ install: {
160
+ state: ExplorationTryIt["state"];
161
+ command: string | null;
162
+ note: string;
163
+ };
164
+ requests: {
165
+ number: number;
166
+ title: string;
167
+ url: string;
168
+ state: string;
169
+ }[];
170
+ number: number;
171
+ url: string;
172
+ branch: string;
173
+ slug: string;
174
+ name: string;
175
+ purpose: string;
176
+ useCase?: string | undefined;
177
+ status: import("@bridge-ds/core").ExplorationStatus;
178
+ draft: boolean;
179
+ author: string;
180
+ source?: ExplorationSource | undefined;
181
+ agent?: string | undefined;
182
+ updatedAt: string;
183
+ headSha: string;
184
+ })[];
103
185
  }>;
104
186
  /** `bridge explore promote`: ready for review, tier labels, exploration label and note removed, linked requests closed on merge. */
105
187
  export declare function promoteExploration(ctx: BridgeContext, a: {
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  VERSION,
4
4
  optionsFromArgv,
5
5
  serveStdio
6
- } from "./chunk-6JO2AG6X.js";
7
- import "./chunk-FJL6UTZG.js";
6
+ } from "./chunk-I3TL76GK.js";
7
+ import "./chunk-X455XY4E.js";
8
8
 
9
9
  // src/index.ts
10
10
  var argv = process.argv.slice(2);
package/dist/lib.d.ts CHANGED
@@ -11,6 +11,7 @@ export * as brand from './brand.js';
11
11
  export * as requests from './requests.js';
12
12
  export * as explorations from './explorations.js';
13
13
  export * as drafts from './drafts.js';
14
+ export { DOCS_DRAFT_MODEL, anthropicReplyText, anthropicRequest, formatDocsJson, mergeDocsDraft, missingProse, parseDocsReply } from '@bridge-ds/core';
14
15
  export interface ServeOptions extends ContextOptions {
15
16
  }
16
17
  /** Starts the stdio MCP server. Logs go to stderr: stdout belongs to the protocol. */