@bridge-ds/mcp 0.11.2 → 0.11.3

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.
@@ -1212,113 +1212,395 @@ function protectionSentence(state) {
1212
1212
  }
1213
1213
  }
1214
1214
 
1215
- // ../core/src/github-client.ts
1216
- var GitHubError = class extends Error {
1217
- constructor(status, message, detail, info = {}) {
1218
- super(message);
1219
- this.status = status;
1220
- this.detail = detail;
1221
- this.name = "GitHubError";
1222
- this.auth = info.auth;
1223
- this.permission = info.permission;
1224
- this.resetsAt = info.resetsAt;
1225
- }
1226
- status;
1227
- detail;
1228
- /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
1229
- auth;
1230
- /** With auth "missing-permission": the permission the call needed, when known. */
1231
- permission;
1232
- /** For a rate limit (status 429): when the limit resets, when known. */
1233
- resetsAt;
1234
- };
1235
- var PERMISSION_LABELS = {
1236
- issues: "Issues",
1237
- pull_requests: "Pull requests",
1238
- contents: "Contents",
1239
- checks: "Checks",
1240
- statuses: "Commit statuses",
1241
- metadata: "Metadata",
1242
- actions: "Actions",
1243
- deployments: "Deployments",
1244
- workflows: "Workflows",
1245
- administration: "Administration"
1215
+ // ../core/src/explorations.ts
1216
+ var EXPLORATION_LABEL = "exploration";
1217
+ var EXPLORATION_PREFIX = "explore/";
1218
+ var EXPLORATION_NOTES_DIR = ".bridge/explorations";
1219
+ var EXPLORATION_CHECK = "Exploration not merged as is";
1220
+ var EXPLORATION_READY_LABEL = "ready-for-devs";
1221
+ var EXPLORATION_ARCHIVED_LABEL = "archived";
1222
+ var EXPLORATION_USE_CASES = [
1223
+ { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype in a test version of a product, with the real components and tokens." },
1224
+ { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
1225
+ { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
1226
+ { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server inside this exploration, never in the library." },
1227
+ { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
1228
+ ];
1229
+ var EXPLORATION_STATUS_LABEL = {
1230
+ active: "Running",
1231
+ ready: "Waiting for code",
1232
+ building: "Being built",
1233
+ shipped: "Shipped",
1234
+ promoted: "Promoted to review",
1235
+ closed: "Closed"
1246
1236
  };
1247
- function permissionLabel(permission) {
1248
- return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
1237
+ var MARK = "bridge-exploration";
1238
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1239
+ var EXPLORATION_SLUG_MAX = 60;
1240
+ function explorationSlug(name) {
1241
+ const s = name.trim().replace(/^explore\//i, "");
1242
+ if (!/[a-z0-9]/i.test(s)) return "";
1243
+ return slugify(s, EXPLORATION_SLUG_MAX);
1249
1244
  }
1250
- var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
1251
- function missingPermissionMessage(permission, action) {
1252
- const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
1253
- if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
1254
- return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
1245
+ function explorationBranch(nameOrBranch) {
1246
+ return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
1255
1247
  }
1256
- function requiredPermission(path, acceptedHeader) {
1257
- if (acceptedHeader) {
1258
- const first2 = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
1259
- if (first2.length) return first2[0];
1248
+ function isExplorationBranch(ref) {
1249
+ const r = (ref ?? "").replace(/^refs\/heads\//, "");
1250
+ return r.startsWith(EXPLORATION_PREFIX) && SLUG_RE.test(r.slice(EXPLORATION_PREFIX.length));
1251
+ }
1252
+ var REPO_NAME = /^[\w.-]+\/[\w.-]+$/;
1253
+ var KEY_NAME = /^[a-z0-9][a-z0-9-]*$/i;
1254
+ function parseExplorationRef(input) {
1255
+ if (typeof input === "number") return { ref: input };
1256
+ const s = input.trim();
1257
+ const link = /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/i.exec(s);
1258
+ if (link) return { repo: `${link[1]}/${link[2]}`, ref: Number(link[3]) };
1259
+ if (/^#?\d+$/.test(s)) return { ref: Number(s.replace(/^#/, "")) };
1260
+ const hash = /^(.+)#(\d+)$/.exec(s);
1261
+ if (hash) {
1262
+ const where2 = hash[1].trim();
1263
+ if (REPO_NAME.test(where2)) return { repo: where2, ref: Number(hash[2]) };
1264
+ if (KEY_NAME.test(where2)) return { product: where2.toLowerCase(), ref: Number(hash[2]) };
1260
1265
  }
1261
- const p = path.replace(/[?#].*$/, "");
1262
- if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
1263
- if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
1264
- if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
1265
- if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
1266
- if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
1267
- if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
1268
- if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
1269
- if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
1270
- return void 0;
1266
+ const named = /^([a-z0-9][a-z0-9-]*):(.+)$/i.exec(s);
1267
+ if (named) return { product: named[1].toLowerCase(), ref: /^#?\d+$/.test(named[2].trim()) ? Number(named[2].trim().replace(/^#/, "")) : named[2].trim() };
1268
+ return { ref: s };
1271
1269
  }
1272
- var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
1273
- function classifyAuth(status, detail, opts = {}) {
1274
- if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
1275
- if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
1276
- if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
1277
- return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
1270
+ var explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
1271
+ var explorationNotePath = (slug3) => `${EXPLORATION_NOTES_DIR}/${slug3}.md`;
1272
+ function explorationProblems(e) {
1273
+ const out = [];
1274
+ const name = e.name.trim();
1275
+ const slug3 = e.slug?.trim() || explorationSlug(name);
1276
+ if (!name) out.push('Name the exploration, for example "Denser data tables".');
1277
+ else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
1278
+ else if (slug3.length < 2 || !SLUG_RE.test(slug3)) out.push("Use letters or digits in the name: it becomes the exploration's link name.");
1279
+ return out;
1280
+ }
1281
+ var useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
1282
+ function explorationNote(e) {
1283
+ const slug3 = e.slug || explorationSlug(e.name);
1284
+ const uc = useCaseLabel(e.useCase);
1285
+ return [
1286
+ `# Exploration: ${e.name.trim()}`,
1287
+ "",
1288
+ ...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
1289
+ `- Branch: \`${EXPLORATION_PREFIX}${slug3}\``,
1290
+ ...uc ? [`- Use case: ${uc}`] : [],
1291
+ ...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
1292
+ "",
1293
+ 'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
1294
+ ""
1295
+ ].join("\n");
1296
+ }
1297
+ function explorationPrTitle(e) {
1298
+ const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
1299
+ return `Exploration: ${n}`;
1300
+ }
1301
+ function explorationPrBody(e, ctx) {
1302
+ const slug3 = e.slug || explorationSlug(e.name);
1303
+ const branch = `${EXPLORATION_PREFIX}${slug3}`;
1304
+ const uc = useCaseLabel(e.useCase);
1305
+ const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
1306
+ const lines2 = [
1307
+ `## Exploration: ${e.name.trim()}`,
1308
+ "",
1309
+ `> Not mergeable as is. This draft pull request is a living prototype on \`${branch}\`: say "Done exploring" when the idea is ready for developers. Keep it open, so its preview link keeps working.`,
1310
+ "",
1311
+ ...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
1312
+ ...uc ? [`**Use case.** ${uc}`, ""] : [],
1313
+ "### What is experimental",
1314
+ "",
1315
+ ...experimental.length ? experimental.map((x) => `- ${x}`) : [ctx.inProduct ? `- The ${ctx.inProduct.name} screens and flows built on this branch with the library. Nothing here reaches main or production.` : "- Token edits and component work committed to this branch. Nothing here reaches main or production."],
1316
+ ...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
1317
+ ""
1318
+ ];
1319
+ if (ctx.inProduct) {
1320
+ const p = ctx.inProduct;
1321
+ lines2.push(
1322
+ "### Work on it",
1323
+ "",
1324
+ `This exploration lives in the ${p.name} repository, so it runs in your usual environment: \`git fetch origin && git switch ${branch}\`, then start ${p.name} as you always do.`,
1325
+ "",
1326
+ `The library comes from npm, as on main. To try library changes that are not released yet, install the snapshot of their branch in ${p.library} (\`npm i @bridge-ds/react@<snapshot>\`): the command is in its "Try this exploration in your product" comment. Library changes are made in ${p.library}, never on this branch.`,
1327
+ "",
1328
+ "### Previews",
1329
+ ""
1330
+ );
1331
+ const url = p.preview ? previewUrl(p.preview, { branch, ...ctx.pr ? { pr: ctx.pr } : {} }) : null;
1332
+ if (p.preview) lines2.push("| App | Preview |", "| --- | --- |", `| ${p.name} | ${url ?? "Link in the Vercel comment on this pull request"} |`);
1333
+ else lines2.push(`The preview deploys of ${p.name}, if it has them, build this branch like any other.`);
1334
+ } else {
1335
+ lines2.push(
1336
+ "### Try it in a product staging branch",
1337
+ "",
1338
+ 'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
1339
+ );
1340
+ lines2.push("", "### Previews", "");
1341
+ const links = resolveBranchPreviews(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
1342
+ if (links.length) lines2.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url ?? "Link in the Vercel comment on this pull request"} |`));
1343
+ else lines2.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
1278
1344
  }
1279
- return {};
1345
+ lines2.push(
1346
+ "",
1347
+ "### When it is done",
1348
+ "",
1349
+ `- **Done exploring:** on the exploration's page in Bridge Studio, or \`bridge explore done ${slug3}\`. Bridge removes the \`${EXPLORATION_LABEL}\` label and the exploration note, adds \`${EXPLORATION_READY_LABEL}\`, and sends the pieces the designer picks to the DS lead as requests. Nothing is closed or merged: developers continue on \`${branch}\`.`,
1350
+ `- **Archive:** \`bridge explore archive ${slug3}\` adds the \`${EXPLORATION_ARCHIVED_LABEL}\` label. Keep this pull request open: ${ctx.inProduct ? "Bridge follows open explorations, and its preview link is how people come back to the prototype." : "closing it removes the preview prerelease."}`,
1351
+ "",
1352
+ explorationBlock({ ...e, slug: slug3 })
1353
+ );
1354
+ return lines2.join("\n");
1280
1355
  }
1281
- function memoryEtagStore(max = 500) {
1282
- const map = /* @__PURE__ */ new Map();
1283
- return {
1284
- get(key) {
1285
- const e = map.get(key);
1286
- if (e) {
1287
- map.delete(key);
1288
- map.set(key, e);
1289
- }
1290
- return e;
1291
- },
1292
- set(key, entry) {
1293
- map.delete(key);
1294
- map.set(key, entry);
1295
- while (map.size > max) map.delete(map.keys().next().value);
1296
- },
1297
- clear: () => map.clear()
1356
+ function explorationBlock(e) {
1357
+ const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
1358
+ const data = {
1359
+ name: e.name.trim(),
1360
+ purpose: (e.purpose ?? "").trim(),
1361
+ slug: e.slug || explorationSlug(e.name),
1362
+ ...e.useCase ? { useCase: e.useCase } : {},
1363
+ ...experimental.length ? { experimental } : {},
1364
+ ...e.author ? { author: e.author } : {},
1365
+ ...e.startedAt ? { startedAt: e.startedAt } : {},
1366
+ ...e.source ? { source: e.source } : {},
1367
+ ...e.agent ? { agent: e.agent } : {},
1368
+ ...e.doneAt ? { doneAt: e.doneAt } : {},
1369
+ ...e.doneSha ? { doneSha: e.doneSha } : {}
1298
1370
  };
1371
+ return `<!-- ${MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
1299
1372
  }
1300
- var sharedEtags = memoryEtagStore();
1301
- function tokenFingerprint(token) {
1302
- if (!token) return "anon";
1303
- let a = 2166136261;
1304
- let b = 16777619 ^ token.length;
1305
- for (let i = 0; i < token.length; i++) {
1306
- const c = token.charCodeAt(i);
1307
- a = Math.imul(a ^ c, 16777619) >>> 0;
1308
- b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
1373
+ function parseExplorationBody(body) {
1374
+ if (!body) return void 0;
1375
+ const m = new RegExp(`<!-- ${MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
1376
+ if (!m) return void 0;
1377
+ try {
1378
+ const e = JSON.parse(m[1]);
1379
+ if (!e || typeof e.name !== "string") return void 0;
1380
+ return typeof e.purpose === "string" ? e : e.purpose === void 0 ? { ...e, purpose: "" } : void 0;
1381
+ } catch {
1382
+ return void 0;
1309
1383
  }
1310
- return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
1311
1384
  }
1312
- function installationFor(list, owner) {
1313
- const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
1314
- if (!inst) return { reach: "no" };
1315
- const selection = inst.repositorySelection ?? inst.repository_selection;
1316
- return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
1385
+ var labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.name ?? "");
1386
+ function isExplorationPull(p) {
1387
+ return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
1317
1388
  }
1318
- var MAX_PAGES = 10;
1319
- function rateLimitMessage(x) {
1320
- const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
1321
- if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
1389
+ function isUnlabeledExploration(p) {
1390
+ if (p.state !== "open" || p.draft !== true) return false;
1391
+ const names4 = labelNames(p.labels ?? []);
1392
+ if (names4.includes(EXPLORATION_LABEL) || names4.includes(EXPLORATION_ARCHIVED_LABEL) || names4.includes(EXPLORATION_READY_LABEL)) return false;
1393
+ const block = parseExplorationBody(p.body);
1394
+ if (block?.doneAt) return false;
1395
+ return isExplorationBranch(p.headRef) || Boolean(block);
1396
+ }
1397
+ function isHandedOff(p) {
1398
+ return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
1399
+ }
1400
+ function explorationStatus(p) {
1401
+ const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
1402
+ if (p.state !== "open" && !merged) return "closed";
1403
+ if (p.state === "open" && (labelNames(p.labels).includes(EXPLORATION_LABEL) || isUnlabeledExploration(p))) return "active";
1404
+ if (!isHandedOff(p)) return "promoted";
1405
+ if (merged) return "shipped";
1406
+ if (p.draft === false) return "building";
1407
+ const doneSha = parseExplorationBody(p.body)?.doneSha;
1408
+ if (doneSha && p.headSha && p.headSha !== doneSha) return "building";
1409
+ return "ready";
1410
+ }
1411
+ function isGuardedExploration(p) {
1412
+ if (p.state !== void 0 && p.state !== "open") return false;
1413
+ if (labelNames(p.labels ?? []).includes(EXPLORATION_LABEL)) return true;
1414
+ return isUnlabeledExploration({ state: "open", draft: p.draft, labels: p.labels, headRef: p.headRef ?? p.head?.ref, body: p.body });
1415
+ }
1416
+ var isExplorationCheck = (name) => (name ?? "").trim() === EXPLORATION_CHECK;
1417
+ var isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
1418
+ var isArchivedExploration = (labels) => labelNames(labels ?? []).includes(EXPLORATION_ARCHIVED_LABEL);
1419
+ function isHeldExploration(p) {
1420
+ if (p.state !== void 0 && p.state !== "open") return false;
1421
+ const names4 = labelNames(p.labels ?? []);
1422
+ if (names4.includes(EXPLORATION_LABEL) || names4.includes(EXPLORATION_ARCHIVED_LABEL)) return true;
1423
+ if (isUnlabeledExploration({ state: "open", draft: p.draft, labels: p.labels, headRef: p.headRef ?? p.head?.ref, body: p.body })) return true;
1424
+ return names4.includes(EXPLORATION_READY_LABEL) && p.draft !== false;
1425
+ }
1426
+ var explorationTakeCommands = (branch) => ["git fetch origin", `git checkout ${branch}`];
1427
+ var CONTINUE = "Continue on this branch: its pull request already exists, so Bridge follows your pushes, and the merge reads as shipped.";
1428
+ var EXPLORATION_STARTING_POINT = `A starting point, not ready to ship: nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
1429
+ function explorationStartingPoint(checks) {
1430
+ switch (checks) {
1431
+ case "success":
1432
+ return `A starting point: its checks pass, but nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
1433
+ case "failure":
1434
+ return `A starting point, not ready to ship: some checks fail on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
1435
+ case "pending":
1436
+ return `A starting point, not ready to ship: its checks are still running and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
1437
+ case "none":
1438
+ return `A starting point, not ready to ship: no checks ran on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
1439
+ default:
1440
+ return EXPLORATION_STARTING_POINT;
1441
+ }
1442
+ }
1443
+ function doneExploringPlan(input) {
1444
+ const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
1445
+ return {
1446
+ removeLabels: [EXPLORATION_LABEL],
1447
+ addLabels: [EXPLORATION_READY_LABEL],
1448
+ removeFiles,
1449
+ body: withDoneExploring(input.body, { branch: input.branch, doneAt: (input.date ?? /* @__PURE__ */ new Date()).toISOString() }),
1450
+ take: explorationTakeCommands(input.branch),
1451
+ markReady: false
1452
+ };
1453
+ }
1454
+ var DEV_SECTION = "### For developers";
1455
+ function withDoneExploring(body, done) {
1456
+ const e = parseExplorationBody(body);
1457
+ const [fetch2, checkout] = explorationTakeCommands(done.branch);
1458
+ const section = [
1459
+ DEV_SECTION,
1460
+ "",
1461
+ `**Done exploring on ${done.doneAt.slice(0, 10)}.** ${explorationStartingPoint(done.checks)}`,
1462
+ "",
1463
+ "```sh",
1464
+ fetch2,
1465
+ checkout,
1466
+ "```",
1467
+ "",
1468
+ "Mark this pull request ready for review when the code is yours. Keep it open: its preview link is how designers, the DS lead and stakeholders come back to the prototype."
1469
+ ].join("\n");
1470
+ const block = new RegExp(`<!-- ${MARK} \\{[\\s\\S]*?\\} -->`);
1471
+ const kept = (body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/### For developers[\s\S]*?(?=### |<!-- |$)/, "");
1472
+ if (!e || !block.test(kept)) return `${kept.trim()}${kept.trim() ? "\n\n" : ""}${section}`;
1473
+ const at = kept.search(block);
1474
+ const nextBlock = explorationBlock({ ...e, doneAt: done.doneAt, ...done.doneSha ? { doneSha: done.doneSha } : {} });
1475
+ const after = kept.slice(at).replace(block, () => nextBlock).trim();
1476
+ return `${kept.slice(0, at).trim()}
1477
+
1478
+ ${section}
1479
+
1480
+ ${after}`.trim();
1481
+ }
1482
+ function explorationTryIt(input) {
1483
+ const c = readPreviewComment(input.comment);
1484
+ if (!c) return { state: "building" };
1485
+ if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
1486
+ if (!c.command || c.source === "none") return { state: "failed" };
1487
+ return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
1488
+ }
1489
+ function explorationRules(branch) {
1490
+ return [
1491
+ `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
1492
+ `- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It stays open as a living prototype.`,
1493
+ `- Do not mark the pull request ready for review, do not remove the exploration label and never close it: "Done exploring" is a person's decision, and closing removes its preview.`
1494
+ ];
1495
+ }
1496
+
1497
+ // ../core/src/github-client.ts
1498
+ var GitHubError = class extends Error {
1499
+ constructor(status, message, detail, info = {}) {
1500
+ super(message);
1501
+ this.status = status;
1502
+ this.detail = detail;
1503
+ this.name = "GitHubError";
1504
+ this.auth = info.auth;
1505
+ this.permission = info.permission;
1506
+ this.resetsAt = info.resetsAt;
1507
+ }
1508
+ status;
1509
+ detail;
1510
+ /** Set when the failure is about the token: rejected (401) or missing a permission (403). */
1511
+ auth;
1512
+ /** With auth "missing-permission": the permission the call needed, when known. */
1513
+ permission;
1514
+ /** For a rate limit (status 429): when the limit resets, when known. */
1515
+ resetsAt;
1516
+ };
1517
+ var PERMISSION_LABELS = {
1518
+ issues: "Issues",
1519
+ pull_requests: "Pull requests",
1520
+ contents: "Contents",
1521
+ checks: "Checks",
1522
+ statuses: "Commit statuses",
1523
+ metadata: "Metadata",
1524
+ actions: "Actions",
1525
+ deployments: "Deployments",
1526
+ workflows: "Workflows",
1527
+ administration: "Administration"
1528
+ };
1529
+ function permissionLabel(permission) {
1530
+ return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
1531
+ }
1532
+ var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
1533
+ function missingPermissionMessage(permission, action) {
1534
+ const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
1535
+ if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
1536
+ return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
1537
+ }
1538
+ function requiredPermission(path, acceptedHeader) {
1539
+ if (acceptedHeader) {
1540
+ const first2 = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
1541
+ if (first2.length) return first2[0];
1542
+ }
1543
+ const p = path.replace(/[?#].*$/, "");
1544
+ if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
1545
+ if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
1546
+ if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
1547
+ if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
1548
+ if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
1549
+ if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
1550
+ if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
1551
+ if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
1552
+ return void 0;
1553
+ }
1554
+ var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
1555
+ function classifyAuth(status, detail, opts = {}) {
1556
+ if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
1557
+ if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
1558
+ if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
1559
+ return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
1560
+ }
1561
+ return {};
1562
+ }
1563
+ function memoryEtagStore(max = 500) {
1564
+ const map = /* @__PURE__ */ new Map();
1565
+ return {
1566
+ get(key) {
1567
+ const e = map.get(key);
1568
+ if (e) {
1569
+ map.delete(key);
1570
+ map.set(key, e);
1571
+ }
1572
+ return e;
1573
+ },
1574
+ set(key, entry) {
1575
+ map.delete(key);
1576
+ map.set(key, entry);
1577
+ while (map.size > max) map.delete(map.keys().next().value);
1578
+ },
1579
+ clear: () => map.clear()
1580
+ };
1581
+ }
1582
+ var sharedEtags = memoryEtagStore();
1583
+ function tokenFingerprint(token) {
1584
+ if (!token) return "anon";
1585
+ let a = 2166136261;
1586
+ let b = 16777619 ^ token.length;
1587
+ for (let i = 0; i < token.length; i++) {
1588
+ const c = token.charCodeAt(i);
1589
+ a = Math.imul(a ^ c, 16777619) >>> 0;
1590
+ b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
1591
+ }
1592
+ return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
1593
+ }
1594
+ function installationFor(list, owner) {
1595
+ const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
1596
+ if (!inst) return { reach: "no" };
1597
+ const selection = inst.repositorySelection ?? inst.repository_selection;
1598
+ return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
1599
+ }
1600
+ var MAX_PAGES = 10;
1601
+ function rateLimitMessage(x) {
1602
+ const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
1603
+ if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
1322
1604
  const hint = x.tokenHint ?? "add a GitHub token";
1323
1605
  return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
1324
1606
  }
@@ -2020,8 +2302,11 @@ var GitHubClient = class _GitHubClient {
2020
2302
  const r = await this.request("GET", `${this.repoPath}/pulls?state=${state}&per_page=50&sort=updated&direction=desc`);
2021
2303
  return r.map(toSummary);
2022
2304
  }
2023
- /** Commit statuses and check runs of a commit, folded into one state. */
2024
- async getChecks(sha) {
2305
+ /**
2306
+ * Commit statuses and check runs of a commit, folded into one state. `exploration`: the commit heads an exploration
2307
+ * (explorations isGuardedExploration), so its guard check, failing by design, is left out (as review-gate summarizeCi).
2308
+ */
2309
+ async getChecks(sha, opts = {}) {
2025
2310
  const [combined, runs] = await Promise.all([
2026
2311
  this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/status`).catch(() => ({ statuses: [] })),
2027
2312
  this.listCheckRuns(sha).catch(() => [])
@@ -2029,7 +2314,7 @@ var GitHubClient = class _GitHubClient {
2029
2314
  const checks = [
2030
2315
  ...runs.map((c) => ({ name: c.name, status: c.status, conclusion: c.conclusion })),
2031
2316
  ...combined.statuses.map((s) => ({ name: s.context, status: s.state === "pending" ? "in_progress" : "completed", conclusion: s.state === "pending" ? null : s.state }))
2032
- ];
2317
+ ].filter((c) => !(opts.exploration && isExplorationCheck(c.name)));
2033
2318
  const failing = checks.some((c) => c.conclusion && !["success", "neutral", "skipped"].includes(c.conclusion));
2034
2319
  const pending = checks.some((c) => c.status !== "completed");
2035
2320
  return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
@@ -3483,641 +3768,366 @@ function composeNewBrandBody(plan, opts) {
3483
3768
  `- ${code("bridge.config.json")} is a guardrail file`,
3484
3769
  `- Semver bump: ${code(plan.risk.bump)} for ${code("@bridge-ds/tokens")}`,
3485
3770
  "",
3486
- "## Checks",
3487
- "",
3488
- `- Token validation: ${errors.length ? `**${errors.length} error(s)**` : "no errors"} (DTCG shapes, aliases, every mode)${warnings.length ? `, ${warnings.length} warning(s): ${warnings.map((w) => w.message).join(" ")}` : ""}`,
3489
- `- Contrast gate for ${plan.name}: ${plan.contrast.failing.length ? `**${plan.contrast.failing.length} failing pair(s)**` : `all ${plan.contrast.checks} pairs pass`}`,
3490
- "",
3491
- "## Authored by",
3492
- "",
3493
- opts.agent ? "- [ ] Human" : "- [x] Human",
3494
- opts.agent ? "- [x] AI agent (Claude), reviewed by a human before merge" : "- [ ] AI agent",
3495
- "",
3496
- "## Design review checklist",
3497
- "",
3498
- "- [ ] Checked in light and dark, with real components (Studio preview or Storybook brand switch)",
3499
- "- [ ] Primary, zone and link colors read as one family",
3500
- "- [ ] Changeset bump matches the tier",
3501
- ""
3502
- ].join("\n");
3503
- }
3504
-
3505
- // ../core/src/docs-draft.ts
3506
- var DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
3507
- var PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
3508
- var ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
3509
- function anatomyLabel(className) {
3510
- if (ACRONYMS.has(className)) return `Size ${className}`;
3511
- const words = className.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().toLowerCase();
3512
- if (!words) return className;
3513
- if (words === "root") return "Container";
3514
- return words.charAt(0).toUpperCase() + words.slice(1);
3515
- }
3516
- function cssModuleClasses(css) {
3517
- const clean = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
3518
- const out = [];
3519
- for (const m of clean.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) {
3520
- const before = clean[m.index - 1] ?? " ";
3521
- if (/[\w)]/.test(before)) continue;
3522
- if (!out.includes(m[1])) out.push(m[1]);
3523
- }
3524
- return out;
3525
- }
3526
- var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
3527
- function propsTable(name, props, inherits) {
3528
- if (!props.length) return inherits ? `${name} has no props of its own. It accepts every native <${inherits}> attribute.` : "";
3529
- const rows = props.map((p) => `| \`${p.name}\`${p.required ? " (required)" : ""} | \`${cell(p.type)}\` | ${p.defaultValue ? `\`${cell(p.defaultValue)}\`` : ""} | ${cell(p.description ?? "")} |`);
3530
- const tail = inherits ? `
3531
-
3532
- Also accepts every native <${inherits}> attribute.` : "";
3533
- return ["| Prop | Type | Default | Description |", "| --- | --- | --- | --- |", ...rows].join("\n") + tail;
3534
- }
3535
- var isEmpty = (v) => v === void 0 || v === null || typeof v === "string" && !v.trim() || Array.isArray(v) && v.length === 0;
3536
- function missingProse(d) {
3537
- return PROSE_KEYS.filter((k) => isEmpty(d?.[k]));
3538
- }
3539
- function prefillDocs(input) {
3540
- const existing = input.existing ?? null;
3541
- const anatomy = {};
3542
- for (const c of input.cssClasses) anatomy[c] = existing?.anatomy?.[c] ?? anatomyLabel(c);
3543
- const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p) => `${p.foreground} on ${p.background}: ${p.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p.min}:1 (${p.ratio >= p.min ? "passes" : "fails"}).`);
3544
- const missing = missingProse(existing);
3545
- const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
3546
- const draft = {
3547
- summary: existing?.summary ?? input.description ?? "",
3548
- ...existing?.category ? { category: existing.category } : {},
3549
- ...existing?.status ? { status: existing.status } : {},
3550
- ...existing?.preview ? { preview: existing.preview } : {},
3551
- whenToUse: existing?.whenToUse ?? [],
3552
- whenNotToUse: existing?.whenNotToUse ?? [],
3553
- dos: existing?.dos ?? [],
3554
- donts: existing?.donts ?? [],
3555
- accessibility: existing?.accessibility ?? [],
3556
- contentGuidelines: existing?.contentGuidelines ?? [],
3557
- anatomy,
3558
- ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
3559
- ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
3560
- ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
3561
- };
3562
- return {
3563
- component: input.name,
3564
- propsTable: propsTable(input.name, input.props, input.inherits),
3565
- tokens: [...new Set(input.tokens)].sort(),
3566
- inheritedTokens: input.inheritedTokens ?? {},
3567
- anatomy,
3568
- contrastNotes,
3569
- changelogLine,
3570
- missing,
3571
- draft
3572
- };
3573
- }
3574
- var SECTION_SHAPE = {
3575
- summary: '"summary": "one or two sentences"',
3576
- whenToUse: '"whenToUse": ["..."]',
3577
- whenNotToUse: '"whenNotToUse": ["..."]',
3578
- dos: '"dos": [{ "title": "...", "text": "..." }]',
3579
- donts: '"donts": [{ "title": "...", "text": "..." }]',
3580
- accessibility: '"accessibility": ["..."]',
3581
- contentGuidelines: '"contentGuidelines": ["..."]'
3582
- };
3583
- var docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
3584
- function docsPrompt(prefill, conventions = {}, options = {}) {
3585
- const facts = [
3586
- `Component: ${prefill.component}`,
3587
- prefill.propsTable ? `Props (parsed from TypeScript):
3588
- ${prefill.propsTable}` : "Props: none of its own.",
3589
- `Tokens its stylesheet reads: ${prefill.tokens.join(", ") || "none"}`,
3590
- ...Object.entries(prefill.inheritedTokens).map(([from, list]) => `Tokens it reads through ${from}'s stylesheet: ${list.length}`),
3591
- `Anatomy (CSS module classes): ${Object.entries(prefill.anatomy).map(([k, v]) => `${k} = ${v}`).join("; ") || "none"}`,
3592
- prefill.contrastNotes.length ? `Contrast checks:
3593
- - ${prefill.contrastNotes.join("\n- ")}` : "",
3594
- `Changelog line: ${prefill.changelogLine}`
3595
- ].filter(Boolean);
3596
- const block = (title, text) => text?.trim() ? `<${title}>
3597
- ${text.trim()}
3598
- </${title}>` : "";
3599
- const keys = options.keys;
3600
- const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
3601
- const write = keys ? keys.filter((k) => !check.includes(k)) : [];
3602
- const task = keys ? [
3603
- ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
3604
- ...check.length ? [
3605
- `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
3606
- "Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
3607
- "Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
3608
- ] : [],
3609
- ...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
3610
- ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
3611
- const shape2 = keys ? [
3612
- `- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
3613
- `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
3614
- ] : [
3615
- `- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
3616
- "- Answer with the full docs.json as one JSON object, nothing else."
3617
- ];
3618
- return [
3619
- ...task,
3620
- "",
3621
- "Rules:",
3622
- "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
3623
- "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
3624
- '- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
3625
- ...shape2,
3626
- "",
3627
- "Facts computed from the code:",
3628
- facts.join("\n\n"),
3629
- "",
3630
- `Current draft:
3631
- ${JSON.stringify(prefill.draft, null, 2)}`,
3632
- "",
3633
- block("source", conventions.source),
3634
- block("styles", conventions.styles),
3635
- block("agents-md", conventions.agents),
3636
- block("voice", conventions.voice),
3637
- block("example-docs-json", conventions.example)
3638
- ].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
3639
- }
3640
- function markClaudeDraft(d, sections2) {
3641
- if (!sections2) {
3642
- const { draftedSections: _all, ...rest } = d;
3643
- return { ...rest, draftedBy: "claude", needsReview: true };
3644
- }
3645
- const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
3646
- const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections2.includes(k));
3647
- return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
3648
- }
3649
- function formatDocsJson(d) {
3650
- const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
3651
- const out = {};
3652
- for (const k of order) if (d[k] !== void 0) out[k] = d[k];
3653
- for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
3654
- return `${JSON.stringify(out, null, 2)}
3655
- `;
3656
- }
3657
- function patchNode(old, next) {
3658
- if (plainEqual(toPlain(old), next)) return old;
3659
- if (old.kind === "object" && next && typeof next === "object" && !Array.isArray(next)) {
3660
- const n = next;
3661
- const kept = old.entries.filter((e) => n[e.key] !== void 0).map((e) => ({ key: e.key, value: patchNode(e.value, n[e.key]) }));
3662
- const added = Object.keys(n).filter((k) => n[k] !== void 0 && !getEntry(old, k)).map((k) => ({ key: k, value: fromPlain(n[k], void 0, old.inline) }));
3663
- return { kind: "object", inline: old.inline, entries: [...kept, ...added] };
3664
- }
3665
- if (old.kind === "array" && Array.isArray(next)) {
3666
- return { kind: "array", inline: old.inline, items: next.map((v, i) => old.items[i] ? patchNode(old.items[i], v) : fromPlain(v, old.items[0], old.inline)) };
3667
- }
3668
- return fromPlain(next, old);
3669
- }
3670
- function patchDocsJson(original, next) {
3671
- const plain2 = JSON.parse(formatDocsJson(next));
3672
- if (!original) return formatDocsJson(next);
3673
- try {
3674
- const doc = parseJson(original);
3675
- if (doc.root.kind !== "object") return formatDocsJson(next);
3676
- return serializeJson({ ...doc, root: patchNode(doc.root, plain2) });
3677
- } catch {
3678
- return formatDocsJson(next);
3679
- }
3680
- }
3681
-
3682
- // ../core/src/change-drafts.ts
3683
- function cleanDraftText(text) {
3684
- return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
3685
- }
3686
- var andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
3687
- var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
3688
- var VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
3689
- function shortVariant(key) {
3690
- const pairs = key.split(", ").map((p) => {
3691
- const at = p.indexOf("=");
3692
- return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
3693
- });
3694
- const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
3695
- if (!kept.length) return key;
3696
- const rank2 = (k) => {
3697
- const i = VARIANT_FIRST.indexOf(k.toLowerCase());
3698
- return i < 0 ? VARIANT_FIRST.length : i;
3699
- };
3700
- return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank2(a.k) - rank2(b.k) || a.i - b.i).map((p) => p.v).join(" ");
3701
- }
3702
- var shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
3703
- var DRAFT_LINE_MAX = 120;
3704
- function clipText(text, max) {
3705
- const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
3706
- if (t.length <= max) return t;
3707
- const cut2 = t.slice(0, max + 1);
3708
- const at = cut2.lastIndexOf(" ");
3709
- return (at > max * 0.5 ? cut2.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
3710
- }
3711
- function sanitizeDraftLine(raw) {
3712
- if (typeof raw !== "string") return "";
3713
- return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
3714
- }
3715
-
3716
- // ../core/src/docs-ai.ts
3717
- var DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
3718
- var ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
3719
- var DOCS_SELECTION_MODES = {
3720
- clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
3721
- shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
3722
- voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
3723
- };
3724
- var isDocsSelectionMode = (m) => typeof m === "string" && m in DOCS_SELECTION_MODES;
3725
- var isProseKey = (k) => typeof k === "string" && PROSE_KEYS.includes(k);
3726
- function validateDocsTask(x) {
3727
- if (!x || typeof x !== "object") return ["The docs task is missing."];
3728
- const t = x;
3729
- if (t.kind === "sections") {
3730
- const keys = t.keys;
3731
- if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
3732
- if (t.check !== void 0 && (!Array.isArray(t.check) || !t.check.every(isProseKey))) return ["check must list prose sections."];
3733
- return [];
3734
- }
3735
- if (t.kind === "selection") {
3736
- const out = [];
3737
- if (!isProseKey(t.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
3738
- if (typeof t.text !== "string" || !t.text.trim()) out.push("The selection has no text.");
3739
- if (!isDocsSelectionMode(t.mode)) out.push("The selection mode must be clearer, shorter or voice.");
3740
- return out;
3741
- }
3742
- return ['The docs task kind must be "sections" or "selection".'];
3743
- }
3744
- var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
3745
- function parseDocsReply(text) {
3746
- const fenced2 = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
3747
- const body = fenced2 ? fenced2[1] : text;
3748
- const start = body.indexOf("{");
3749
- const end = body.lastIndexOf("}");
3750
- if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
3751
- const json = JSON.parse(body.slice(start, end + 1));
3752
- if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
3753
- return json;
3754
- }
3755
- var cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
3756
- var isExample = (x) => {
3757
- if (typeof x === "string") return true;
3758
- if (!x || typeof x !== "object" || Array.isArray(x)) return false;
3759
- const n = x;
3760
- if (typeof n.c !== "string" || !n.c) return false;
3761
- if (n.p !== void 0 && (!n.p || typeof n.p !== "object" || Array.isArray(n.p))) return false;
3762
- return n.children === void 0 || Array.isArray(n.children) && n.children.every(isExample);
3763
- };
3764
- var textList = (x) => {
3765
- if (typeof x === "string") x = [x];
3766
- if (!Array.isArray(x)) return void 0;
3767
- const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
3768
- return out.length ? out : void 0;
3769
- };
3770
- var guidanceList = (x, examples) => {
3771
- if (!Array.isArray(x)) return void 0;
3772
- const out = [];
3773
- for (const g of x) {
3774
- if (!g || typeof g !== "object") continue;
3775
- const r = g;
3776
- const title = typeof r.title === "string" ? cleanDocsText(r.title) : "";
3777
- const text = typeof r.text === "string" ? cleanDocsText(r.text) : "";
3778
- if (!title || !text) continue;
3779
- out.push({ title, text, ...examples && r.example !== void 0 && isExample(r.example) ? { example: r.example } : {} });
3780
- }
3781
- return out.length ? out : void 0;
3782
- };
3783
- function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
3784
- const out = {};
3785
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
3786
- const r = raw;
3787
- for (const k of keys) {
3788
- const v = r[k];
3789
- if (k === "summary") {
3790
- const s = typeof v === "string" ? cleanDocsText(v) : "";
3791
- if (s) out.summary = s;
3792
- } else if (k === "dos" || k === "donts") {
3793
- const g = guidanceList(v, opts.examples ?? false);
3794
- if (g) out[k] = g;
3795
- } else {
3796
- const l = textList(v);
3797
- if (l) out[k] = l;
3798
- }
3799
- }
3800
- return out;
3801
- }
3802
- function sanitizeDocsReplacement(raw) {
3803
- if (typeof raw !== "string") return "";
3804
- return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
3805
- }
3806
- function docsSectionsAnswer(task, raw) {
3807
- const check = (task.check ?? []).filter((k) => task.keys.includes(k));
3808
- const sections2 = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
3809
- return { sections: sections2, checked: check.filter((k) => sections2[k] === void 0) };
3810
- }
3811
- function docsSectionsResult(task, raw) {
3812
- const sections2 = sanitizeDocsSections(raw, task.keys);
3813
- if (!Object.keys(sections2).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
3814
- return sections2;
3815
- }
3816
- function mergeDocsDraft(prefillDraft, reply) {
3817
- const empty = PROSE_KEYS.filter((k) => {
3818
- const v = prefillDraft[k];
3819
- return v === void 0 || (typeof v === "string" ? !v.trim() : Array.isArray(v) && !v.length);
3820
- });
3821
- const sections2 = sanitizeDocsSections(reply, empty, { examples: true });
3822
- const merged = { ...prefillDraft, ...sections2 };
3823
- if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
3824
- merged.anatomy = prefillDraft.anatomy;
3825
- const drafted = PROSE_KEYS.filter((k) => sections2[k] !== void 0);
3826
- return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
3827
- }
3828
- function anthropicRequest(input) {
3829
- const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
3830
- if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
3831
- if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
3832
- const body = {
3833
- model: input.model || DOCS_DRAFT_MODEL,
3834
- max_tokens: input.maxTokens ?? 16e3,
3835
- ...input.fallback ? { fallbacks: "default" } : {},
3836
- ...input.system ? { system: input.system } : {},
3837
- messages: [{ role: "user", content: input.prompt }]
3838
- };
3839
- return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
3840
- }
3841
- function anthropicReplyText(msg) {
3842
- const m = msg ?? {};
3843
- if (m.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
3844
- if (m.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
3845
- return (m.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
3846
- }
3847
-
3848
- // ../core/src/explorations.ts
3849
- var EXPLORATION_LABEL = "exploration";
3850
- var EXPLORATION_PREFIX = "explore/";
3851
- var EXPLORATION_NOTES_DIR = ".bridge/explorations";
3852
- var EXPLORATION_READY_LABEL = "ready-for-devs";
3853
- var EXPLORATION_ARCHIVED_LABEL = "archived";
3854
- var EXPLORATION_USE_CASES = [
3855
- { value: "user-test", label: "Prototype for a user test", hint: "A coded prototype in a test version of a product, with the real components and tokens." },
3856
- { value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
3857
- { value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
3858
- { value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server inside this exploration, never in the library." },
3859
- { value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
3860
- ];
3861
- var EXPLORATION_STATUS_LABEL = {
3862
- active: "Running",
3863
- ready: "Waiting for code",
3864
- building: "Being built",
3865
- shipped: "Shipped",
3866
- promoted: "Promoted to review",
3867
- closed: "Closed"
3868
- };
3869
- var MARK = "bridge-exploration";
3870
- var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3871
- var EXPLORATION_SLUG_MAX = 60;
3872
- function explorationSlug(name) {
3873
- const s = name.trim().replace(/^explore\//i, "");
3874
- if (!/[a-z0-9]/i.test(s)) return "";
3875
- return slugify(s, EXPLORATION_SLUG_MAX);
3876
- }
3877
- function explorationBranch(nameOrBranch) {
3878
- return `${EXPLORATION_PREFIX}${explorationSlug(nameOrBranch)}`;
3879
- }
3880
- function isExplorationBranch(ref) {
3881
- const r = (ref ?? "").replace(/^refs\/heads\//, "");
3882
- return r.startsWith(EXPLORATION_PREFIX) && SLUG_RE.test(r.slice(EXPLORATION_PREFIX.length));
3883
- }
3884
- var REPO_NAME = /^[\w.-]+\/[\w.-]+$/;
3885
- var KEY_NAME = /^[a-z0-9][a-z0-9-]*$/i;
3886
- function parseExplorationRef(input) {
3887
- if (typeof input === "number") return { ref: input };
3888
- const s = input.trim();
3889
- const link = /^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/i.exec(s);
3890
- if (link) return { repo: `${link[1]}/${link[2]}`, ref: Number(link[3]) };
3891
- if (/^#?\d+$/.test(s)) return { ref: Number(s.replace(/^#/, "")) };
3892
- const hash = /^(.+)#(\d+)$/.exec(s);
3893
- if (hash) {
3894
- const where2 = hash[1].trim();
3895
- if (REPO_NAME.test(where2)) return { repo: where2, ref: Number(hash[2]) };
3896
- if (KEY_NAME.test(where2)) return { product: where2.toLowerCase(), ref: Number(hash[2]) };
3897
- }
3898
- const named = /^([a-z0-9][a-z0-9-]*):(.+)$/i.exec(s);
3899
- if (named) return { product: named[1].toLowerCase(), ref: /^#?\d+$/.test(named[2].trim()) ? Number(named[2].trim().replace(/^#/, "")) : named[2].trim() };
3900
- return { ref: s };
3901
- }
3902
- var explorationRefLabel = (x) => x.repo && x.product ? `${x.product}#${x.number}` : `#${x.number}`;
3903
- var explorationNotePath = (slug3) => `${EXPLORATION_NOTES_DIR}/${slug3}.md`;
3904
- function explorationProblems(e) {
3905
- const out = [];
3906
- const name = e.name.trim();
3907
- const slug3 = e.slug?.trim() || explorationSlug(name);
3908
- if (!name) out.push('Name the exploration, for example "Denser data tables".');
3909
- else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
3910
- else if (slug3.length < 2 || !SLUG_RE.test(slug3)) out.push("Use letters or digits in the name: it becomes the exploration's link name.");
3911
- return out;
3912
- }
3913
- var useCaseLabel = (u) => EXPLORATION_USE_CASES.find((x) => x.value === u)?.label;
3914
- function explorationNote(e) {
3915
- const slug3 = e.slug || explorationSlug(e.name);
3916
- const uc = useCaseLabel(e.useCase);
3917
- return [
3918
- `# Exploration: ${e.name.trim()}`,
3771
+ "## Checks",
3919
3772
  "",
3920
- ...e.purpose?.trim() ? [e.purpose.trim(), ""] : [],
3921
- `- Branch: \`${EXPLORATION_PREFIX}${slug3}\``,
3922
- ...uc ? [`- Use case: ${uc}`] : [],
3923
- ...e.startedAt ? [`- Started: ${e.startedAt.slice(0, 10)}${e.author ? ` by @${e.author}` : ""}`] : [],
3773
+ `- Token validation: ${errors.length ? `**${errors.length} error(s)**` : "no errors"} (DTCG shapes, aliases, every mode)${warnings.length ? `, ${warnings.length} warning(s): ${warnings.map((w) => w.message).join(" ")}` : ""}`,
3774
+ `- Contrast gate for ${plan.name}: ${plan.contrast.failing.length ? `**${plan.contrast.failing.length} failing pair(s)**` : `all ${plan.contrast.checks} pairs pass`}`,
3924
3775
  "",
3925
- 'This file marks the branch as an exploration. It is never merged as is: "Done exploring" removes it before developers take the branch.',
3926
- ""
3927
- ].join("\n");
3928
- }
3929
- function explorationPrTitle(e) {
3930
- const n = e.name.trim().replace(/\.$/, "").replace(/^exploration\s*:\s*/i, "").replace(/\s*:\s+/g, ", ").trim();
3931
- return `Exploration: ${n}`;
3932
- }
3933
- function explorationPrBody(e, ctx) {
3934
- const slug3 = e.slug || explorationSlug(e.name);
3935
- const branch = `${EXPLORATION_PREFIX}${slug3}`;
3936
- const uc = useCaseLabel(e.useCase);
3937
- const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
3938
- const lines2 = [
3939
- `## Exploration: ${e.name.trim()}`,
3776
+ "## Authored by",
3940
3777
  "",
3941
- `> Not mergeable as is. This draft pull request is a living prototype on \`${branch}\`: say "Done exploring" when the idea is ready for developers. Keep it open, so its preview link keeps working.`,
3778
+ opts.agent ? "- [ ] Human" : "- [x] Human",
3779
+ opts.agent ? "- [x] AI agent (Claude), reviewed by a human before merge" : "- [ ] AI agent",
3942
3780
  "",
3943
- ...e.purpose?.trim() ? [`**Purpose.** ${e.purpose.trim()}`, ""] : [],
3944
- ...uc ? [`**Use case.** ${uc}`, ""] : [],
3945
- "### What is experimental",
3781
+ "## Design review checklist",
3946
3782
  "",
3947
- ...experimental.length ? experimental.map((x) => `- ${x}`) : [ctx.inProduct ? `- The ${ctx.inProduct.name} screens and flows built on this branch with the library. Nothing here reaches main or production.` : "- Token edits and component work committed to this branch. Nothing here reaches main or production."],
3948
- ...ctx.requests?.length ? ["", "Requests implemented here:", ...ctx.requests.map((r) => `- #${r.number} ${r.title}`)] : [],
3783
+ "- [ ] Checked in light and dark, with real components (Studio preview or Storybook brand switch)",
3784
+ "- [ ] Primary, zone and link colors read as one family",
3785
+ "- [ ] Changeset bump matches the tier",
3949
3786
  ""
3950
- ];
3951
- if (ctx.inProduct) {
3952
- const p = ctx.inProduct;
3953
- lines2.push(
3954
- "### Work on it",
3955
- "",
3956
- `This exploration lives in the ${p.name} repository, so it runs in your usual environment: \`git fetch origin && git switch ${branch}\`, then start ${p.name} as you always do.`,
3957
- "",
3958
- `The library comes from npm, as on main. To try library changes that are not released yet, install the snapshot of their branch in ${p.library} (\`npm i @bridge-ds/react@<snapshot>\`): the command is in its "Try this exploration in your product" comment. Library changes are made in ${p.library}, never on this branch.`,
3959
- "",
3960
- "### Previews",
3961
- ""
3962
- );
3963
- const url = p.preview ? previewUrl(p.preview, { branch, ...ctx.pr ? { pr: ctx.pr } : {} }) : null;
3964
- if (p.preview) lines2.push("| App | Preview |", "| --- | --- |", `| ${p.name} | ${url ?? "Link in the Vercel comment on this pull request"} |`);
3965
- else lines2.push(`The preview deploys of ${p.name}, if it has them, build this branch like any other.`);
3966
- } else {
3967
- lines2.push(
3968
- "### Try it in a product staging branch",
3969
- "",
3970
- 'In the product repo, on a branch that deploys to staging (production keeps the released version), install the snapshot with the command in the "Try this exploration in your product" comment below. It appears a few minutes after each push and always matches the latest commit.'
3971
- );
3972
- lines2.push("", "### Previews", "");
3973
- const links = resolveBranchPreviews(ctx.apps, { branch, ...ctx.pr ? { pr: ctx.pr } : {} });
3974
- if (links.length) lines2.push("| App | Preview |", "| --- | --- |", ...links.map((l) => `| ${l.label} | ${l.url ?? "Link in the Vercel comment on this pull request"} |`));
3975
- else lines2.push("Preview deploys are not set up: add `previews.apps` to `bridge.config.json` (docs/PREVIEWS.md).");
3787
+ ].join("\n");
3788
+ }
3789
+
3790
+ // ../core/src/docs-draft.ts
3791
+ var DOCS_KEYS = ["summary", "preview", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines", "anatomy"];
3792
+ var PROSE_KEYS = ["summary", "whenToUse", "whenNotToUse", "dos", "donts", "accessibility", "contentGuidelines"];
3793
+ var ACRONYMS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "xs"]);
3794
+ function anatomyLabel(className) {
3795
+ if (ACRONYMS.has(className)) return `Size ${className}`;
3796
+ const words = className.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().toLowerCase();
3797
+ if (!words) return className;
3798
+ if (words === "root") return "Container";
3799
+ return words.charAt(0).toUpperCase() + words.slice(1);
3800
+ }
3801
+ function cssModuleClasses(css) {
3802
+ const clean = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
3803
+ const out = [];
3804
+ for (const m of clean.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) {
3805
+ const before = clean[m.index - 1] ?? " ";
3806
+ if (/[\w)]/.test(before)) continue;
3807
+ if (!out.includes(m[1])) out.push(m[1]);
3976
3808
  }
3977
- lines2.push(
3809
+ return out;
3810
+ }
3811
+ var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
3812
+ function propsTable(name, props, inherits) {
3813
+ if (!props.length) return inherits ? `${name} has no props of its own. It accepts every native <${inherits}> attribute.` : "";
3814
+ const rows = props.map((p) => `| \`${p.name}\`${p.required ? " (required)" : ""} | \`${cell(p.type)}\` | ${p.defaultValue ? `\`${cell(p.defaultValue)}\`` : ""} | ${cell(p.description ?? "")} |`);
3815
+ const tail = inherits ? `
3816
+
3817
+ Also accepts every native <${inherits}> attribute.` : "";
3818
+ return ["| Prop | Type | Default | Description |", "| --- | --- | --- | --- |", ...rows].join("\n") + tail;
3819
+ }
3820
+ var isEmpty = (v) => v === void 0 || v === null || typeof v === "string" && !v.trim() || Array.isArray(v) && v.length === 0;
3821
+ function missingProse(d) {
3822
+ return PROSE_KEYS.filter((k) => isEmpty(d?.[k]));
3823
+ }
3824
+ function prefillDocs(input) {
3825
+ const existing = input.existing ?? null;
3826
+ const anatomy = {};
3827
+ for (const c of input.cssClasses) anatomy[c] = existing?.anatomy?.[c] ?? anatomyLabel(c);
3828
+ const contrastNotes = (input.contrast ?? []).slice().sort((a, b) => a.ratio - b.ratio).map((p) => `${p.foreground} on ${p.background}: ${p.ratio.toFixed(2)}:1 at worst over every brand and theme, needs ${p.min}:1 (${p.ratio >= p.min ? "passes" : "fails"}).`);
3829
+ const missing = missingProse(existing);
3830
+ const changelogLine = input.change ? `${input.name}: ${input.change.replace(/\.$/, "")}.` : missing.length === PROSE_KEYS.length ? `${input.name}: usage guidance added.` : `${input.name}: usage guidance updated.`;
3831
+ const draft = {
3832
+ summary: existing?.summary ?? input.description ?? "",
3833
+ ...existing?.category ? { category: existing.category } : {},
3834
+ ...existing?.status ? { status: existing.status } : {},
3835
+ ...existing?.preview ? { preview: existing.preview } : {},
3836
+ whenToUse: existing?.whenToUse ?? [],
3837
+ whenNotToUse: existing?.whenNotToUse ?? [],
3838
+ dos: existing?.dos ?? [],
3839
+ donts: existing?.donts ?? [],
3840
+ accessibility: existing?.accessibility ?? [],
3841
+ contentGuidelines: existing?.contentGuidelines ?? [],
3842
+ anatomy,
3843
+ ...existing?.draftedBy ? { draftedBy: existing.draftedBy } : {},
3844
+ ...existing?.draftedSections?.length ? { draftedSections: existing.draftedSections } : {},
3845
+ ...existing?.needsReview !== void 0 ? { needsReview: existing.needsReview } : {}
3846
+ };
3847
+ return {
3848
+ component: input.name,
3849
+ propsTable: propsTable(input.name, input.props, input.inherits),
3850
+ tokens: [...new Set(input.tokens)].sort(),
3851
+ inheritedTokens: input.inheritedTokens ?? {},
3852
+ anatomy,
3853
+ contrastNotes,
3854
+ changelogLine,
3855
+ missing,
3856
+ draft
3857
+ };
3858
+ }
3859
+ var SECTION_SHAPE = {
3860
+ summary: '"summary": "one or two sentences"',
3861
+ whenToUse: '"whenToUse": ["..."]',
3862
+ whenNotToUse: '"whenNotToUse": ["..."]',
3863
+ dos: '"dos": [{ "title": "...", "text": "..." }]',
3864
+ donts: '"donts": [{ "title": "...", "text": "..." }]',
3865
+ accessibility: '"accessibility": ["..."]',
3866
+ contentGuidelines: '"contentGuidelines": ["..."]'
3867
+ };
3868
+ var docsSectionShapes = (keys) => `{ ${keys.map((k) => SECTION_SHAPE[k]).join(", ")} }`;
3869
+ function docsPrompt(prefill, conventions = {}, options = {}) {
3870
+ const facts = [
3871
+ `Component: ${prefill.component}`,
3872
+ prefill.propsTable ? `Props (parsed from TypeScript):
3873
+ ${prefill.propsTable}` : "Props: none of its own.",
3874
+ `Tokens its stylesheet reads: ${prefill.tokens.join(", ") || "none"}`,
3875
+ ...Object.entries(prefill.inheritedTokens).map(([from, list]) => `Tokens it reads through ${from}'s stylesheet: ${list.length}`),
3876
+ `Anatomy (CSS module classes): ${Object.entries(prefill.anatomy).map(([k, v]) => `${k} = ${v}`).join("; ") || "none"}`,
3877
+ prefill.contrastNotes.length ? `Contrast checks:
3878
+ - ${prefill.contrastNotes.join("\n- ")}` : "",
3879
+ `Changelog line: ${prefill.changelogLine}`
3880
+ ].filter(Boolean);
3881
+ const block = (title, text) => text?.trim() ? `<${title}>
3882
+ ${text.trim()}
3883
+ </${title}>` : "";
3884
+ const keys = options.keys;
3885
+ const check = keys ? (options.check ?? []).filter((k) => keys.includes(k)) : [];
3886
+ const write = keys ? keys.filter((k) => !check.includes(k)) : [];
3887
+ const task = keys ? [
3888
+ ...write.length ? [`${options.rewrite ? "Rewrite" : "Write"} these sections of ${prefill.component}.docs.json for the Bridge design system: ${write.join(", ")}.`] : [],
3889
+ ...check.length ? [
3890
+ `Check these written sections of ${prefill.component}.docs.json against the current code: ${check.join(", ")}. The code changed after they were written.`,
3891
+ "Return a checked section ONLY when something in it is no longer true for the code (a wrong or removed prop, a removed state or variant, changed behavior). Then return the whole section with only the untrue parts fixed, keeping the writer's wording everywhere else.",
3892
+ "Leave a checked section out of the answer when it is still accurate: a missing key means unchanged. Never return a checked section just to improve its style."
3893
+ ] : [],
3894
+ ...options.rewrite ? ["Rewrite each one from the facts and the source: keep what is true in the current text, drop what the code does not support."] : []
3895
+ ] : [`Write the missing prose of ${prefill.component}.docs.json for the Bridge design system.`];
3896
+ const shape2 = keys ? [
3897
+ `- ${check.length ? "Return" : "Write"} only these keys: ${keys.join(", ")}${check.length ? ` (the checked ones, ${check.join(", ")}, only when they need a fix)` : ""}. Never return anatomy, preview or any other key: code decides them.`,
3898
+ `- Answer with one JSON object holding only those keys, nothing else: ${docsSectionShapes(keys)}.`
3899
+ ] : [
3900
+ `- Fill only these keys: ${prefill.missing.join(", ") || "none (everything is written: improve nothing, return the draft as is)"}. Keep every other key exactly as given.`,
3901
+ "- Answer with the full docs.json as one JSON object, nothing else."
3902
+ ];
3903
+ return [
3904
+ ...task,
3978
3905
  "",
3979
- "### When it is done",
3906
+ "Rules:",
3907
+ "- Ground every sentence in the facts and the source below. Never invent props, variants, keyboard behavior or tokens that are not there.",
3908
+ "- Bridge voice: sentence case, plain words, verbs first, no em dashes, no marketing words.",
3909
+ '- Keep the keys and shapes of the example docs.json. dos and donts are { "title", "text" } objects; add an "example" only with components and props that exist.',
3910
+ ...shape2,
3980
3911
  "",
3981
- `- **Done exploring:** on the exploration's page in Bridge Studio, or \`bridge explore done ${slug3}\`. Bridge removes the \`${EXPLORATION_LABEL}\` label and the exploration note, adds \`${EXPLORATION_READY_LABEL}\`, and sends the pieces the designer picks to the DS lead as requests. Nothing is closed or merged: developers continue on \`${branch}\`.`,
3982
- `- **Archive:** \`bridge explore archive ${slug3}\` adds the \`${EXPLORATION_ARCHIVED_LABEL}\` label. Keep this pull request open: ${ctx.inProduct ? "Bridge follows open explorations, and its preview link is how people come back to the prototype." : "closing it removes the preview prerelease."}`,
3912
+ "Facts computed from the code:",
3913
+ facts.join("\n\n"),
3983
3914
  "",
3984
- explorationBlock({ ...e, slug: slug3 })
3985
- );
3986
- return lines2.join("\n");
3915
+ `Current draft:
3916
+ ${JSON.stringify(prefill.draft, null, 2)}`,
3917
+ "",
3918
+ block("source", conventions.source),
3919
+ block("styles", conventions.styles),
3920
+ block("agents-md", conventions.agents),
3921
+ block("voice", conventions.voice),
3922
+ block("example-docs-json", conventions.example)
3923
+ ].filter((x, i, all) => x !== "" || all[i - 1] !== "").join("\n");
3987
3924
  }
3988
- function explorationBlock(e) {
3989
- const experimental = (e.experimental ?? []).map((x) => x.trim()).filter(Boolean);
3990
- const data = {
3991
- name: e.name.trim(),
3992
- purpose: (e.purpose ?? "").trim(),
3993
- slug: e.slug || explorationSlug(e.name),
3994
- ...e.useCase ? { useCase: e.useCase } : {},
3995
- ...experimental.length ? { experimental } : {},
3996
- ...e.author ? { author: e.author } : {},
3997
- ...e.startedAt ? { startedAt: e.startedAt } : {},
3998
- ...e.source ? { source: e.source } : {},
3999
- ...e.agent ? { agent: e.agent } : {},
4000
- ...e.doneAt ? { doneAt: e.doneAt } : {},
4001
- ...e.doneSha ? { doneSha: e.doneSha } : {}
4002
- };
4003
- return `<!-- ${MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
3925
+ function markClaudeDraft(d, sections2) {
3926
+ if (!sections2) {
3927
+ const { draftedSections: _all, ...rest } = d;
3928
+ return { ...rest, draftedBy: "claude", needsReview: true };
3929
+ }
3930
+ const waiting = d.needsReview === true ? d.draftedSections ?? (d.draftedBy ? [...PROSE_KEYS] : []) : [];
3931
+ const union = PROSE_KEYS.filter((k) => waiting.includes(k) || sections2.includes(k));
3932
+ return { ...d, draftedBy: "claude", needsReview: true, draftedSections: union };
4004
3933
  }
4005
- function parseExplorationBody(body) {
4006
- if (!body) return void 0;
4007
- const m = new RegExp(`<!-- ${MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
4008
- if (!m) return void 0;
4009
- try {
4010
- const e = JSON.parse(m[1]);
4011
- if (!e || typeof e.name !== "string") return void 0;
4012
- return typeof e.purpose === "string" ? e : e.purpose === void 0 ? { ...e, purpose: "" } : void 0;
3934
+ function formatDocsJson(d) {
3935
+ const order = ["summary", "category", "status", ...DOCS_KEYS.filter((k) => k !== "summary"), "draftedBy", "draftedSections", "needsReview"];
3936
+ const out = {};
3937
+ for (const k of order) if (d[k] !== void 0) out[k] = d[k];
3938
+ for (const [k, v] of Object.entries(d)) if (!(k in out) && v !== void 0) out[k] = v;
3939
+ return `${JSON.stringify(out, null, 2)}
3940
+ `;
3941
+ }
3942
+ function patchNode(old, next) {
3943
+ if (plainEqual(toPlain(old), next)) return old;
3944
+ if (old.kind === "object" && next && typeof next === "object" && !Array.isArray(next)) {
3945
+ const n = next;
3946
+ const kept = old.entries.filter((e) => n[e.key] !== void 0).map((e) => ({ key: e.key, value: patchNode(e.value, n[e.key]) }));
3947
+ const added = Object.keys(n).filter((k) => n[k] !== void 0 && !getEntry(old, k)).map((k) => ({ key: k, value: fromPlain(n[k], void 0, old.inline) }));
3948
+ return { kind: "object", inline: old.inline, entries: [...kept, ...added] };
3949
+ }
3950
+ if (old.kind === "array" && Array.isArray(next)) {
3951
+ return { kind: "array", inline: old.inline, items: next.map((v, i) => old.items[i] ? patchNode(old.items[i], v) : fromPlain(v, old.items[0], old.inline)) };
3952
+ }
3953
+ return fromPlain(next, old);
3954
+ }
3955
+ function patchDocsJson(original, next) {
3956
+ const plain2 = JSON.parse(formatDocsJson(next));
3957
+ if (!original) return formatDocsJson(next);
3958
+ try {
3959
+ const doc = parseJson(original);
3960
+ if (doc.root.kind !== "object") return formatDocsJson(next);
3961
+ return serializeJson({ ...doc, root: patchNode(doc.root, plain2) });
4013
3962
  } catch {
4014
- return void 0;
3963
+ return formatDocsJson(next);
4015
3964
  }
4016
3965
  }
4017
- var labelNames = (labels) => labels.map((l) => typeof l === "string" ? l : l.name ?? "");
4018
- function isExplorationPull(p) {
4019
- return isExplorationBranch(p.headRef) || labelNames(p.labels).includes(EXPLORATION_LABEL) || Boolean(parseExplorationBody(p.body));
3966
+
3967
+ // ../core/src/change-drafts.ts
3968
+ function cleanDraftText(text) {
3969
+ return text.replace(/\s*\u2014\s*/g, ", ").replace(/,\s*,/g, ",").replace(/^,\s*/, "").replace(/[ \t]+/g, " ").trim();
4020
3970
  }
4021
- function isUnlabeledExploration(p) {
4022
- if (p.state !== "open" || p.draft !== true) return false;
4023
- const names4 = labelNames(p.labels ?? []);
4024
- if (names4.includes(EXPLORATION_LABEL) || names4.includes(EXPLORATION_ARCHIVED_LABEL) || names4.includes(EXPLORATION_READY_LABEL)) return false;
4025
- const block = parseExplorationBody(p.body);
4026
- if (block?.doneAt) return false;
4027
- return isExplorationBranch(p.headRef) || Boolean(block);
3971
+ var andList = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
3972
+ var capital = (s) => s.charAt(0).toUpperCase() + s.slice(1);
3973
+ var VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
3974
+ function shortVariant(key) {
3975
+ const pairs = key.split(", ").map((p) => {
3976
+ const at = p.indexOf("=");
3977
+ return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
3978
+ });
3979
+ const kept = pairs.filter((p) => p.v && !/^default$/i.test(p.v));
3980
+ if (!kept.length) return key;
3981
+ const rank2 = (k) => {
3982
+ const i = VARIANT_FIRST.indexOf(k.toLowerCase());
3983
+ return i < 0 ? VARIANT_FIRST.length : i;
3984
+ };
3985
+ return kept.map((p, i) => ({ ...p, i })).sort((a, b) => rank2(a.k) - rank2(b.k) || a.i - b.i).map((p) => p.v).join(" ");
4028
3986
  }
4029
- function isHandedOff(p) {
4030
- return labelNames(p.labels).includes(EXPLORATION_READY_LABEL) || Boolean(parseExplorationBody(p.body)?.doneAt);
3987
+ var shortVariants = (list) => list ? andList(list.split("; ").map(shortVariant)) : "every variant";
3988
+ var DRAFT_LINE_MAX = 120;
3989
+ function clipText(text, max) {
3990
+ const t = text.replace(/\s+/g, " ").trim().replace(/\.$/, "");
3991
+ if (t.length <= max) return t;
3992
+ const cut2 = t.slice(0, max + 1);
3993
+ const at = cut2.lastIndexOf(" ");
3994
+ return (at > max * 0.5 ? cut2.slice(0, at) : t.slice(0, max)).replace(/[\s,;:(]+$/, "");
4031
3995
  }
4032
- function explorationStatus(p) {
4033
- const merged = Boolean(p.merged || p.merged_at || p.state === "merged");
4034
- if (p.state !== "open" && !merged) return "closed";
4035
- if (p.state === "open" && (labelNames(p.labels).includes(EXPLORATION_LABEL) || isUnlabeledExploration(p))) return "active";
4036
- if (!isHandedOff(p)) return "promoted";
4037
- if (merged) return "shipped";
4038
- if (p.draft === false) return "building";
4039
- const doneSha = parseExplorationBody(p.body)?.doneSha;
4040
- if (doneSha && p.headSha && p.headSha !== doneSha) return "building";
4041
- return "ready";
3996
+ function sanitizeDraftLine(raw) {
3997
+ if (typeof raw !== "string") return "";
3998
+ return capital(clipText(cleanDraftText(raw.replace(/[\r\n]+/g, " ")), DRAFT_LINE_MAX));
4042
3999
  }
4043
- var isActiveExploration = (labels) => labelNames(labels).includes(EXPLORATION_LABEL);
4044
- var isArchivedExploration = (labels) => labelNames(labels ?? []).includes(EXPLORATION_ARCHIVED_LABEL);
4045
- function isHeldExploration(p) {
4046
- if (p.state !== void 0 && p.state !== "open") return false;
4047
- const names4 = labelNames(p.labels ?? []);
4048
- if (names4.includes(EXPLORATION_LABEL) || names4.includes(EXPLORATION_ARCHIVED_LABEL)) return true;
4049
- if (isUnlabeledExploration({ state: "open", draft: p.draft, labels: p.labels, headRef: p.headRef ?? p.head?.ref, body: p.body })) return true;
4050
- return names4.includes(EXPLORATION_READY_LABEL) && p.draft !== false;
4000
+
4001
+ // ../core/src/docs-ai.ts
4002
+ var DOCS_DRAFT_MODEL = "claude-sonnet-5-5";
4003
+ var ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
4004
+ var DOCS_SELECTION_MODES = {
4005
+ clearer: "Make it clearer: plain words, one idea per sentence, the most useful fact first.",
4006
+ shorter: "Make it shorter: keep every fact, drop filler, aim for half the words or fewer.",
4007
+ voice: "Put it in the Bridge voice: sentence case, verbs first, calm and precise, no marketing words, no em dash."
4008
+ };
4009
+ var isDocsSelectionMode = (m) => typeof m === "string" && m in DOCS_SELECTION_MODES;
4010
+ var isProseKey = (k) => typeof k === "string" && PROSE_KEYS.includes(k);
4011
+ function validateDocsTask(x) {
4012
+ if (!x || typeof x !== "object") return ["The docs task is missing."];
4013
+ const t = x;
4014
+ if (t.kind === "sections") {
4015
+ const keys = t.keys;
4016
+ if (!Array.isArray(keys) || !keys.length || !keys.every(isProseKey)) return [`The docs task lists no sections: use ${PROSE_KEYS.join(", ")}.`];
4017
+ if (t.check !== void 0 && (!Array.isArray(t.check) || !t.check.every(isProseKey))) return ["check must list prose sections."];
4018
+ return [];
4019
+ }
4020
+ if (t.kind === "selection") {
4021
+ const out = [];
4022
+ if (!isProseKey(t.key)) out.push(`The selection needs a section: ${PROSE_KEYS.join(", ")}.`);
4023
+ if (typeof t.text !== "string" || !t.text.trim()) out.push("The selection has no text.");
4024
+ if (!isDocsSelectionMode(t.mode)) out.push("The selection mode must be clearer, shorter or voice.");
4025
+ return out;
4026
+ }
4027
+ return ['The docs task kind must be "sections" or "selection".'];
4051
4028
  }
4052
- var explorationTakeCommands = (branch) => ["git fetch origin", `git checkout ${branch}`];
4053
- var CONTINUE = "Continue on this branch: its pull request already exists, so Bridge follows your pushes, and the merge reads as shipped.";
4054
- var EXPLORATION_STARTING_POINT = `A starting point, not ready to ship: nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
4055
- function explorationStartingPoint(checks) {
4056
- switch (checks) {
4057
- case "success":
4058
- return `A starting point: its checks pass, but nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
4059
- case "failure":
4060
- return `A starting point, not ready to ship: some checks fail on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
4061
- case "pending":
4062
- return `A starting point, not ready to ship: its checks are still running and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
4063
- case "none":
4064
- return `A starting point, not ready to ship: no checks ran on this branch and nobody has reviewed Claude's prototype code yet. ${CONTINUE}`;
4065
- default:
4066
- return EXPLORATION_STARTING_POINT;
4029
+ var docsTaskKeys = (t) => t.kind === "sections" ? t.keys : [t.key];
4030
+ function parseDocsReply(text) {
4031
+ const fenced2 = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
4032
+ const body = fenced2 ? fenced2[1] : text;
4033
+ const start = body.indexOf("{");
4034
+ const end = body.lastIndexOf("}");
4035
+ if (start < 0 || end <= start) throw new Error("The reply holds no JSON object.");
4036
+ const json = JSON.parse(body.slice(start, end + 1));
4037
+ if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("The reply is not a docs.json object.");
4038
+ return json;
4039
+ }
4040
+ var cleanDocsText = (s) => cleanDraftText(s.replace(/[\r\n]+/g, " "));
4041
+ var isExample = (x) => {
4042
+ if (typeof x === "string") return true;
4043
+ if (!x || typeof x !== "object" || Array.isArray(x)) return false;
4044
+ const n = x;
4045
+ if (typeof n.c !== "string" || !n.c) return false;
4046
+ if (n.p !== void 0 && (!n.p || typeof n.p !== "object" || Array.isArray(n.p))) return false;
4047
+ return n.children === void 0 || Array.isArray(n.children) && n.children.every(isExample);
4048
+ };
4049
+ var textList = (x) => {
4050
+ if (typeof x === "string") x = [x];
4051
+ if (!Array.isArray(x)) return void 0;
4052
+ const out = x.filter((s) => typeof s === "string").map(cleanDocsText).filter(Boolean);
4053
+ return out.length ? out : void 0;
4054
+ };
4055
+ var guidanceList = (x, examples) => {
4056
+ if (!Array.isArray(x)) return void 0;
4057
+ const out = [];
4058
+ for (const g of x) {
4059
+ if (!g || typeof g !== "object") continue;
4060
+ const r = g;
4061
+ const title = typeof r.title === "string" ? cleanDocsText(r.title) : "";
4062
+ const text = typeof r.text === "string" ? cleanDocsText(r.text) : "";
4063
+ if (!title || !text) continue;
4064
+ out.push({ title, text, ...examples && r.example !== void 0 && isExample(r.example) ? { example: r.example } : {} });
4065
+ }
4066
+ return out.length ? out : void 0;
4067
+ };
4068
+ function sanitizeDocsSections(raw, keys = PROSE_KEYS, opts = {}) {
4069
+ const out = {};
4070
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out;
4071
+ const r = raw;
4072
+ for (const k of keys) {
4073
+ const v = r[k];
4074
+ if (k === "summary") {
4075
+ const s = typeof v === "string" ? cleanDocsText(v) : "";
4076
+ if (s) out.summary = s;
4077
+ } else if (k === "dos" || k === "donts") {
4078
+ const g = guidanceList(v, opts.examples ?? false);
4079
+ if (g) out[k] = g;
4080
+ } else {
4081
+ const l = textList(v);
4082
+ if (l) out[k] = l;
4083
+ }
4067
4084
  }
4085
+ return out;
4068
4086
  }
4069
- function doneExploringPlan(input) {
4070
- const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
4071
- return {
4072
- removeLabels: [EXPLORATION_LABEL],
4073
- addLabels: [EXPLORATION_READY_LABEL],
4074
- removeFiles,
4075
- body: withDoneExploring(input.body, { branch: input.branch, doneAt: (input.date ?? /* @__PURE__ */ new Date()).toISOString() }),
4076
- take: explorationTakeCommands(input.branch),
4077
- markReady: false
4078
- };
4087
+ function sanitizeDocsReplacement(raw) {
4088
+ if (typeof raw !== "string") return "";
4089
+ return cleanDocsText(raw.trim().replace(/^["'“]+|["'”]+$/g, ""));
4079
4090
  }
4080
- var DEV_SECTION = "### For developers";
4081
- function withDoneExploring(body, done) {
4082
- const e = parseExplorationBody(body);
4083
- const [fetch2, checkout] = explorationTakeCommands(done.branch);
4084
- const section = [
4085
- DEV_SECTION,
4086
- "",
4087
- `**Done exploring on ${done.doneAt.slice(0, 10)}.** ${explorationStartingPoint(done.checks)}`,
4088
- "",
4089
- "```sh",
4090
- fetch2,
4091
- checkout,
4092
- "```",
4093
- "",
4094
- "Mark this pull request ready for review when the code is yours. Keep it open: its preview link is how designers, the DS lead and stakeholders come back to the prototype."
4095
- ].join("\n");
4096
- const block = new RegExp(`<!-- ${MARK} \\{[\\s\\S]*?\\} -->`);
4097
- const kept = (body ?? "").replace(/^> Not mergeable as is\..*$\n?\n?/m, "").replace(/### When it is done[\s\S]*?(?=<!-- |$)/, "").replace(/### For developers[\s\S]*?(?=### |<!-- |$)/, "");
4098
- if (!e || !block.test(kept)) return `${kept.trim()}${kept.trim() ? "\n\n" : ""}${section}`;
4099
- const at = kept.search(block);
4100
- const nextBlock = explorationBlock({ ...e, doneAt: done.doneAt, ...done.doneSha ? { doneSha: done.doneSha } : {} });
4101
- const after = kept.slice(at).replace(block, () => nextBlock).trim();
4102
- return `${kept.slice(0, at).trim()}
4103
-
4104
- ${section}
4105
-
4106
- ${after}`.trim();
4091
+ function docsSectionsAnswer(task, raw) {
4092
+ const check = (task.check ?? []).filter((k) => task.keys.includes(k));
4093
+ const sections2 = check.length ? sanitizeDocsSections(raw, task.keys) : docsSectionsResult(task, raw);
4094
+ return { sections: sections2, checked: check.filter((k) => sections2[k] === void 0) };
4107
4095
  }
4108
- function explorationTryIt(input) {
4109
- const c = readPreviewComment(input.comment);
4110
- if (!c) return { state: "building" };
4111
- if (input.headSha && c.sha7 && !input.headSha.startsWith(c.sha7)) return { state: "building" };
4112
- if (!c.command || c.source === "none") return { state: "failed" };
4113
- return { state: "ready", command: c.command, source: c.source, sha7: c.sha7 };
4096
+ function docsSectionsResult(task, raw) {
4097
+ const sections2 = sanitizeDocsSections(raw, task.keys);
4098
+ if (!Object.keys(sections2).length) throw new Error(`Claude's answer holds none of the sections asked for (${task.keys.join(", ")}). Try again.`);
4099
+ return sections2;
4114
4100
  }
4115
- function explorationRules(branch) {
4116
- return [
4117
- `- Work on the existing branch ${branch} (git fetch origin && git switch ${branch}). Commit there.`,
4118
- `- Do not open a pull request to main and do not create another branch: the draft pull request of ${branch} is the place. It stays open as a living prototype.`,
4119
- `- Do not mark the pull request ready for review, do not remove the exploration label and never close it: "Done exploring" is a person's decision, and closing removes its preview.`
4120
- ];
4101
+ function mergeDocsDraft(prefillDraft, reply) {
4102
+ const empty = PROSE_KEYS.filter((k) => {
4103
+ const v = prefillDraft[k];
4104
+ return v === void 0 || (typeof v === "string" ? !v.trim() : Array.isArray(v) && !v.length);
4105
+ });
4106
+ const sections2 = sanitizeDocsSections(reply, empty, { examples: true });
4107
+ const merged = { ...prefillDraft, ...sections2 };
4108
+ if (reply.preview && !prefillDraft.preview && isExample(reply.preview)) merged.preview = reply.preview;
4109
+ merged.anatomy = prefillDraft.anatomy;
4110
+ const drafted = PROSE_KEYS.filter((k) => sections2[k] !== void 0);
4111
+ return markClaudeDraft(merged, drafted.length && drafted.length < PROSE_KEYS.length ? drafted : void 0);
4112
+ }
4113
+ function anthropicRequest(input) {
4114
+ const headers = { "x-api-key": input.apiKey, "anthropic-version": "2023-06-01", "content-type": "application/json" };
4115
+ if (input.fallback) headers["anthropic-beta"] = "server-side-fallback-2026-07-01";
4116
+ if (input.browser) headers["anthropic-dangerous-direct-browser-access"] = "true";
4117
+ const body = {
4118
+ model: input.model || DOCS_DRAFT_MODEL,
4119
+ max_tokens: input.maxTokens ?? 16e3,
4120
+ ...input.fallback ? { fallbacks: "default" } : {},
4121
+ ...input.system ? { system: input.system } : {},
4122
+ messages: [{ role: "user", content: input.prompt }]
4123
+ };
4124
+ return { url: ANTHROPIC_MESSAGES_URL, init: { method: "POST", headers, body: JSON.stringify(body) } };
4125
+ }
4126
+ function anthropicReplyText(msg) {
4127
+ const m = msg ?? {};
4128
+ if (m.stop_reason === "refusal") throw new Error("Claude declined to draft this one.");
4129
+ if (m.stop_reason === "max_tokens") throw new Error("The reply was cut off (max_tokens).");
4130
+ return (m.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
4121
4131
  }
4122
4132
 
4123
4133
  // ../core/src/titles.ts
@@ -6089,7 +6099,7 @@ async function doneExploringOnGitHub(gh, ref, input = {}) {
6089
6099
  const files = fresh ? await gh.listPullRequestFiles(p.number) : [];
6090
6100
  const plan = fresh ? doneExploringPlan({ body: p.body, branch: p.headRef, files: files.map((f) => ({ path: f.filename, status: f.status })), date: input.date }) : null;
6091
6101
  const take = plan?.take ?? ["git fetch origin", `git checkout ${p.headRef}`];
6092
- const checks = p.headSha ? await Promise.resolve(p.headSha).then((sha) => gh.getChecks(sha)).then((c) => c.state, () => null) : null;
6102
+ const checks = p.headSha ? await Promise.resolve(p.headSha).then((sha) => gh.getChecks(sha, { exploration: isGuardedExploration({ state: p.state, draft: p.draft, labels: p.labels, headRef: p.headRef, body: p.body }) })).then((c) => c.state, () => null) : null;
6093
6103
  const out = { number: p.number, url: p.url, branch: p.headRef, status: fresh ? "ready" : status, handedOff: fresh, plan, pieces: { proposed: [], already: [], unknown: [], warnings: [] }, take, checks, note: explorationStartingPoint(checks), warnings: [] };
6094
6104
  const products = input.products ?? [];
6095
6105
  const ask = { pieces: input.pieces ?? [], products, previewUrl: input.previewUrl, openedBy: input.openedBy, agent: input.agent, source: input.source, library: input.library };
@@ -8186,11 +8196,13 @@ function isPlatformRefusal(s) {
8186
8196
  return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
8187
8197
  }
8188
8198
  var OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
8189
- function summarizeCi(runs, statuses = []) {
8199
+ function summarizeCi(runs, statuses = [], opts = {}) {
8190
8200
  const failed = [];
8191
8201
  const pending = [];
8192
8202
  let passed = 0;
8203
+ const skip = (name) => Boolean(opts.exploration) && isExplorationCheck(name);
8193
8204
  for (const r of runs) {
8205
+ if (skip(r.name)) continue;
8194
8206
  if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
8195
8207
  else if (r.conclusion && OK.has(r.conclusion)) passed++;
8196
8208
  else failed.push({ name: r.name, url: r.html_url });
@@ -8199,7 +8211,7 @@ function summarizeCi(runs, statuses = []) {
8199
8211
  for (const s of statuses) {
8200
8212
  if (seen.has(s.context)) continue;
8201
8213
  seen.add(s.context);
8202
- if (isPlatformRefusal(s)) continue;
8214
+ if (isPlatformRefusal(s) || skip(s.context)) continue;
8203
8215
  if (s.state === "success") passed++;
8204
8216
  else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
8205
8217
  else failed.push({ name: s.context, url: s.target_url ?? void 0 });
@@ -13866,7 +13878,9 @@ async function branchFacts(ctx, gh, n, viewer) {
13866
13878
  const labels = pr.labels.map((name) => ({ name }));
13867
13879
  const ci = summarizeCi(
13868
13880
  runs.map((r) => ({ name: r.name, status: r.status, conclusion: r.conclusion, ...r.url ? { html_url: r.url } : {} })),
13869
- statuses.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url, description: s.description }))
13881
+ statuses.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url, description: s.description })),
13882
+ // An exploration's guard check fails by design: never a failing test (core isGuardedExploration).
13883
+ { exploration: isGuardedExploration({ state: pr.state, draft: pr.draft, labels: pr.labels, headRef: pr.headRef, body: pr.body }) }
13870
13884
  );
13871
13885
  const reviewLikes = reviews.map((r) => ({ user: r.user ? { login: r.user } : null, state: r.state, submitted_at: r.submittedAt ?? null, commit_id: r.commitId ?? null }));
13872
13886
  const lookAgain = pr.state === "open" ? lookAgainReviewers(reviewLikes, pr.headSha, pr.author) : [];
@@ -15648,7 +15662,7 @@ async function getReview(ctx, a) {
15648
15662
  const gh = await ctx.github();
15649
15663
  if (!gh) throw new Error('No GitHub repo known: set "repo" in bridge.config.json.');
15650
15664
  const pr = await gh.getPullRequest(a.pr);
15651
- const [files, checks, reviews] = await Promise.all([gh.listPullRequestFiles(a.pr), gh.getChecks(pr.headSha), gh.listReviews(a.pr)]);
15665
+ const [files, checks, reviews] = await Promise.all([gh.listPullRequestFiles(a.pr), gh.getChecks(pr.headSha, { exploration: isGuardedExploration({ state: pr.state, draft: pr.draft, labels: pr.labels, headRef: pr.headRef, body: pr.body }) }), gh.listReviews(a.pr)]);
15652
15666
  baseSrc = new GitHubSource(gh, pr.baseRef);
15653
15667
  headSrc = new GitHubSource(gh, pr.headSha);
15654
15668
  changedFiles = files.map((f) => f.filename);
@@ -17154,7 +17168,7 @@ async function proposeFigmaFixesOp(ctx, a) {
17154
17168
  }
17155
17169
 
17156
17170
  // src/server.ts
17157
- var VERSION = true ? "0.11.2" : "dev";
17171
+ var VERSION = true ? "0.11.3" : "dev";
17158
17172
  var BRIDGE_SITE = "https://bridge.floriangouloubi.com";
17159
17173
  var SERVER_INFO = {
17160
17174
  name: "bridge",